on 3cdb718 help sign in
streamly/docs/Introduction.md
651 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/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/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 `SerialT IO a` is a serial stream of values of type `a` in the IO Monad.
59* An `AsyncT IO a` is a concurrent (asynchronous) stream of values of type
60 `a` in the IO Monad.
61* An `Unfold IO a b` is a representation of a function that converts a seed
62 value of type `a` into a stream of values of type `b` in the IO Monad.
63* A `Fold IO a b` is a representation of a function that converts a stream of
64 type `a` to a final accumulator of type `b` in the IO Monad.
66### A Note on Module Naming
68Some of the examples below use modules from the `Internal` Streamly package
69hierarchy. These are not really internal to the library. We classify
70`Streamly` modules into two categories:
72* _Released modules and APIs_: These modules and APIs are
73 stable. Significant changes to these modules and APIs will cause
74 Streamly's version number to change according to the package versioning
75 policy.
76* _Pre-release modules and APIs_: These modules and APIs have not been
77 formally released yet. They may change in the near future, and such
78 changes will not necessarily be reflected in Streamly's package
79 version number. As yet unreleased modules and APIs reside in the
80 `Internal` namespace.
82Please use a minor release upper bound to adhere to the Haskell PVP when
83using the pre-release (internal) modules.
85## The Examples
87### Modular Word Counting
89A `Fold` in Streamly is a composable stream consumer. For our first
90example, we will use `Fold`s to count the number of bytes, words and lines
91present in a file. We will then compose individual `Fold`s together to
92count words, bytes and lines at the same time.
94Please see the file [WordCountModular.hs][] for the complete example
95program, including the imports that we have omitted here.
97#### Count Bytes (wc -c)
99We start with a code fragment that counts the number of bytes in a file:
101```haskell
102import qualified Streamly.Data.Fold as Fold
103import qualified Streamly.Internal.FileSystem.File as File
104import qualified Streamly.Prelude as Stream
106wcb :: String -> IO Int
107wcb file =
108 File.toBytes file -- SerialT 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
117-- ASCII character 10 is a newline.
118countl :: Int -> Word8 -> Int
119countl n ch = if ch == 10 then n + 1 else n
121-- The fold accepts a stream of `Word8` and returns a line count (`Int`).
122nlines :: Monad m => Fold m Word8 Int
123nlines = Fold.foldl' countl 0
125wcl :: String -> IO Int
126wcl file =
127 File.toBytes file -- SerialT IO Word8
128 & Stream.fold nlines -- IO Int
129```
131### Count Words (wc -w)
133Our final code fragment counts the number of whitespace-separated words
134in a stream:
136```haskell
137countw :: (Int, Bool) -> Word8 -> (Int, Bool)
138countw (n, wasSpace) ch =
139 if isSpace $ chr $ fromIntegral ch
140 then (n, True)
141 else (if wasSpace then n + 1 else n, False)
143-- The fold accepts a stream of `Word8` and returns a word count (`Int`).
144nwords :: Monad m => Fold m Word8 Int
145nwords = fst <$> Fold.foldl' countw (0, True)
147wcw :: String -> IO Int
148wcw file =
149 File.toBytes file -- SerialT IO Word8
150 & Stream.fold nwords -- IO Int
151```
153### Counting Bytes, Words and Lines Together
155By using the `Tee` combinator we can compose the three folds that count
156bytes, lines and words individually into a single fold that counts all
157three at once. The applicative instance of `Tee` distributes its input
158to all the supplied folds (`Fold.length`, `nlines`, and `nwords`) and
159then combines the outputs from the folds using the supplied combiner
160function (`(,,)`).
162```haskell
163import qualified Streamly.Internal.Data.Fold.Tee as Tee
165-- The fold accepts a stream of `Word8` and returns the three counts.
166countAll :: Fold IO Word8 (Int, Int, Int)
167countAll = Tee.toFold $ (,,) <$> Tee Fold.length <*> Tee nlines <*> Tee nwords
169wc :: String -> IO (Int, Int, Int)
170wc file =
171 File.toBytes file -- SerialT IO Word8
172 & Stream.fold countAll -- IO (Int, Int, Int)
173```
175This example demonstrates the excellent modularity offered by
176[Streamly][]'s simple and concise API. Experienced Haskellers will
177notice that we have not used bytestrings&mdash;we instead used a stream of
178`Word8` values, simplifying our program.
180### The Performance of Word Counting
182We compare two equivalent implementations: one using [Streamly][],
183and the other using C.
185The performance of the [Streamly word counting
186implementation][WordCount.hs] is:
188```
189$ time WordCount-hs gutenberg-500MB.txt
19011242220 97050938 574714449 gutenberg-500MB.txt
192real 0m1.825s
193user 0m1.697s
194sys 0m0.128s
195```
197The performance of an equivalent [wc implementation in C][WordCount.c] is:
199```
200$ time WordCount-c gutenberg-500MB.txt
20111242220 97050938 574714449 gutenberg-500MB.txt
203real 0m2.100s
204user 0m1.935s
205sys 0m0.165s
206```
208### Concurrent Word Counting
210In our next example we show how the task of counting words, lines,
211and bytes could be done in parallel on multiprocessor hardware.
213To count words in parallel we first divide the stream into chunks
214(arrays), do the counting within each chunk, and then add all the
215counts across chunks. We use the same code as above except that we use
216arrays for our input data.
218Please see the file [WordCountParallel.hs][] for the complete working
219code for this example, including the imports that we have omitted below.
221The `countArray` function counts the line, word, char counts in one chunk:
223```haskell
224import qualified Streamly.Data.Array.Foreign as Array
226countArray :: Array Word8 -> IO Counts
227countArray arr =
228 Stream.unfold Array.read arr -- SerialT IO Word8
229 & Stream.decodeLatin1 -- SerialT IO Char
230 & Stream.foldl' count (Counts 0 0 0 True) -- IO Counts
231```
233Here the function `count` and the `Counts` data type are defined in the
234`WordCount` helper module defined in [WordCount.hs][].
236When combining the counts in two contiguous chunks, we need to check
237whether the first element of the next chunk is a whitespace character
238in order to determine if the same word continues in the next chunk or
239whether the chunk starts with a new word. The `partialCounts` function
240adds a `Bool` flag to `Counts` returned by `countArray` to indicate
241whether the first character in the chunk is a space.
243```haskell
244partialCounts :: Array Word8 -> IO (Bool, Counts)
245partialCounts arr = do
246 let r = Array.getIndex arr 0
247 case r of
248 Just x -> do
249 counts <- countArray arr
250 return (isSpace (chr (fromIntegral x)), counts)
251 Nothing -> return (False, Counts 0 0 0 True)
252```
254`addCounts` then adds the counts from two consecutive chunks:
256```haskell
257addCounts :: (Bool, Counts) -> (Bool, Counts) -> (Bool, Counts)
258addCounts (sp1, Counts l1 w1 c1 ws1) (sp2, Counts l2 w2 c2 ws2) =
259 let wcount =
260 if not ws1 && not sp2 -- No space between two chunks.
261 then w1 + w2 - 1
262 else w1 + w2
263 in (sp1, Counts (l1 + l2) wcount (c1 + c2) ws2)
264```
266To count in parallel we now only need to divide the stream into arrays,
267apply our counting function to each array, and then combine the counts
268from each chunk.
270```haskell
271wc :: String -> IO (Bool, Counts)
272wc file = do
273 Stream.unfold File.readChunks file -- AheadT IO (Array Word8)
274 & Stream.mapM partialCounts -- AheadT IO (Bool, Counts)
275 & Stream.maxThreads numCapabilities -- AheadT IO (Bool, Counts)
276 & Stream.fromAhead -- SerialT IO (Bool, Counts)
277 & Stream.foldl' addCounts (False, Counts 0 0 0 True) -- IO (Bool, Counts)
278```
280Please note that the only difference between a concurrent and a
281non-concurrent program lies in the use of the `Stream.fromAhead`
282combinator. If we remove the call to `Stream.fromAhead`, we would
283still have a perfectly valid and performant serial program. Notice
284how succinctly and idiomatically we have expressed the concurrent word
285counting problem.
287A benchmark with 2 CPUs:
289```
290$ time WordCount-hs-parallel gutenberg-500MB.txt
29111242220 97050938 574714449 gutenberg-500MB.txt
293real 0m1.284s
294user 0m1.952s
295sys 0m0.140s
296```
298These example programs have assumed ASCII encoded input data. For UTF-8
299streams, we have a [concurrent wc implementation][WordCountParallelUTF8.hs]
300with UTF-8 decoding. This concurrent implementation performs as well
301as the standard `wc` program in serial benchmarks. In concurrent mode
302[Streamly][]'s implementation can utilise multiple processing cores if
303these are present, and can thereby run much faster than the standard
304binary.
306Streamly provides concurrency facilities similar
307to [OpenMP](https://en.wikipedia.org/wiki/OpenMP) and
308[Cilk](https://en.wikipedia.org/wiki/Cilk) but with a more declarative
309style of expression. With Streamly you can write concurrent programs
310with ease, with support for different types of concurrent scheduling.
312### A Concurrent Network Server
314We now move to a slightly more complicated example: we simulate a
315dictionary lookup server which can serve word meanings to multiple
316clients concurrently. This example demonstrates the use of the concurrent
317`mapM` combinator.
319Please see the file [WordServer.hs][] for the complete code for this
320example, including the imports that we have omitted below.
322```haskell
323import qualified Streamly.Data.Fold as Fold
324import qualified Streamly.Network.Inet.TCP as TCP
325import qualified Streamly.Network.Socket as Socket
326import qualified Streamly.Unicode.Stream as Unicode
328-- Simulate network/db query by adding a delay.
329fetch :: String -> IO (String, String)
330fetch w = threadDelay 1000000 >> return (w,w)
332-- Read lines of whitespace separated list of words from a socket, fetch the
333-- meanings of each word concurrently and return the meanings separated by
334-- newlines, in same order as the words were received. Repeat until the
335-- connection is closed.
336lookupWords :: Socket -> IO ()
337lookupWords sk =
338 Stream.unfold Socket.read sk -- SerialT IO Word8
339 & Unicode.decodeLatin1 -- SerialT IO Char
340 & Stream.wordsBy isSpace Fold.toList -- SerialT IO String
341 & Stream.fromSerial -- AheadT IO String
342 & Stream.mapM fetch -- AheadT IO (String, String)
343 & Stream.fromAhead -- SerialT IO (String, String)
344 & Stream.map show -- SerialT IO String
345 & Stream.intersperse "\n" -- SerialT IO String
346 & Unicode.encodeStrings Unicode.encodeLatin1 -- SerialT IO (Array Word8)
347 & Stream.fold (Socket.writeChunks sk) -- IO ()
349serve :: Socket -> IO ()
350serve sk = finally (lookupWords sk) (close sk)
352-- | Run a server on port 8091. Accept and handle connections concurrently. The
353-- connection handler is "serve" (i.e. lookupWords). You can use "telnet" or
354-- "nc" as a client to try it out.
355main :: IO ()
356main =
357 Stream.unfold TCP.acceptOnPort 8091 -- SerialT IO Socket
358 & Stream.fromSerial -- AsyncT IO ()
359 & Stream.mapM serve -- AsyncT IO ()
360 & Stream.fromAsync -- SerialT IO ()
361 & Stream.drain -- IO ()
362```
364### Merging Incoming Streams
366In the next example, we show how to merge logs coming from multiple
367nodes in your network. These logs are merged at line boundaries and
368the merged logs are written to a file or to a network destination.
369This example uses the `concatMapWith` combinator to merge multiple
370streams concurrently.
372Please see the file [MergeServer.hs][] for the complete working code,
373including the imports that we have omitted below.
375```haskell
376import qualified Streamly.Data.Unfold as Unfold
377import qualified Streamly.Network.Socket as Socket
379-- | Read a line stream from a socket.
380-- Note: lines are buffered, and we could add a limit to the
381-- buffering for safety.
382readLines :: Socket -> SerialT IO (Array Char)
383readLines sk =
384 Stream.unfold Socket.read sk -- SerialT IO Word8
385 & Unicode.decodeLatin1 -- SerialT IO Char
386 & Stream.splitWithSuffix (== '\n') Array.write -- SerialT IO String
388recv :: Socket -> SerialT IO (Array Char)
389recv sk = Stream.finally (liftIO $ close sk) (readLines sk)
391-- | Starts a server at port 8091 listening for lines with space separated
392-- words. Multiple clients can connect to the server and send streams of lines.
393-- The server handles all the connections concurrently, merges the incoming
394-- streams at line boundaries and writes the merged stream to a file.
395server :: Handle -> IO ()
396server file =
397 Stream.unfold TCP.acceptOnPort 8090 -- SerialT IO Socket
398 & Stream.concatMapWith Stream.parallel recv -- SerialT IO (Array Char)
399 & Stream.unfoldMany Array.read -- SerialT IO Char
400 & Unicode.encodeLatin1 -- SerialT IO Word8
401 & Stream.fold (Handle.write file) -- IO ()
403main :: IO ()
404main = withFile "output.txt" AppendMode server
405```
407### Listing Directories Recursively/Concurrently
409Our next example lists a directory tree recursively, reading
410multiple directories concurrently.
412This example uses the tree traversing combinator `iterateMapLeftsWith`.
413This combinator maps a stream generator on the `Left` values in its
414input stream (directory names in this case), feeding the resulting `Left`
415values back to the input, while it lets the `Right` values (file names
416in this case) pass through to the output. The `Stream.ahead` stream
417joining combinator then makes it iterate on the directories concurrently.
419Please see the file [ListDir.hs][] for the complete working code,
420including the imports that we have omitted below.
422```haskell
423import Streamly.Internal.Data.Stream.IsStream (iterateMapLeftsWith)
425import qualified Streamly.Prelude as Stream
426import qualified Streamly.Internal.FileSystem.Dir as Dir (toEither)
428-- Lists a directory as a stream of (Either Dir File).
429listDir :: String -> SerialT IO (Either String String)
430listDir dir =
431 Dir.toEither dir -- SerialT IO (Either String String)
432 & Stream.map (bimap mkAbs mkAbs) -- SerialT IO (Either String String)
434 where mkAbs x = dir ++ "/" ++ x
436-- | List the current directory recursively using concurrent processing.
437main :: IO ()
438main = do
439 hSetBuffering stdout LineBuffering
440 let start = Stream.fromPure (Left ".")
441 Stream.iterateMapLeftsWith Stream.ahead listDir start
442 & Stream.mapM_ print
443```
445### Rate Limiting
447For bounded concurrent streams, a stream yield rate can be specified
448easily. For example, to print "tick" once every second you can simply
449write:
451```haskell
452main :: IO ()
453main =
454 Stream.repeatM (pure "tick") -- AsyncT IO String
455 & Stream.timestamped -- AsyncT IO (AbsTime, String)
456 & Stream.avgRate 1 -- AsyncT IO (AbsTime, String)
457 & Stream.fromAsync -- SerialT IO (AbsTime, String)
458 & Stream.mapM_ print -- IO ()
459```
461Please see the file [Rate.hs][] for the complete working code.
463The concurrency of the stream is automatically controlled to match the
464specified rate. [Streamly][]'s rate control works precisely even at
465throughputs as high as millions of yields per second.
467For more sophisticated rate control needs please see the Streamly [reference
468documentation][Streamly].
470### Reactive Programming
472Streamly supports reactive (time domain) programming because of its
473support for declarative concurrency. Please see the `Streamly.Prelude`
474module for time-specific combinators like `intervalsOf`, and
475folds like `takeInterval` in `Streamly.Internal.Data.Fold`.
476Please also see the pre-release sampling combinators in the
477`Streamly.Internal.Data.Stream.IsStream.Top` module for `throttle` and
478`debounce` like operations.
480The examples [AcidRain.hs][] and [CirclingSquare.hs][] demonstrate
481reactive programming using [Streamly][].
483### More Examples
485If you would like to view more examples, please visit the [Streamly
486Examples][streamly-examples] web page.
488### Further Reading
490* [Streaming Benchmarks][streaming-benchmarks]
491* [Concurrency Benchmarks][concurrency-benchmarks]
492* Functional Conf 2019 [Video](https://www.youtube.com/watch?v=uzsqgdMMgtk) | [Slides](https://www.slideshare.net/HarendraKumar10/streamly-concurrent-data-flow-programming)
493* [Other Guides](/)
494* [Streamly Homepage][Streamly]
496## Performance
498As you have seen in the word count example above, [Streamly][] offers
499highly modular abstractions for building programs while also offering
500the performance close to an equivalent (imperative) C program.
502Streamly offers excellent performance even for byte-at-a-time stream
503operations using efficient abstractions like `Unfold`s and terminating
504`Fold`s. Byte-at-a-time stream operations can simplify programming
505because the developer does not have to deal explicitly with chunking
506and re-combining data.
508Streamly exploits GHC's stream fusion optimizations (`case-of-case` and
509`spec-constr`) aggressively to achieve C-like speed, while also offering
510highly modular abstractions to developers.
512[Streamly][] will usually perform very well without any
513compiler plugins. However, we have fixed some deficiencies
514that we had noticed in GHC's optimizer using a [compiler
515plugin](https://github.com/composewell/fusion-plugin). We hope to fold
516these optimizations into GHC in the future; until then we recommend that
517you use this plugin for applications that are performance sensitive.
519### Benchmarks
521We measured several Haskell streaming implementations
522using various micro-benchmarks. Please see the [streaming
523benchmarks][streaming-benchmarks] page for a detailed comparison of
524Streamly against other streaming libraries.
526Our results show that [Streamly][] is the fastest effectful streaming
527implementation on almost all the measured microbenchmarks. In many cases
528it runs up to 100x faster, and in some cases even 1000x faster than
529some of the tested alternatives. In some composite operation benchmarks
530[Streamly][] turns out to be significantly faster than Haskell's list
531implementation.
533*Note*: If you can write a program in some other way or with some other
534language that runs significantly faster than what [Streamly][] offers,
535please let us know and we will improve.
537## Notes
539Streamly comes equipped with a very powerful set of abstractions to
540accomplish many kinds of programming tasks: it provides support for
541programming with streams and arrays, for reading and writing from the
542file system and from the network, for time domain programming (reactive
543programming), and for reacting to file system events using `fsnotify`.
545Please view [Streamly's documentation][Streamly] for more information
546about Streamly's features.
548### Concurrency
550Streamly uses lock-free synchronization for achieving concurrent
551operation with low overheads. The number of tasks performed concurrently
552are determined automatically based on the rate at which a consumer
553is consuming the results. In other words, you do not need to manage
554thread pools or decide how many threads to use for a particular task.
555For CPU-bound tasks Streamly will try to keep the number of threads
556close to the number of CPUs available; for IO-bound tasks it will utilize
557more threads.
559The parallelism available during program execution can be utilized with
560very little overhead even where the task size is very
561small, because Streamly will automatically switch between
562serial or batched execution of tasks on the same CPU depending
563on whichever is more efficient. Please see our [concurrency
564benchmarks][concurrency-benchmarks] for more detailed performance
565measurements, and for a comparison with the `async` package.
567### Design Goals
569Our goals for [Streamly][] from the very beginning have been:
5711. To achieve simplicity by unifying abstractions.
5722. To offer high performance.
574These goals are hard to achieve simultaneously because they are usually
575inversely related. We have spent many years trying to get the abstractions
576right without compromising performance.
578`Unfold` is an example of an abstraction that we have created to achieve
579high performance when mapping streams on streams. `Unfold` allows stream
580generation to be optimized well by the compiler through stream fusion.
581A `Fold` with termination capability is another example which modularizes
582stream elimination operations through stream fusion. Terminating folds
583can perform many simple parsing tasks that do not require backtracking.
584In Streamly, `Parser`s are a natural extension to terminating `Fold`s;
585`Parser`s add the ability to backtrack to `Fold`s. Unification leads
586to simpler abstractions and lower cognitive overheads while also not
587compromising performance.
589## Credits
591The following authors/libraries have influenced or inspired this library in a
592significant way:
594 * Roman Leshchinskiy ([vector](http://hackage.haskell.org/package/vector))
595 * Gabriella Gonzalez ([foldl](https://hackage.haskell.org/package/foldl))
596 * Alberto G. Corona ([transient](https://hackage.haskell.org/package/transient))
598Please see the [`credits`](/docs/Credits.md) directory for a full
599list of contributors, credits and licenses.
601## Licensing
603Streamly is an [open source](https://github.com/composewell/streamly)
604project available under a liberal [BSD-3-Clause license][LICENSE]
606## Contributing to Streamly
608As an open project we welcome contributions:
610* [Streamly Contributor's Guide][CONTRIBUTING.md]
611* [Contact the streamly development team](mailto:streamly@composewell.com)
613## Getting Support
615Professional support is available for [Streamly][]: please contact
616[support@composewell.com](mailto:support@composewell.com).
618You can also join our [community chat
619channel](https://gitter.im/composewell/streamly) on Gitter.
621<!--
622Link References.
623-->
625[Streamly]: https://streamly.composewell.com/
626[streamly-examples]: https://github.com/composewell/streamly-examples
627[streaming-benchmarks]: https://github.com/composewell/streaming-benchmarks
628[concurrency-benchmarks]: https://github.com/composewell/concurrency-benchmarks
630<!--
631Keep all the unstable links here so that they can be updated to stable
632links (for online docs) before we release.
633-->
635<!-- examples -->
636[WordCountModular.hs]: https://github.com/composewell/streamly-examples/blob/master/examples/WordCountModular.hs
637[WordCount.hs]: https://github.com/composewell/streamly-examples/blob/master/examples/WordCount.hs
638[WordCount.c]: https://github.com/composewell/streamly-examples/blob/master/examples/WordCount.c
639[WordCountParallel.hs]: https://github.com/composewell/streamly-examples/blob/master/examples/WordCountParallel.hs
640[WordCountParallelUTF8.hs]: https://github.com/composewell/streamly-examples/blob/master/examples/WordCountParallelUTF8.hs
641[WordServer.hs]: https://github.com/composewell/streamly-examples/blob/master/examples/WordServer.hs
642[MergeServer.hs]: https://github.com/composewell/streamly-examples/blob/master/examples/MergeServer.hs
643[ListDir.hs]: https://github.com/composewell/streamly-examples/blob/master/examples/ListDir.hs
644[Rate.hs]: https://github.com/composewell/streamly-examples/blob/master/examples/Rate.hs
645[AcidRain.hs]: https://github.com/composewell/streamly-examples/tree/master/examples/AcidRain.hs
646[CirclingSquare.hs]: https://github.com/composewell/streamly-examples/tree/master/examples/CirclingSquare.hs
648<!-- local files -->
649[LICENSE]: /LICENSE
650[CONTRIBUTING.md]: /CONTRIBUTING.md
651[docs]: docs/