Repository: plaid/async-problem Branch: master Commit: fb03d31bbcd9 Files: 32 Total size: 20.3 KB Directory structure: gitextract_xmvspb7n/ ├── .eslintrc.json ├── .github/ │ └── CODEOWNERS ├── .gitignore ├── .npmrc ├── README.md ├── async.js ├── await.js ├── bluebird-promisell.js ├── callbacks-revenge.js ├── callbacks.js ├── common/ │ ├── exit0.js │ ├── exit1.js │ ├── join.js │ ├── read-file-callback.js │ ├── read-file-promise.js │ ├── revengeutils.js │ └── sanctuary.js ├── coroutines-bluebird.js ├── coroutines-co.js ├── futures.js ├── input/ │ ├── bar.txt │ ├── baz.txt │ ├── foo.txt │ └── index.txt ├── most.js ├── node-streams.js ├── package.json ├── promises-pipe.js ├── promises.js ├── righto.js ├── synchronous.js └── test ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintrc.json ================================================ { "root": true, "extends": ["./node_modules/sanctuary-style/eslint-es6.json"], "parserOptions": {"ecmaVersion": 8}, "env": {"node": true}, "rules": { "max-len": ["error", {"code": 79, "ignoreComments": true}] } } ================================================ FILE: .github/CODEOWNERS ================================================ # This is a CODEOWNERS file. # # Please consult https://help.github.com/articles/about-codeowners/ # for documentation. # Plaid codeowners requirements. * @plaid/developer-relations ================================================ FILE: .gitignore ================================================ /node_modules/ ================================================ FILE: .npmrc ================================================ package-lock=false ================================================ FILE: README.md ================================================ # JavaScript has a problem We like to say that JavaScript has a callback problem. This project considers various approaches to the following problem: *Given a path to a directory containing an index file, __index.txt__, and zero or more other files, read the index file (which contains one filename per line), then read each of the files listed in the index concurrently, concat the resulting strings (in the order specified by the index), and write the result to stdout.* *The program's exit code should be 0 if the entire operation is successful; 1 otherwise.* All side effects must be isolated to the `main` function. Impure functions may be defined at the top level but may only be invoked in `main`. The solutions are best read in the following order: - [__synchronous.js__](./synchronous.js) - [__callbacks.js__](./callbacks.js) - [__node-streams.js__](./node-streams.js) - [__async.js__](./async.js) - [__righto.js__](./righto.js) - [__promises.js__](./promises.js) - [__promises-pipe.js__](./promises-pipe.js) - [__bluebird-promisell.js__](./bluebird-promisell.js) - [__most.js__](./most.js) - [__coroutines-co.js__](./coroutines-co.js) - [__coroutines-bluebird.js__](./coroutines-bluebird.js) - [__await.js__](./await.js) - [__futures.js__](./futures.js) - [__callbacks-revenge.js__](./callbacks-revenge.js) The above ordering is suggested reading order only – not grade. Solutions are graded on the following criteria: - the size of `main` (smaller is better); and - the degree to which the approach facilitates breaking the problem into manageable subproblems. --- † Perhaps in five years we'll be saying that JavaScript has a promise problem. ================================================ FILE: async.js ================================================ 'use strict'; const async = require ('async'); const S = require ('sanctuary'); const exit0 = require ('./common/exit0'); const exit1 = require ('./common/exit1'); const join = require ('./common/join'); const readFile = require ('./common/read-file-callback'); const main = () => { const path = join (process.argv[2]); readFile (path ('index.txt')) ((err, data) => { if (err != null) exit1 (err); async.map ( S.map (path) (S.lines (data)), (filename, callback) => readFile (filename) (callback), (err, results) => { if (err == null) exit0 (S.joinWith ('') (results)); else exit1 (err); } ); }); }; if (process.mainModule.filename === __filename) main (); ================================================ FILE: await.js ================================================ 'use strict'; const exit0 = require ('./common/exit0'); const exit1 = require ('./common/exit1'); const join = require ('./common/join'); const readFile = require ('./common/read-file-promise'); const S = require ('./common/sanctuary'); // concatFiles :: (String -> String) -> Promise Error String async function concatFiles(path) { const index = await readFile (path ('index.txt')); const filenames = S.map (path) (S.lines (index)); const results = await Promise.all (S.map (readFile) (filenames)); return S.joinWith ('') (results); } const main = () => { concatFiles (join (process.argv[2])) .then (exit0, exit1); }; if (process.mainModule.filename === __filename) main (); ================================================ FILE: bluebird-promisell.js ================================================ 'use strict'; const P = require ('bluebird-promisell'); const exit0 = require ('./common/exit0'); const exit1 = require ('./common/exit1'); const join = require ('./common/join'); const readFile = require ('./common/read-file-promise'); const S = require ('./common/sanctuary'); // concatFiles :: (String -> String) -> Promise Error String const concatFiles = path => { // readFileRel :: String -> Promise Error String const readFileRel = S.compose (readFile) (path); // indexP :: Promise Error String const indexP = readFileRel ('index.txt'); // filenamesP :: Promise Error (Array String) const filenamesP = P.liftp1 (S.lines) (indexP); // resultsP :: Promise Error (Array String) const resultsP = P.liftp1 (P.traversep (readFileRel)) (filenamesP); // (return value) :: Promise Error String return P.liftp1 (S.joinWith ('')) (resultsP); }; const main = () => { concatFiles (join (process.argv[2])) .then (exit0, exit1); }; if (process.mainModule.filename === __filename) main (); ================================================ FILE: callbacks-revenge.js ================================================ 'use strict'; const fs = require ('fs'); const exit0 = require ('./common/exit0'); const exit1 = require ('./common/exit1'); const { Arr, Fn, Str, Path, NodeEither, GenericEitherT, Cont, } = require ('./common/revengeutils'); const getpath = Path.combine (process.argv[2]); // A monad for working with continuations that contain NodeEithers // :: type NC r e x = GenericEither (Cont r) NodeEither e x // :: = Cont r (NodeEither e x) const NC = GenericEitherT (NodeEither) (Cont); // Standard NodeJS APIs need a little massaging to be valid continuations // - Callback must be a curried, final argument // - Can only pass a single argument to callback (an [err, ...data] array is fine) // :: Path -> NC () String String const readFile = path => cb => fs.readFile (path, {encoding: 'utf8'}, (...args) => cb (args)); // Main // :: String -> NC () String String const readAllFiles = Fn.pipe ([ getpath, // :: Path readFile, // :: NC () String String NC.map (Str.lines), // :: NC () String [String] NC.map (Arr.map (getpath)), // :: NC () String [Path] NC.chain (Arr.traverse (NC) (readFile)), // :: NC () String [String] NC.map (Str.join ('')), // :: NC () String String ]); // :: NC () String String const result = readAllFiles ('index.txt'); // Remember that: // :: type NC r e x = Cont r (NodeEither e x) // so... // :: NC () String String = Cont () (NodeEither String String) // :: = ((NodeEither String String) -> ()) -> () // :: NodeEither String String -> ⊥ const fork = NodeEither.match ({Left: exit1, Right: exit0}); const main = () => result (fork); // :: () main (); ================================================ FILE: callbacks.js ================================================ 'use strict'; const S = require ('sanctuary'); const exit0 = require ('./common/exit0'); const exit1 = require ('./common/exit1'); const join = require ('./common/join'); const readFile = require ('./common/read-file-callback'); const main = () => { const path = join (process.argv[2]); readFile (path ('index.txt')) ((err, data) => { if (err != null) exit1 (err); const filenames = S.lines (data); const $results = []; let filesRead = 0; filenames.forEach ((file, idx) => { readFile (path (file)) ((err, data) => { if (err != null) exit1 (err); $results[idx] = data; filesRead += 1; if (filesRead === filenames.length) exit0 (S.joinWith ('') ($results)); }); }); }); }; if (process.mainModule.filename === __filename) main (); ================================================ FILE: common/exit0.js ================================================ 'use strict'; module.exports = s => { process.stdout.write (s); process.exit (0); }; ================================================ FILE: common/exit1.js ================================================ 'use strict'; module.exports = err => { process.stderr.write (`${err}\n`); process.exit (1); }; ================================================ FILE: common/join.js ================================================ 'use strict'; const path = require ('path'); const S = require ('sanctuary'); // join :: String -> String -> String module.exports = S.curry2 (path.join); ================================================ FILE: common/read-file-callback.js ================================================ 'use strict'; const fs = require ('fs'); // readFile :: String -> ((Error?, String?) -> Undefined) -> Undefined module.exports = filename => callback => fs.readFile (filename, {encoding: 'utf8'}, callback); ================================================ FILE: common/read-file-promise.js ================================================ 'use strict'; const fs = require ('fs'); // readFile :: String -> Promise Error String module.exports = filename => new Promise ((res, rej) => { fs.readFile (filename, {encoding: 'utf8'}, (err, data) => { if (err == null) res (data); else rej (err); }); }); ================================================ FILE: common/revengeutils.js ================================================ 'use strict'; const path = require ('path'); const derivemonad = M => { const {of, chain} = M; const map = f => chain (x => of (f (x))); const lift2 = f => mx => my => chain (x => chain (y => of (f (x) (y))) (my)) (mx); return Object.assign ({map, lift2}, M); }; const Arr = (() => { const of = x => [x]; const map = f => x => x.map (f); const foldl = f => z => xs => xs.reduce ((p, c) => f (p) (c), z); const empty = []; const append = a => b => [...a, ...b]; const sequence = A => foldl (A.lift2 (b => a => append (b) (of (a)))) (A.of (empty)); const traverse = A => f => xs => sequence (A) (map (f) (xs)); return {map, foldl, traverse}; }) (); const Fn = (() => { const id = x => x; const compose = f => g => a => f (g (a)); const flip = f => x => y => f (y) (x); const pipe = Arr.foldl (flip (compose)) (id); return {id, compose, flip, pipe, '.': compose}; }) (); const Str = (() => { const trim = s => s.trim (); const join = sep => arr => arr.join (sep); const split = sep => s => s.split (sep); const lines = Fn['.'] (split ('\n')) (trim); return {split, lines, join, trim}; }) (); const Path = (() => { const combine = base => sub => path.join (base, sub); return {combine}; }) (); // An Either interpretation of Node style first-element-falsy arrays // :: type NodeEither e d = [Maybe e, ...d] const NodeEither = (() => { const Left = l => [l]; const Right = (...r) => [null, ...r]; const match = ({Left, Right}) => ([e, ...x]) => e ? Left (e) : Right (...x); return {Left, Right, match}; }) (); // The GenericEitherT monad transformer // :: type GenericEitherT e m l r = m (e l r) const GenericEitherT = E => M => { const {Left, Right, match} = E; // :: x -> m (e l x) const of = x => M.of (Right (x)); // :: (a -> m (e l b)) -> m (e l a) -> m (e l b) const chain = f => Fn['.'] (M.chain) (match) ({ Left: l => M.of (Left (l)), Right: f, }); return derivemonad ({of, chain}); }; // The continuation monad // :: type Cont r a = (a -> r) -> r const Cont = (() => { const of = x => cb => cb (x); const chain = f => m => cb => m (x => f (x) (cb)); return derivemonad ({of, chain}); }) (); module.exports = {Arr, Fn, Str, Path, NodeEither, GenericEitherT, Cont}; ================================================ FILE: common/sanctuary.js ================================================ 'use strict'; const FutureTypes = require ('fluture-sanctuary-types'); const S = require ('sanctuary'); const $ = require ('sanctuary-def'); // PromiseType :: Type const PromiseType = $.NullaryType ('Promise') ('') ([]) (x => x != null && x.constructor === Promise); module.exports = S.create ({ checkTypes: true, env: S.env.concat (FutureTypes.env.concat ([PromiseType])), }); ================================================ FILE: coroutines-bluebird.js ================================================ 'use strict'; const bluebird = require ('bluebird'); const exit0 = require ('./common/exit0'); const exit1 = require ('./common/exit1'); const join = require ('./common/join'); const readFile = require ('./common/read-file-promise'); const S = require ('./common/sanctuary'); // concatFiles :: (String -> String) -> Promise Error String const concatFiles = bluebird.coroutine (function* generator(path) { const index = yield readFile (path ('index.txt')); const filenames = S.map (path) (S.lines (index)); const results = yield bluebird.all (S.map (readFile) (filenames)); return S.joinWith ('') (results); }); const main = () => { concatFiles (join (process.argv[2])) .then (exit0, exit1); }; if (process.mainModule.filename === __filename) main (); ================================================ FILE: coroutines-co.js ================================================ 'use strict'; const co = require ('co'); const exit0 = require ('./common/exit0'); const exit1 = require ('./common/exit1'); const join = require ('./common/join'); const readFile = require ('./common/read-file-promise'); const S = require ('./common/sanctuary'); // concatFiles :: (String -> String) -> Promise Error String const concatFiles = path => co (function* generator() { const index = yield readFile (path ('index.txt')); const filenames = S.map (path) (S.lines (index)); const results = yield Promise.all (S.map (readFile) (filenames)); return S.joinWith ('') (results); }); const main = () => { concatFiles (join (process.argv[2])) .then (exit0, exit1); }; if (process.mainModule.filename === __filename) main (); ================================================ FILE: futures.js ================================================ 'use strict'; const fs = require ('fs'); const Future = require ('fluture'); const exit0 = require ('./common/exit0'); const exit1 = require ('./common/exit1'); const join = require ('./common/join'); const S = require ('./common/sanctuary'); // readFile :: String -> Future Error String const readFile = S.flip (Future.encaseN2 (fs.readFile)) ({encoding: 'utf8'}); // readFilePar :: String -> ConcurrentFuture Error String const readFilePar = S.compose (Future.Par) (readFile); // concatFiles :: (String -> String) -> Future Error String const concatFiles = path => S.pipe ([path, // :: String readFile, // :: Future Error String S.map (S.lines), // :: Future Error (Array String) S.map (S.map (path)), // :: Future Error (Array String) S.map (S.traverse (Future.Par) (readFilePar)), // :: Future Error (ConcurrentFuture Error (Array String)) S.chain (Future.seq), // :: Future Error (Array String) S.map (S.joinWith (''))]) // :: Future Error String ('index.txt'); const main = () => { concatFiles (join (process.argv[2])) .fork (exit1, exit0); }; if (process.mainModule.filename === __filename) main (); ================================================ FILE: input/bar.txt ================================================ BAR ================================================ FILE: input/baz.txt ================================================ BAZ ================================================ FILE: input/foo.txt ================================================ FOO ================================================ FILE: input/index.txt ================================================ foo.txt bar.txt baz.txt ================================================ FILE: most.js ================================================ 'use strict'; const most = require ('most'); const exit0 = require ('./common/exit0'); const exit1 = require ('./common/exit1'); const join = require ('./common/join'); const readFile = require ('./common/read-file-promise'); const S = require ('./common/sanctuary'); // concatFiles :: (String -> String) -> Promise Error String const concatFiles = path => most.fromPromise (readFile (path ('index.txt'))) .map (S.lines) .map (S.map (path)) .map (S.map (readFile)) .map (Promise.all.bind (Promise)) .awaitPromises () .map (S.joinWith ('')) .reduce ((x, y) => x + y, ''); const main = () => { concatFiles (join (process.argv[2])) .then (exit0, exit1); }; if (process.mainModule.filename === __filename) main (); ================================================ FILE: node-streams.js ================================================ 'use strict'; const fs = require ('fs'); const miss = require ('mississippi'); const split = require ('split2'); const exit0 = require ('./common/exit0'); const exit1 = require ('./common/exit1'); const join = require ('./common/join'); const readFile = require ('./common/read-file-callback'); const main = () => { const path = join (process.argv[2]); let results; miss.pipe ( fs.createReadStream (path ('index.txt')), split (), miss.parallel (123, (filename, cb) => readFile (path (filename)) (cb)), miss.concat (data => { results = data; }), err => { if (err != null) exit1 (err); else exit0 (results); } ); }; if (process.mainModule.filename === __filename) main (); ================================================ FILE: package.json ================================================ { "name": "async-problem", "private": true, "scripts": { "lint": "eslint -- \"**/*.js\"", "test": "./test" }, "dependencies": { "async": "3.1.0", "bluebird": "3.7.1", "bluebird-promisell": "0.7.0", "co": "4.6.0", "fluture": "11.0.3", "fluture-sanctuary-types": "4.1.1", "mississippi": "4.0.0", "most": "1.7.3", "righto": "6.0.0", "sanctuary": "2.0.0", "sanctuary-def": "0.20.0", "split2": "3.1.1" }, "devDependencies": { "eslint": "5.15.x", "sanctuary-style": "3.0.0" } } ================================================ FILE: promises-pipe.js ================================================ 'use strict'; const exit0 = require ('./common/exit0'); const exit1 = require ('./common/exit1'); const join = require ('./common/join'); const readFile = require ('./common/read-file-promise'); const S = require ('./common/sanctuary'); // then :: (a -> (b | Promise e b)) -> Promise e a -> Promise e b const then = f => p => p.then (f); // concatFiles :: (String -> String) -> Promise Error String const concatFiles = path => S.pipe ([path, readFile, then (S.lines), then (S.map (path)), then (S.map (readFile)), then (Promise.all.bind (Promise)), then (S.joinWith (''))]) ('index.txt'); const main = () => { concatFiles (join (process.argv[2])) .then (exit0, exit1); }; if (process.mainModule.filename === __filename) main (); ================================================ FILE: promises.js ================================================ 'use strict'; const exit0 = require ('./common/exit0'); const exit1 = require ('./common/exit1'); const join = require ('./common/join'); const readFile = require ('./common/read-file-promise'); const S = require ('./common/sanctuary'); // concatFiles :: (String -> String) -> Promise Error String const concatFiles = path => Promise.resolve ('index.txt') .then (path) .then (readFile) .then (S.lines) .then (S.map (path)) .then (S.map (readFile)) .then (Promise.all.bind (Promise)) .then (S.joinWith ('')); const main = () => { concatFiles (join (process.argv[2])) .then (exit0, exit1); }; if (process.mainModule.filename === __filename) main (); ================================================ FILE: righto.js ================================================ 'use strict'; const fs = require ('fs'); const righto = require ('righto'); const S = require ('sanctuary'); const exit0 = require ('./common/exit0'); const exit1 = require ('./common/exit1'); const join = require ('./common/join'); // readFile :: String -> Righto String const readFile = filename => righto (fs.readFile, filename, {encoding: 'utf8'}); // concatFiles :: (String -> String) -> Righto String const concatFiles = path => { const readFileRel = S.compose (readFile) (path); const index = readFileRel ('index.txt'); const files = righto.sync (S.compose (S.map (readFileRel)) (S.lines), index); return righto.sync (S.joinWith (''), righto.all (files)); }; const main = () => { concatFiles (join (process.argv[2])) ((err, data) => { if (err == null) exit0 (data); else exit1 (err); }); }; if (process.mainModule.filename === __filename) main (); ================================================ FILE: synchronous.js ================================================ 'use strict'; const fs = require ('fs'); const S = require ('sanctuary'); const exit0 = require ('./common/exit0'); const exit1 = require ('./common/exit1'); const join = require ('./common/join'); const readFile = filename => { try { return fs.readFileSync (filename, {encoding: 'utf8'}); } catch (err) { exit1 (err); } }; const main = () => { const path = join (process.argv[2]); S.pipe ([path, readFile, S.lines, S.map (path), S.map (readFile), S.joinWith (''), exit0]) ('index.txt'); }; if (process.mainModule.filename === __filename) main (); ================================================ FILE: test ================================================ #!/usr/bin/env bash passed="\033[0;32mpassed\033[0m\n" failed="\033[0;31mfailed\033[0m\n" test() { local actual local expected printf "Testing \033[0;36m%s\033[0m...\n" "$1" printf %s "- correct directory name... " expected="$(printf "FOO\nBAR\nBAZ\n")" if ! actual="$(node "$1" input)" ; then printf "$failed" elif [[ "$actual" == "$expected" ]] ; then printf "$passed" else printf "$failed" fi printf %s "- incorrect directory name... " expected="^Error: ENOENT[,:] .* 'XXX/index.txt'$" if actual="$(node "$1" XXX 2>&1 1>/dev/null)" ; then printf "$failed" elif [[ $? != 1 ]] ; then printf "$failed" elif [[ "$actual" =~ $expected ]] ; then printf "$passed" else printf "$failed" fi printf %s "- inaccurate index... " rm -f -- input/baz.txt expected="^Error: ENOENT[,:] .* 'input/baz.txt'$" if actual="$(node "$1" input 2>&1 1>/dev/null)" ; then printf "$failed" elif [[ $? != 1 ]] ; then printf "$failed" elif [[ "$actual" =~ $expected ]] ; then printf "$passed" else printf "$failed" fi git checkout -- input/baz.txt echo } test synchronous.js test callbacks.js test node-streams.js test async.js test righto.js test promises.js test promises-pipe.js test bluebird-promisell.js test most.js test coroutines-co.js test coroutines-bluebird.js test await.js test futures.js test callbacks-revenge.js