on 75a174f help sign in
streamly/docs/User/Tutorials/Introduction.md
675 lines 26 kB history raw
1# [Streamly][]: Idiomatic Haskell with the Performance of C
3[![Gitter chat](https://badges.gitter.im/composewell/gitter.svg)](https://gitter.im/composewell/streamly)
4[![Hackage](https://img.shields.io/hackage/v/streamly.svg?style=flat)](https://hackage.haskell.org/package/streamly)
6Streamly is a Haskell library that provides the building blocks to build
7safe, scalable, modular and high performance software. Streamly offers:
9* The type safety of Haskell.
10* The performance of C programs.
11* Powerful abstractions for structuring your code.
12* Idiomatic functional programming.
13* Declarative concurrency for the seamless use of multiprocessing hardware.
15## About This Document
17This guide introduces programming with [Streamly][] using a few practical
18examples:
20* We will start with a simple program that [counts the number of words
21 in a text](#modular-word-counting). We will then transform this program
22 into a [concurrent](#concurrent-word-counting) program that can efficiently
23 use multiprocessing hardware.
24* Next, we will create a [concurrent network
25 server](#a-concurrent-network-server). We then show
26 how to write a network server that [merges multiple
27 streams](#merging-incoming-streams) concurrently.
28* Our third example shows how to list a directory tree concurrently,
29 by reading [multiple directories in
30 parallel](#listing-directories-recursivelyconcurrently).
31* Finally, we will look at how to [rate limit](#rate-limiting) stream
32 processing.
34The guide then looks at how Streamly achieves its
35[performance](#performance). It [concludes](#notes) with a brief
36discussion about Streamly's design philosophy, and with suggestions for
37further reading.
39## Getting Started
41### Installing Streamly
43If you wish to follow along with this guide, you will need to have
44[Streamly][] installed.
46Please see the [Getting Started With The Streamly Package](/docs/User/Tutorials/getting-started.md)
47guide for instructions on how to install [Streamly][].
49If you wish to run benchmarks, please be sure to build your
50application using the instructions in the [Build Guide](/docs/User/HowTo/Compiling.md).
52### An overview of the types used in these examples
54As an expository device, we have indicated the types at the intermediate
55stages of stream computations as comments in the examples below.
56The meaning of these types are:
58* A `Stream IO a` is a serial stream of values of type `a` in the IO Monad.
59* An `Unfold IO a b` is a representation of a function that converts a seed
60 value of type `a` into a stream of values of type `b` in the IO Monad.
61* A `Fold IO a b` is a representation of a function that converts a stream of
62 type `a` to a final accumulator of type `b` in the IO Monad.
64### A Note on Module Naming
66Some of the examples below use modules from the `Internal` Streamly package
67hierarchy. These are not really internal to the library. We classify
68`Streamly` modules into two categories:
70* _Released modules and APIs_: These modules and APIs are
71 stable. Significant changes to these modules and APIs will cause
72 Streamly's version number to change according to the package versioning
73 policy.
74* _Pre-release modules and APIs_: These modules and APIs have not been
75 formally released yet. They may change in the near future, and such
76 changes will not necessarily be reflected in Streamly's package
77 version number. As yet unreleased modules and APIs reside in the
78 `Internal` namespace.
80Please use a minor release upper bound to adhere to the Haskell PVP when
81using the pre-release (internal) modules.
83## The Examples
85### Modular Word Counting
87A `Fold` in Streamly is a composable stream consumer. For our first
88example, we will use `Fold`s to count the number of bytes, words and lines
89present in a file. We will then compose individual `Fold`s together to
90count words, bytes and lines at the same time.
92Please see the file [WordCountModular.hs][] for the complete example
93program, including the imports that we have omitted here.
95#### Count Bytes (wc -c)
97We start with a code fragment that counts the number of bytes in a file:
99```haskell ghci
100import Data.Function ((&))
102import qualified Streamly.Data.Fold as Fold
103import qualified Streamly.Data.Stream as Stream
104import qualified Streamly.FileSystem.File as File
106wcb :: String -> IO Int
107wcb file =
108 File.read file -- Stream IO Word8
109 & Stream.fold Fold.length -- IO Int
110```
112### Count Lines (wc -l)
114The next code fragment shows how to count the number of lines in a file:
116```haskell ghci
117import Data.Word (Word8)
118import Streamly.Data.Fold (Fold)
120-- ASCII character 10 is a newline.
121countl :: Int -> Word8 -> Int
122countl n ch = if ch == 10 then n + 1 else n
124-- The fold accepts a stream of `Word8` and returns a line count (`Int`).
125nlines :: Monad m => Fold m Word8 Int
126nlines = Fold.foldl' countl 0
128wcl :: String -> IO Int
129wcl file =
130 File.read file -- Stream IO Word8
131 & Stream.fold nlines -- IO Int
132```
134### Count Words (wc -w)
136Our final code fragment counts the number of whitespace-separated words
137in a stream:
139```haskell ghci
140import Data.Char (chr, isSpace)
142countw :: (Int, Bool) -> Word8 -> (Int, Bool)
143countw (n, wasSpace) ch =
144 if isSpace $ chr $ fromIntegral ch
145 then (n, True)
146 else (if wasSpace then n + 1 else n, False)
148-- The fold accepts a stream of `Word8` and returns a word count (`Int`).
149nwords :: Monad m => Fold m Word8 Int
150nwords = fst <$> Fold.foldl' countw (0, True)
152wcw :: String -> IO Int
153wcw file =
154 File.read file -- Stream IO Word8
155 & Stream.fold nwords -- IO Int
156```
158### Counting Bytes, Words and Lines Together
160By using the `Tee` combinator we can compose the three folds that count
161bytes, lines and words individually into a single fold that counts all
162three at once. The applicative instance of `Tee` distributes its input
163to all the supplied folds (`Fold.length`, `nlines`, and `nwords`) and
164then combines the outputs from the folds using the supplied combiner
165function (`(,,)`).
167```haskell ghci
168import Streamly.Data.Fold (Tee(..))
170-- The fold accepts a stream of `Word8` and returns the three counts.
171countAll :: Fold IO Word8 (Int, Int, Int)
172countAll = unTee $ (,,) <$> Tee Fold.length <*> Tee nlines <*> Tee nwords
174wc :: String -> IO (Int, Int, Int)
175wc file =
176 File.read file -- Stream IO Word8
177 & Stream.fold countAll -- IO (Int, Int, Int)
178```
180This example demonstrates the excellent modularity offered by
181[Streamly][]'s simple and concise API. Experienced Haskellers will
182notice that we have not used bytestrings&mdash;we instead used a stream of
183`Word8` values, simplifying our program.
185### The Performance of Word Counting
187We compare two equivalent implementations: one using [Streamly][],
188and the other using C.
190The performance of the [Streamly word counting
191implementation][WordCount.hs] is:
193```
194$ time WordCount-hs gutenberg-500MB.txt
19511242220 97050938 574714449 gutenberg-500MB.txt
197real 0m1.825s
198user 0m1.697s
199sys 0m0.128s
200```
202The performance of an equivalent [wc implementation in C][WordCount.c] is:
204```
205$ time WordCount-c gutenberg-500MB.txt
20611242220 97050938 574714449 gutenberg-500MB.txt
208real 0m2.100s
209user 0m1.935s
210sys 0m0.165s
211```
213### Concurrent Word Counting
215In our next example we show how the task of counting words, lines,
216and bytes could be done in parallel on multiprocessor hardware.
218To count words in parallel we first divide the stream into chunks
219(arrays), do the counting within each chunk, and then add all the
220counts across chunks. We use the same code as above except that we use
221arrays for our input data.
223Please see the file [WordCountParallel.hs][] for the complete working
224code for this example, including the imports that we have omitted below.
226First we create a new data type `Counts` that holds all the context.
228```haskell ghci
229-- Counts lines words chars lastCharWasSpace
230data Counts = Counts !Int !Int !Int !Bool deriving Show
232{-# INLINE count #-}
233count :: Counts -> Char -> Counts
234count (Counts l w c wasSpace) ch =
235 let l1 = if ch == '\n' then l + 1 else l
236 (w1, wasSpace1) =
237 if isSpace ch
238 then (w, True)
239 else (if wasSpace then w + 1 else w, False)
240 in Counts l1 w1 (c + 1) wasSpace1
241```
243The `countArray` function counts the line, word, char counts in one chunk:
245```haskell ghci
246import Streamly.Data.Array (Array)
248import qualified Streamly.Data.Array as Array
249import qualified Streamly.Unicode.Stream as Unicode
251countArray :: Array Word8 -> IO Counts
252countArray arr =
253 Stream.unfold Array.reader arr -- Stream IO Word8
254 & Unicode.decodeLatin1 -- Stream IO Char
255 & Stream.fold (Fold.foldl' count (Counts 0 0 0 True)) -- IO Counts
256```
258Here the function `count` and the `Counts` data type are defined in the
259`WordCount` helper module defined in [WordCount.hs][].
261When combining the counts in two contiguous chunks, we need to check
262whether the first element of the next chunk is a whitespace character
263in order to determine if the same word continues in the next chunk or
264whether the chunk starts with a new word. The `partialCounts` function
265adds a `Bool` flag to `Counts` returned by `countArray` to indicate
266whether the first character in the chunk is a space.
268```haskell ghci
269partialCounts :: Array Word8 -> IO (Bool, Counts)
270partialCounts arr = do
271 let r = Array.getIndex 0 arr
272 case r of
273 Just x -> do
274 counts <- countArray arr
275 return (isSpace (chr (fromIntegral x)), counts)
276 Nothing -> return (False, Counts 0 0 0 True)
277```
279`addCounts` then adds the counts from two consecutive chunks:
281```haskell ghci
282addCounts :: (Bool, Counts) -> (Bool, Counts) -> (Bool, Counts)
283addCounts (sp1, Counts l1 w1 c1 ws1) (sp2, Counts l2 w2 c2 ws2) =
284 let wcount =
285 if not ws1 && not sp2 -- No space between two chunks.
286 then w1 + w2 - 1
287 else w1 + w2
288 in (sp1, Counts (l1 + l2) wcount (c1 + c2) ws2)
289```
291To count in parallel we now only need to divide the stream into arrays,
292apply our counting function to each array, and then combine the counts
293from each chunk.
295```haskell ghci
296{-# LANGUAGE FlexibleContexts #-}
298import GHC.Conc (numCapabilities)
299import qualified Streamly.Data.Stream.Prelude as Stream
301wc :: String -> IO (Bool, Counts)
302wc file = do
303 File.readChunks file -- Stream IO (Array Word8)
304 & Stream.parMapM cfg partialCounts -- Stream IO (Bool, Counts)
305 & Stream.fold add -- IO (Bool, Counts)
307 where
309 cfg = Stream.maxThreads numCapabilities . Stream.ordered True
310 add = Fold.foldl' addCounts (False, Counts 0 0 0 True)
311```
313We can replace `parMapM` with `mapM` to get a serial version of the program.
315A benchmark with 2 CPUs:
317```
318$ time WordCount-hs-parallel gutenberg-500MB.txt
31911242220 97050938 574714449 gutenberg-500MB.txt
321real 0m1.284s
322user 0m1.952s
323sys 0m0.140s
324```
326These example programs have assumed ASCII encoded input data. For UTF-8
327streams, we have a [concurrent wc implementation][WordCountParallelUTF8.hs]
328with UTF-8 decoding. This concurrent implementation performs as well
329as the standard `wc` program in serial benchmarks. In concurrent mode
330[Streamly][]'s implementation can utilise multiple processing cores if
331these are present, and can thereby run much faster than the standard
332binary.
334Streamly provides concurrency facilities similar
335to [OpenMP](https://en.wikipedia.org/wiki/OpenMP) and
336[Cilk](https://en.wikipedia.org/wiki/Cilk) but with a more declarative
337style of expression. With Streamly you can write concurrent programs
338with ease, with support for different types of concurrent scheduling.
340### A Concurrent Network Server
342We now move to a slightly more complicated example: we simulate a
343dictionary lookup server which can serve word meanings to multiple
344clients concurrently.
346Please see the file [WordServer.hs][] for the complete code for this
347example, including the imports that we have omitted below.
349```haskell ghci
350import Control.Concurrent (threadDelay)
351import Control.Exception (finally)
352import Network.Socket (Socket, close)
354import qualified Streamly.Data.Parser as Parser
355import qualified Streamly.Network.Inet.TCP as TCP
356import qualified Streamly.Network.Socket as Socket
357import qualified Streamly.Unicode.Stream as Unicode
359-- Simulate network/db query by adding a delay.
360fetch :: String -> IO (String, String)
361fetch w = threadDelay 1000000 >> return (w,w)
363-- Read lines of whitespace separated list of words from a socket, fetch the
364-- meanings of each word concurrently and return the meanings separated by
365-- newlines, in same order as the words were received. Repeat until the
366-- connection is closed.
367lookupWords :: Socket -> IO ()
368lookupWords sk =
369 Stream.unfold Socket.reader sk -- Stream IO Word8
370 & Unicode.decodeLatin1 -- Stream IO Char
371 & Stream.parseMany word -- Stream IO String
372 & Stream.parMapM cfg fetch -- Stream IO (String, String)
373 & fmap show -- Stream IO String
374 & Stream.intersperse "\n" -- Stream IO String
375 & Unicode.encodeStrings Unicode.encodeLatin1 -- Stream IO (Array Word8)
376 & Stream.fold (Socket.writeChunks sk)
378 where
380 word = Parser.wordBy isSpace Fold.toList
381 cfg = Stream.ordered True
383serve :: Socket -> IO ()
384serve sk = finally (lookupWords sk) (close sk)
386-- | Run a server on port 8091. Accept and handle connections concurrently. The
387-- connection handler is "serve" (i.e. lookupWords). You can use "telnet" or
388-- "nc" as a client to try it out.
389main :: IO ()
390main =
391 Stream.unfold TCP.acceptorOnPort 8091 -- Stream IO Socket
392 & Stream.parMapM id serve -- Stream IO ()
393 & Stream.fold Fold.drain -- IO ()
394```
396### Merging Incoming Streams
398In the next example, we show how to merge logs coming from multiple
399nodes in your network. These logs are merged at line boundaries and
400the merged logs are written to a file or to a network destination.
401This example uses the `concatMapWith` combinator to merge multiple
402streams concurrently.
404Please see the file [MergeServer.hs][] for the complete working code,
405including the imports that we have omitted below.
407```haskell ghci
408{-# LANGUAGE FlexibleContexts #-}
410import Streamly.Data.Stream (Stream)
411import System.IO (IOMode(AppendMode), Handle, withFile)
413import qualified Streamly.Data.Unfold as Unfold
414import qualified Streamly.Network.Socket as Socket
415import qualified Streamly.FileSystem.Handle as Handle
417-- | Read a line stream from a socket.
418-- Note: lines are buffered, and we could add a limit to the
419-- buffering for safety.
420readLines :: Socket -> Stream IO (Array Char)
421readLines sk =
422 Stream.unfold Socket.reader sk -- Stream IO Word8
423 & Unicode.decodeLatin1 -- Stream IO Char
424 & Stream.foldMany line -- Stream IO (Array Char)
426 where
428 line = Fold.takeEndBy (== '\n') Array.write
430recv :: Socket -> Stream IO (Array Char)
431recv sk = Stream.finallyIO (close sk) (readLines sk)
433-- | Starts a server at port 8091 listening for lines with space separated
434-- words. Multiple clients can connect to the server and send streams of lines.
435-- The server handles all the connections concurrently, merges the incoming
436-- streams at line boundaries and writes the merged stream to a file.
437server :: Handle -> IO ()
438server file =
439 Stream.unfold TCP.acceptorOnPort 8090 -- Stream IO Socket
440 & Stream.parConcatMap (Stream.eager True) recv -- Stream IO (Array Char)
441 & Stream.unfoldMany Array.reader -- Stream IO Char
442 & Unicode.encodeLatin1 -- Stream IO Word8
443 & Stream.fold (Handle.write file) -- IO ()
445main :: IO ()
446main = withFile "output.txt" AppendMode server
447```
449### Listing Directories Recursively/Concurrently
451Our next example lists a directory tree recursively, and concurrently.
453This example uses the tree traversing combinator `parConcatIterate`. This
454combinator maps a stream generator function on the input stream and then
455recursively on the generated stream as well and flattens the results. We map a
456directory to a stream generating its children and a file to a nil stream. This
457results in a concurrent recursive depth first traversal of the directory tree.
459Please see [ListDir.hs][] for the complete working code.
461```haskell ghci
462import System.IO (stdout, hSetBuffering, BufferMode(LineBuffering))
463import qualified Streamly.Internal.FileSystem.Dir as Dir (readEitherPaths)
465main :: IO ()
466main = do
467 hSetBuffering stdout LineBuffering
468 let start = Stream.fromPure (Left ".")
469 f = either Dir.readEitherPaths (const Stream.nil)
470 ls = Stream.parConcatIterate id f start
471 in Stream.fold (Fold.drainMapM print) ls
472```
474### Rate Limiting
476For concurrent streams, a stream evaluation rate can be specified. For
477example, to print "tick" once every second you can simply write:
479```haskell ghci
480import qualified Streamly.Internal.Data.Stream as Stream (timestamped)
482main :: IO ()
483main =
484 Stream.parRepeatM (Stream.avgRate 1) (pure "tick") -- Stream IO String
485 & Stream.timestamped -- Stream IO (AbsTime, String)
486 & Stream.fold (Fold.drainMapM print) -- IO ()
487```
489Please see the file [Rate.hs][] for the complete working code.
491The concurrency of the stream is automatically controlled to match the
492specified rate. [Streamly][]'s rate control works precisely even at
493throughputs as high as millions of yields per second.
495For more sophisticated rate control needs please see the Streamly [reference
496documentation][Streamly].
498### Reactive Programming
500Streamly supports reactive (time domain) programming because of its support for
501declarative concurrency. Please see the `Streamly.Data.Stream.Prelude`
502module for time-specific and sampling combinators.
504The examples [AcidRain.hs][] and [CirclingSquare.hs][] demonstrate
505reactive programming using [Streamly][].
507### More Examples
509If you would like to view more examples, please visit the [Streamly
510Examples][streamly-examples] web page.
512### Further Reading
514* [Streaming Benchmarks][streaming-benchmarks]
515* [Concurrency Benchmarks][concurrency-benchmarks]
516* Functional Conf 2019 [Video](https://www.youtube.com/watch?v=uzsqgdMMgtk) | [Slides](https://www.slideshare.net/HarendraKumar10/streamly-concurrent-data-flow-programming)
517* [Other Guides](/)
518* [Streamly Homepage][Streamly]
520## Performance
522As you have seen in the word count example above, [Streamly][] offers
523highly modular abstractions for building programs while also offering
524the performance close to an equivalent (imperative) C program.
526Streamly offers excellent performance even for byte-at-a-time stream
527operations using efficient abstractions like `Unfold`s and terminating
528`Fold`s. Byte-at-a-time stream operations can simplify programming
529because the developer does not have to deal explicitly with chunking
530and re-combining data.
532Streamly exploits GHC's stream fusion optimizations (`case-of-case` and
533`spec-constr`) aggressively to achieve C-like speed, while also offering
534highly modular abstractions to developers.
536[Streamly][] will usually perform very well without any
537compiler plugins. However, we have fixed some deficiencies
538that we had noticed in GHC's optimizer using a [compiler
539plugin](https://github.com/composewell/fusion-plugin). We hope to fold
540these optimizations into GHC in the future; until then we recommend that
541you use this plugin for applications that are performance sensitive.
543### Benchmarks
545We measured several Haskell streaming implementations
546using various micro-benchmarks. Please see the [streaming
547benchmarks][streaming-benchmarks] page for a detailed comparison of
548Streamly against other streaming libraries.
550Our results show that [Streamly][] is the fastest effectful streaming
551implementation on almost all the measured microbenchmarks. In many cases
552it runs up to 100x faster, and in some cases even 1000x faster than
553some of the tested alternatives. In some composite operation benchmarks
554[Streamly][] turns out to be significantly faster than Haskell's list
555implementation.
557*Note*: If you can write a program in some other way or with some other
558language that runs significantly faster than what [Streamly][] offers,
559please let us know and we will improve.
561## Notes
563Streamly comes equipped with a very powerful set of abstractions to
564accomplish many kinds of programming tasks: it provides support for
565programming with streams and arrays, for reading and writing from the
566file system and from the network, for time domain programming (reactive
567programming), and for reacting to file system events using `fsnotify`.
569Please view [Streamly's documentation][Streamly] for more information
570about Streamly's features.
572### Concurrency
574Streamly uses lock-free synchronization for achieving concurrent
575operation with low overheads. The number of tasks performed concurrently
576are determined automatically based on the rate at which a consumer
577is consuming the results. In other words, you do not need to manage
578thread pools or decide how many threads to use for a particular task.
579For CPU-bound tasks Streamly will try to keep the number of threads
580close to the number of CPUs available; for IO-bound tasks it will utilize
581more threads.
583The parallelism available during program execution can be utilized with
584very little overhead even where the task size is very
585small, because Streamly will automatically switch between
586serial or batched execution of tasks on the same CPU depending
587on whichever is more efficient. Please see our [concurrency
588benchmarks][concurrency-benchmarks] for more detailed performance
589measurements, and for a comparison with the `async` package.
591### Design Goals
593Our goals for [Streamly][] from the very beginning have been:
5951. To achieve simplicity by unifying abstractions.
5962. To offer high performance.
598These goals are hard to achieve simultaneously because they are usually
599inversely related. We have spent many years trying to get the abstractions
600right without compromising performance.
602`Unfold` is an example of an abstraction that we have created to achieve
603high performance when mapping streams on streams. `Unfold` allows stream
604generation to be optimized well by the compiler through stream fusion.
605A `Fold` with termination capability is another example which modularizes
606stream elimination operations through stream fusion. Terminating folds
607can perform many simple parsing tasks that do not require backtracking.
608In Streamly, `Parser`s are a natural extension to terminating `Fold`s;
609`Parser`s add the ability to backtrack to `Fold`s. Unification leads
610to simpler abstractions and lower cognitive overheads while also not
611compromising performance.
613## Credits
615The following authors/libraries have influenced or inspired this library in a
616significant way:
618 * Roman Leshchinskiy ([vector](http://hackage.haskell.org/package/vector))
619 * Gabriella Gonzalez ([foldl](https://hackage.haskell.org/package/foldl))
620 * Alberto G. Corona ([transient](https://hackage.haskell.org/package/transient))
622Please see the [`credits`](/docs/User/ProjectRelated/Credits.md) directory for a full
623list of contributors, credits and licenses.
625## Licensing
627Streamly is an [open source](https://github.com/composewell/streamly)
628project available under a liberal [BSD-3-Clause license][LICENSE]
630## Contributing to Streamly
632As an open project we welcome contributions:
634* [Streamly Contributor's Guide][CONTRIBUTING.md]
635* [Contact the streamly development team](mailto:streamly@composewell.com)
637## Getting Support
639Professional support is available for [Streamly][]: please contact
640[support@composewell.com](mailto:support@composewell.com).
642You can also join our [community chat
643channel](https://gitter.im/composewell/streamly) on Gitter.
645<!--
646Link References.
647-->
649[Streamly]: https://streamly.composewell.com/
650[streamly-examples]: https://github.com/composewell/streamly-examples
651[streaming-benchmarks]: https://github.com/composewell/streaming-benchmarks
652[concurrency-benchmarks]: https://github.com/composewell/concurrency-benchmarks
654<!--
655Keep all the unstable links here so that they can be updated to stable
656links (for online docs) before we release.
657-->
659<!-- examples -->
660[WordCountModular.hs]: https://github.com/composewell/streamly-examples/blob/master/examples/WordCountModular.hs
661[WordCount.hs]: https://github.com/composewell/streamly-examples/blob/master/examples/WordCount.hs
662[WordCount.c]: https://github.com/composewell/streamly-examples/blob/master/examples/WordCount.c
663[WordCountParallel.hs]: https://github.com/composewell/streamly-examples/blob/master/examples/WordCountParallel.hs
664[WordCountParallelUTF8.hs]: https://github.com/composewell/streamly-examples/blob/master/examples/WordCountParallelUTF8.hs
665[WordServer.hs]: https://github.com/composewell/streamly-examples/blob/master/examples/WordServer.hs
666[MergeServer.hs]: https://github.com/composewell/streamly-examples/blob/master/examples/MergeServer.hs
667[ListDir.hs]: https://github.com/composewell/streamly-examples/blob/master/examples/ListDir.hs
668[Rate.hs]: https://github.com/composewell/streamly-examples/blob/master/examples/Rate.hs
669[AcidRain.hs]: https://github.com/composewell/streamly-examples/tree/master/examples/AcidRain.hs
670[CirclingSquare.hs]: https://github.com/composewell/streamly-examples/tree/master/examples/CirclingSquare.hs
672<!-- local files -->
673[LICENSE]: /LICENSE
674[CONTRIBUTING.md]: /CONTRIBUTING.md
675[docs]: docs/