diff --git a/SUMMARY.md b/SUMMARY.md index b0de6991..75dad7a0 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -80,18 +80,46 @@ * [In Summary](ch12.md#in-summary) * [Exercises](ch12.md#exercises) * [Appendix A: Essential Functions Support](appendix_a.md) - * [chain](appendix_a.md#chain) + * [always](appendix_a.md#always) * [compose](appendix_a.md#compose) * [curry](appendix_a.md#curry) * [either](appendix_a.md#either) + * [identity](appendix_a.md#identity) * [inspect](appendix_a.md#inspect) - * [join](appendix_a.md#join) - * [liftA2](appendix_a.md#liftA2) - * [liftA3](appendix_a.md#liftA3) + * [left](appendix_a.md#left) + * [liftA\*](appendix_a.md#lifta) * [maybe](appendix_a.md#maybe) + * [nothing](appendix_a.md#maybe) + * [reject](appendix_a.md#reject) * [Appendix B: Algebraic Structures Support](appendix_b.md) - * [Either](appendix_b.md#Either) - * [Identity](appendix_b.md#Identity) - * [IO](appendix_b.md#IO) - * [Maybe](appendix_b.md#Maybe) + * [Compose](appendix_b.md#compose) + * [Either](appendix_b.md#either) + * [Identity](appendix_b.md#identity) + * [IO](appendix_b.md#io) + * [List](appendix_b.md#list) + * [Map](appendix_b.md#map) + * [Maybe](appendix_b.md#maybe) + * [Task](appendix_b.md#task) * [Appendix C: Pointfree Utilities](appendix_c.md) + * [chain](appendix_c.md#chain) + * [concat](appendix_c.md#concat) + * [filter](appendix_c.md#filter) + * [flip](appendix_c.md#flip) + * [forEach](appendix_c.md#foreach) + * [head](appendix_c.md#head) + * [intercalate](appendix_c.md#intercalate) + * [join](appendix_c.md#join) + * [last](appendix_c.md#last) + * [map](appendix_c.md#map) + * [match](appendix_c.md#match) + * [prop](appendix_c.md#prop) + * [reduce](appendix_c.md#reduce) + * [safeHead](appendix_c.md#safehead) + * [safeLast](appendix_c.md#safelast) + * [safeProp](appendix_c.md#safeprop) + * [sequence](appendix_c.md#sequence) + * [sortBy](appendix_c.md#sortby) + * [split](appendix_c.md#split) + * [take](appendix_c.md#take) + * [traverse](appendix_c.md#traverse) + * [unsafePerformIO](appendix_c.md#unsafeperformio) diff --git a/appendix_a.md b/appendix_a.md index be990464..096b4b10 100644 --- a/appendix_a.md +++ b/appendix_a.md @@ -1,4 +1,4 @@ -# Appendix A: Functions Support +# Appendix A: Essential Functions Support In this appendix, you'll find some basic JavaScript implementations of various functions described in the book. Keep in mind that these implementations may not be the fastest or the @@ -9,23 +9,18 @@ In order to find functions that are more production-ready, have a peak at Note that some functions also refer to algebraic structures defined in the [Appendix B](./appendix_b.md) -## chain - -```hs -chain :: Monad m => (a -> m b) -> m a -> m b -``` +## always ```js -const chain = f => compose(join, map(f)) +// always :: a -> b -> a +const always = curry((a, b) => a); ``` -## compose -```hs -compose :: ((a -> b), (b -> c), ..., (y -> z)) -> a -> z -``` +## compose ```js +// compose :: ((a -> b), (b -> c), ..., (y -> z)) -> a -> z function compose(...fns) { const n = fns.length; @@ -41,13 +36,11 @@ function compose(...fns) { } ``` -## curry -```hs -curry :: ((a, b, ...) -> c) -> a -> b -> ... -> c -``` +## curry ```js +// curry :: ((a, b, ...) -> c) -> a -> b -> ... -> c function curry(fn) { const arity = fn.length; @@ -61,13 +54,11 @@ function curry(fn) { } ``` -## either -```hs -either :: (a -> c) -> (b -> c) -> Either a b -> c -``` +## either ```js +// either :: (a -> c) -> (b -> c) -> Either a b -> c const either = curry((f, g, e) => { if (e.isLeft) { return f(e.$value); @@ -77,13 +68,19 @@ const either = curry((f, g, e) => { }); ``` -## inspect -```hs -inspect :: a -> String +## identity + +```js +// identity :: x -> x +const identity = x => x; ``` + +## inspect + ```js +// inspect :: a -> String function inspect(x) { if (x && typeof x.inspect === 'function') { return x.inspect(); @@ -93,52 +90,53 @@ function inspect(x) { return f.name ? f.name : f.toString(); } + function inspectTerm(t) { + switch (typeof t) { + case 'string': + return `'${t}'`; + case 'object': { + const ts = Object.keys(t).map(k => [k, inspect(t[k])]); + return `{${ts.map(kv => kv.join(': ')).join(', ')}}`; + } + default: + return String(t); + } + } + function inspectArgs(args) { - const str = args.reduce((acc, x) => `${acc}, ${inspect(x)}`, ''); - return `(${str})`; + return Array.isArray(args) ? `[${args.map(inspect).join(', ')}]` : inspectTerm(args); } return (typeof x === 'function') ? inspectFn(x) : inspectArgs(x); } ``` -## join -```hs -join :: Monad m => m (m a) -> m a -``` +## left ```js -const join = m => m.join(); +// left :: a -> Either a b +const left = a => new Left(a); ``` -## liftA2 -```hs -liftA2 :: (Applicative f) => (a1 -> a2 -> b) -> f a1 -> f a2 -> f b -``` +## liftA\* ```js +// liftA2 :: (Applicative f) => (a1 -> a2 -> b) -> f a1 -> f a2 -> f b const liftA2 = curry((fn, a1, a2) => a1.map(fn).ap(a2)); ``` -## liftA3 - -```hs -liftA3 :: (Applicative f) => (a1 -> a2 -> a3 -> b) -> f a1 -> f a2 -> f a3 -> f b -``` - ```js -const liftA3 = curry((fn, a1, a2, a3) => a1.map(fn).ap(a2).ap(a3)) +// liftA3 :: (Applicative f) => (a1 -> a2 -> a3 -> b) -> f a1 -> f a2 -> f a3 -> f b +const liftA3 = curry((fn, a1, a2, a3) => a1.map(fn).ap(a2).ap(a3)); ``` -## maybe -```hs -maybe :: b -> (a -> b) -> Maybe a -> b -``` +## maybe ```js +// maybe :: b -> (a -> b) -> Maybe a -> b const maybe = curry((v, f, m) => { if (m.isNothing) { return v; @@ -147,3 +145,18 @@ const maybe = curry((v, f, m) => { return f(m.$value); }); ``` + + +## nothing + +```js +// nothing :: () -> Maybe a +const nothing = () => Maybe.of(null); +``` + +## reject + +```js +// reject :: a -> Task a b +const reject = a => Task.rejected(a); +``` diff --git a/appendix_b.md b/appendix_b.md index e28d3f10..5ecf1859 100644 --- a/appendix_b.md +++ b/appendix_b.md @@ -9,17 +9,48 @@ or [fantasy-land](https://github.com/fantasyland). Note that some methods also refer to functions defined in the [Appendix A](./appendix_a.md) -## Either +## Compose ```js -class Either { +const createCompose = curry((F, G) => class Compose { + constructor(x) { + this.$value = x; + } + + inspect() { + return `Compose(${inspect(this.$value)})`; + } + + // ----- Pointed (Compose F G) static of(x) { - return new Right(x); + return new Compose(F(G(x))); } + // ----- Functor (Compose F G) + map(fn) { + return new Compose(this.$value.map(x => x.map(fn))); + } + + // ----- Applicative (Compose F G) + ap(f) { + return f.map(this.$value); + } +}); +``` + + +## Either + +```js +class Either { constructor(x) { this.$value = x; } + + // ----- Pointed (Either a) + static of(x) { + return new Right(x); + } } class Left extends Either { @@ -31,24 +62,36 @@ class Left extends Either { return false; } - ap() { + inspect() { + return `Left(${inspect(this.$value)})`; + } + + // ----- Functor (Either a) + map() { return this; } - chain() { + // ----- Applicative (Either a) + ap() { return this; } - inspect() { - return `Left(${inspect(this.$value)})`; + // ----- Monad (Either a) + chain() { + return this; } join() { return this; } - map() { - return this; + // ----- Traversable (Either a) + sequence(of) { + return of(this); + } + + traverse(of, fn) { + return of(this); } } @@ -61,24 +104,36 @@ class Right extends Either { return true; } + inspect() { + return `Right(${inspect(this.$value)})`; + } + + // ----- Functor (Either a) + map(fn) { + return Either.of(fn(this.$value)); + } + + // ----- Applicative (Either a) ap(f) { return f.map(this.$value); } + // ----- Monad (Either a) chain(fn) { return fn(this.$value); } - inspect() { - return `Right(${inspect(this.$value)})`; - } - join() { return this.$value; } - map(fn) { - return Either.of(fn(this.$value)); + // ----- Traversable (Either a) + sequence(of) { + return this.traverse(of, identity); + } + + traverse(of, fn) { + fn(this.$value).map(Either.of); } } ``` @@ -87,32 +142,45 @@ class Right extends Either { ```js class Identity { + constructor(x) { + this.$value = x; + } + + inspect() { + return `Identity(${inspect(this.$value)})`; + } + + // ----- Pointed Identity static of(x) { return new Identity(x); } - constructor(x) { - this.$value = x; + // ----- Functor Identity + map(fn) { + return Identity.of(fn(this.$value)); } + // ----- Applicative Identity ap(f) { return f.map(this.$value); } + // ----- Monad Identity chain(fn) { return this.map(fn).join(); } - inspect() { - return `Identity(${inspect(this.$value)})`; - } - join() { return this.$value; } - map(fn) { - return Identity.of(fn(this.$value)); + // ----- Traversable Identity + sequence(of) { + return this.traverse(of, identity); + } + + traverse(of, fn) { + return fn(this.$value).map(Identity.of); } } ``` @@ -121,36 +189,127 @@ class Identity { ```js class IO { + constructor(fn) { + this.unsafePerformIO = fn; + } + + inspect() { + return `IO(?)`; + } + + // ----- Pointed IO static of(x) { return new IO(() => x); } - constructor(fn) { - this.unsafePerformIO = fn; + // ----- Functor IO + map(fn) { + return new IO(compose(fn, this.unsafePerformIO)); } + // ----- Applicative IO ap(f) { return this.chain(fn => f.map(fn)); } + // ----- Monad IO chain(fn) { return this.map(fn).join(); } + join() { + return this.unsafePerformIO(); + } +} +``` + +## List + +```js +class List { + constructor(xs) { + this.$value = xs; + } + inspect() { - return `IO(${inspect(this.unsafePerformIO)})`; + return `List(${inspect(this.$value)})`; } - join() { - return this.unsafePerformIO(); + concat(x) { + return new List(this.$value.concat(x)); } + // ----- Pointed List + static of(x) { + return new List([x]); + } + + // ----- Functor List map(fn) { - return new IO(compose(fn, this.unsafePerformIO)); + return new List(this.$value.map(fn)); + } + + // ----- Traversable List + sequence(of) { + return this.traverse(of, identity); + } + + traverse(of, fn) { + return this.$value.reduce( + (f, a) => fn(a).map(b => bs => bs.concat(b)).ap(f), + of(new List([])), + ); + } +} +``` + + +## Map + +```js +class Map { + constructor(x) { + this.$value = x; + } + + inspect() { + return `Map(${inspect(this.$value)})`; + } + + insert(k, v) { + const singleton = {}; + singleton[k] = v; + return Map.of(Object.assign({}, this.$value, singleton)); + } + + reduceWithKeys(fn, zero) { + return Object.keys(this.$value) + .reduce((acc, k) => fn(acc, this.$value[k], k), zero); + } + + // ----- Functor (Map a) + map(fn) { + return this.reduceWithKeys( + (m, v, k) => m.insert(k, fn(v)), + new Map({}), + ); + } + + // ----- Traversable (Map a) + sequence(of) { + return this.traverse(of, identity); + } + + traverse(of, fn) { + return this.reduceWithKeys( + (f, a, k) => fn(a).map(b => m => m.insert(k, b)).ap(f), + of(new Map({})), + ); } } ``` + ## Maybe > Note that `Maybe` could also be defined in a similar fashion as we did for `Either` with two @@ -158,36 +317,95 @@ class IO { ```js class Maybe { - static of(x) { - return new Maybe(x); - } - get isNothing() { return this.$value === null || this.$value === undefined; } + get isJust() { + return !this.isNothing; + } + constructor(x) { this.$value = x; } + inspect() { + return `Maybe(${inspect(this.$value)})`; + } + + // ----- Pointed Maybe + static of(x) { + return new Maybe(x); + } + + // ----- Functor Maybe + map(fn) { + return this.isNothing ? this : Maybe.of(fn(this.$value)); + } + + // ----- Applicative Maybe ap(f) { return this.isNothing ? this : f.map(this.$value); } + // ----- Monad Maybe chain(fn) { return this.map(fn).join(); } + join() { + return this.isNothing ? this : this.$value; + } + + // ----- Traversable Maybe + sequence(of) { + this.traverse(of, identity); + } + + traverse(of, fn) { + return this.isNothing ? of(this) : fn(this.$value).map(Maybe.of); + } +} +``` + +## Task + +```js +class Task { + constructor(fork) { + this.fork = fork; + } + inspect() { - return `Maybe(${inspect(this.$value)})`; + return 'Task(?)'; } - join() { - return this.isNothing ? this : this.$value; + static rejected(x) { + return new Task((reject, _) => reject(x)); + } + + // ----- Pointed (Task a) + static of(x) { + return new Task((_, resolve) => resolve(x)); } + // ----- Functor (Task a) map(fn) { - return this.isNothing ? this : Maybe.of(fn(this.$value)); + return new Task((reject, resolve) => this.fork(reject, compose(resolve, fn))); + } + + // ----- Applicative (Task a) + ap(f) { + return this.chain(fn => f.map(fn)); + } + + // ----- Monad (Task a) + chain(fn) { + return new Task((reject, resolve) => this.fork(reject, x => fn(x).fork(reject, resolve))); + } + + join() { + return this.chain(identity); } } ``` diff --git a/appendix_c.md b/appendix_c.md new file mode 100644 index 00000000..a1028e77 --- /dev/null +++ b/appendix_c.md @@ -0,0 +1,180 @@ +# Appendix C: Pointfree Utilities + +In this appendix, you'll find pointfree versions of rather classic JavaScript functions +described in the book. All of the following functions are seemingly available in exercises, as +part of the global context. Keep in mind that these implementations may not be the fastest or +the most efficient implementation out there; they *solely serve an educational purpose*. + +In order to find functions that are more production-ready, have a peak at +[ramda](http://ramdajs.com/), [lodash](https://lodash.com/), or [folktale](http://folktale.origamitower.com/). + +Note that functions refer to the `curry` & `compose` functions defined in [Appendix A](./appendix_a.md) + +## add + +```js +// add :: Number -> Number -> Number +const add = curry((a, b) => a + b); +``` + +## chain + +```js +// chain :: Monad m => (a -> m b) -> m a -> m b +const chain = curry((fn, m) => m.chain(fn)); +``` + +## concat + +```js +// concat :: String -> String -> String +const concat = curry((a, b) => a.concat(b)); +``` + +## filter + +```js +// filter :: (a -> Boolean) -> [a] -> [a] +const filter = curry((fn, xs) => xs.filter(fn)); +``` + +## flip + +```js +// flip :: (a -> b) -> (b -> a) +const flip = curry((fn, a, b) => fn(b, a)); +``` + +## forEach + +```js +// forEach :: (a -> ()) -> [a] -> () +const forEach = curry((fn, xs) => xs.forEach(fn)); +``` + +## head + +```js +// head :: [a] -> a +const head = xs => xs[0]; +``` + +## intercalate + +```js +// intercalate :: String -> [String] -> String +const intercalate = curry((str, xs) => xs.join(str)); +``` + +## join + +```js +// join :: Monad m => m (m a) -> m a +const join = m => m.join(); +``` + +## last + +```js +// last :: [a] -> a +const last = xs => xs[xs.length - 1]; +``` + +## map + +```js +// map :: Functor f => (a -> f b) -> f a -> f b +const map = curry((fn, f) => f.map(fn)); +``` + +## match + +```js +// match :: RegExp -> String -> Boolean +const match = curry((re, str) => re.test(str)); +``` + +## prop + +```js +// prop :: String -> Object -> a +const prop = curry((p, obj) => obj[p]); +``` + +## reduce + +```js +// reduce :: (b -> a -> b) -> b -> [a] -> b +const reduce = curry((fn, zero, xs) => xs.reduce(fn, zero)); +``` + +## safeHead + +```js +// safeHead :: [a] -> Maybe a +const safeHead = compose(Maybe.of, head); +``` + +## safeLast + +```js +// safeLast :: [a] -> Maybe a +const safeLast = compose(Maybe.of, last); +``` + +## safeProp + +```js +// safeProp :: String -> Object -> Maybe a +const safeProp = curry((p, obj) => compose(Maybe.of, prop(p))(obj)); +``` + +## sequence + +```js +// sequence :: (Applicative f, Traversable t) => (a -> f a) -> t (f a) -> f (t a) +const sequence = curry((of, f) => f.sequence(of)); +``` + +## sortBy + +```js +// sortBy :: Ord b => (a -> b) -> [a] -> [a] +const sortBy = curry((fn, xs) => { + return xs.sort((a, b) => { + if (fn(a) === fn(b)) { + return 0; + } + + return fn(a) > fn(b) ? 1 : -1; + }); +}); +``` + +## split + +```js +// split :: String -> String -> [String] +const split = curry((sep, str) => str.split(sep)); +``` + +## take + +```js +// take :: Number -> [a] -> [a] +const take = curry((n, xs) => xs.slice(0, n)); +``` + +## traverse + +```js +// traverse :: (Applicative f, Traversable t) => (a -> f a) -> (a -> f b) -> t a -> f (t b) +const traverse = curry((of, fn, f) => f.traverse(of, fn)); +``` + +## unsafePerformIO + +```js +// unsafePerformIO :: IO a -> a +const unsafePerformIO = io => io.unsafePerformIO(); +``` diff --git a/ch12.md b/ch12.md index bd735059..0f0af56f 100644 --- a/ch12.md +++ b/ch12.md @@ -43,7 +43,7 @@ Let's rearrange our types using `sequence`: ```js sequence(List.of, Maybe(['the facts'])); // [Just('the facts')] -sequence(Task.of, Map.of({ a: Task.of(1), b: Task.of(2) })); // Task(Map({ a: 1, b: 2 })) +sequence(Task.of, new Map({ a: Task.of(1), b: Task.of(2) })); // Task(Map({ a: 1, b: 2 })) sequence(IO.of, Either.of(IO.of('buckle my shoe'))); // IO(Right('buckle my shoe')) sequence(Either.of, [Either.of('wing')]); // Right(['wing']) sequence(Task.of, left('wing')); // Task(Left('wing')) @@ -215,7 +215,7 @@ Considering the following elements: // httpGet :: Route -> Task Error JSON // routes :: Map Route Route -const routes = Map.of({ '/': '/', '/about': '/about' }); +const routes = new Map({ '/': '/', '/about': '/about' }); ``` diff --git a/exercises/ch12/exercise_a.js b/exercises/ch12/exercise_a.js index b3712a5d..e272be91 100644 --- a/exercises/ch12/exercise_a.js +++ b/exercises/ch12/exercise_a.js @@ -2,7 +2,7 @@ // // // httpGet :: Route -> Task Error JSON // // routes :: Map Route Route -// const routes = Map.of({ '/': '/', '/about': '/about' }); +// const routes = new Map({ '/': '/', '/about': '/about' }); // // Use the traversable interface to change the type signature of `getJsons`. // diff --git a/exercises/ch12/validation_b.js b/exercises/ch12/validation_b.js index 41b9c5f6..101e223f 100644 --- a/exercises/ch12/validation_b.js +++ b/exercises/ch12/validation_b.js @@ -1,6 +1,6 @@ /* globals startGame */ -const res = startGame(List.of([albert, theresa])); +const res = startGame(new List([albert, theresa])); assert( res instanceof Either, @@ -12,7 +12,7 @@ assert( 'The function gives incorrect results; a game should have started for a list of valid players!', ); -const rej = startGame(List.of([gary, { what: 14 }])); +const rej = startGame(new List([gary, { what: 14 }])); assert( rej.isLeft && rej.$value === 'must have name', 'The function gives incorrect results; a game shouldn\'t be started if the list contains invalid players!', diff --git a/exercises/support.js b/exercises/support.js index c98d8560..43f60dcc 100644 --- a/exercises/support.js +++ b/exercises/support.js @@ -266,7 +266,6 @@ class Right extends Either { return `(Either ? ${getType(this.$value)})`; } - join() { return this.$value; } @@ -369,10 +368,6 @@ class IO { class Map { - static of(x) { - return new Map(x); - } - constructor(x) { assert( typeof x === 'object' && x !== null, @@ -395,7 +390,7 @@ class Map { insert(k, v) { const singleton = {}; singleton[k] = v; - return Map.of(Object.assign({}, this.$value, singleton)); + return new Map(Object.assign({}, this.$value, singleton)); } reduce(fn, zero) { @@ -421,15 +416,15 @@ class Map { traverse(of, fn) { return this.reduceWithKeys( (f, a, k) => fn(a).map(b => m => m.insert(k, b)).ap(f), - of(Map.of({})), + of(new Map({})), ); } } class List { - static of(xs) { - return new List(xs); + static of(x) { + return new List([x]); } constructor(xs) { @@ -442,7 +437,7 @@ class List { } concat(x) { - return List.of(this.$value.concat(x)); + return new List(this.$value.concat(x)); } inspect() { @@ -456,7 +451,7 @@ class List { } map(fn) { - return List.of(this.$value.map(fn)); + return new List(this.$value.map(fn)); } sequence(of) { @@ -466,7 +461,7 @@ class List { traverse(of, fn) { return this.$value.reduce( (f, a) => fn(a).map(b => bs => bs.concat(b)).ap(f), - of(List.of([])), + of(new List([])), ); } } @@ -620,7 +615,7 @@ const map = curry(function map(fn, f) { const sequence = curry(function sequence(of, x) { assert( typeof of === 'function' && typeof x.sequence === 'function', - typeMismatch('(Applicative x, Traversable t) => (a -> f a) -> t (f a) -> f (t a)', [getType(of), getType(x), 'f (t a)'].join(' -> '), 'sequence'), + typeMismatch('(Applicative f, Traversable t) => (a -> f a) -> t (f a) -> f (t a)', [getType(of), getType(x), 'f (t a)'].join(' -> '), 'sequence'), ); return x.sequence(of); @@ -630,7 +625,7 @@ const traverse = curry(function traverse(of, fn, x) { assert( typeof of === 'function' && typeof fn === 'function' && typeof x.traverse === 'function', typeMismatch( - '(Applicative x, Traversable t) => (a -> f a) -> (a -> f b) -> t (f a) -> f (t b)', + '(Applicative f, Traversable t) => (a -> f a) -> (a -> f b) -> t a -> f (t b)', [getType(of), getType(fn), getType(x), 'f (t b)'].join(' -> '), 'traverse', ), @@ -959,7 +954,7 @@ const eitherToTask = namedAs('eitherToTask', either(Task.rejected, Task.of)); const httpGet = function httpGet(route) { return Task.of(`json for ${route}`); }; -const routes = Map.of({ +const routes = new Map({ '/': '/', '/about': '/about', });