diff --git a/ch08.md b/ch08.md index 1b38f337..f2e682e0 100644 --- a/ch08.md +++ b/ch08.md @@ -490,7 +490,7 @@ There, much better. Now our calling code becomes `findParam('searchTerm').unsafe Callbacks are the narrowing spiral staircase to hell. They are control flow as designed by M.C. Escher. With each nested callback squeezed in between the jungle gym of curly braces and parenthesis, they feel like limbo in an oubliette (how low can we go!). I'm getting claustrophobic chills just thinking about them. Not to worry, we have a much better way of dealing with asynchronous code and it starts with an "F". -The internals are a bit too complicated to spill out all over the page here so we will use `Data.Task` (previously `Data.Future`) from Quildreen Motta's fantastic [Folktale](http://folktalejs.org/). Behold some example usage: +The internals are a bit too complicated to spill out all over the page here so we will use `Data.Task` (previously `Data.Future`) from Quildreen Motta's fantastic [Folktale](http://folktale.origamitower.com/). Behold some example usage: ```js // -- Node readFile example ------------------------------------------ diff --git a/ch10.md b/ch10.md index 486dc660..f4d36af1 100644 --- a/ch10.md +++ b/ch10.md @@ -327,7 +327,7 @@ A good use case for applicatives is when one has multiple functor arguments. The We're almost finished with container apis. We've learned how to `map`, `chain`, and now `ap` functions. In the next chapter, we'll learn how to work better with multiple functors and disassemble them in a principled way. -[Chapter 11: Traversable/Foldable Functors](ch11.md) +[Chapter 11: Transformation Again, Naturally](ch11.md) ## Exercises diff --git a/ch11.md b/ch11.md new file mode 100644 index 00000000..f26b1420 --- /dev/null +++ b/ch11.md @@ -0,0 +1,225 @@ +# Ch11: Transform Again, Naturally + +We are about to discuss *natural transformations* in the context of practical utility in every day code. It just so happens they are a pillar of category theory and absolutely indispensable when applying mathematics to reason about and refactor our code. As such, I believe it is my duty to inform you about the lamentable injustice you are about to witness undoubtedly due to my limited scope. Let's begin. + +## Curse this nest + +I'd like to address the issue of nesting. Not the instinctive urge felt by soon to be mothers wherein they tidy and rearrange with obsessive compulsion, but the...well actually, come to think of it, that isn't far from the mark as we'll see in the coming chapters... In any case, what I mean by *nesting* is to have two or more different types all huddled together around a value, cradling it like a newborn, as it were. + +```js + Right(Maybe('b')) + + IO(Task(IO(1000))) + + [Identity('bee thousand')] +``` + +Until now, we've managed to evade this common scenario with carefully crafted examples, but in practice, as one codes, types tend to tangle themselves up like earbuds in an exorcism. If we don't meticulously keep our types organized as we go along, our code will read hairier than a beatnik in a cat café. + +## A situational comedy + +```js +// getValue :: Selector -> Task Error (Maybe String) +// postComment :: String -> Task Error Comment +// validate :: String -> Either ValidationError String + +// saveComment :: () -> Task Error (Maybe (Either ValidationError (Task Error Comment))) +const saveComment = compose(map(map(map(postComment))), map(map(validate)), getValue('#comment')) +``` + +The gang is all here, much to our type signature's dismay. Allow me to briefly explain the code. We start by getting the user input with `getValue('#comment')` which is an action which retrieves text on an element. Now, it might error finding the element or the value string may not exist so it returns `Task Error (Maybe String)`. After that, we must `map` over both the `Task` and the `Maybe` to pass our text to `validate`, which in turn, gives us back `Either` a `ValidationError` or our `String`. Then onto mapping for days to send the `String` in our current `Task Error (Maybe (Either ValidationError String))` into `postComment` which returns our resulting `Task`. + +What a frightful mess. A collage of abstract types, amateur type expressionism, polymorphic Pollock, monolithic Mondrian. There are many solutions to this common issue. We can compose the types into one monstrous container, sort and `join` a few, homogenize them, deconstruct them, and so on. In this chapter, we'll focus on homogenizing them via *natural transformations*. + +## All natural + +A *Natural Transformation* is a "morphism between functors", that is, a function which operates on the containers themselves. Typewise, it is a function `(Functor f, Functor g) => f a -> g a`. What makes it special is that we cannot, for any reason, peek at the contents of our functor. Think of it as an exchange of highly classified information - the two parties oblivious to what's in the sealed manila envelope stamped "top secret". This is a structural operation. A functorial costume change. Formally, a *natural transformation* is any function for which the following holds: + +natural transformation diagram + +or in code: + +```js +// nt :: (Functor f, Functor g) => f a -> g a +compose(map(f), nt) == compose(nt, map(f)) +``` + +Both the diagram and the code say the same thing: We can run our natural transformation then `map` or `map` then run our natural transformation and get the same result. Incidentally, that follows from a [free theorem](ch7.md#free-as-in-theorem) though natural transformations (and functors) are not limited to functions on types. + +## Principled Type Conversions + +As programmers we are familiar with type conversions. We transform types like `Strings` into `Booleans` and `Integers` into `Floats` (though JavaScript only has `Numbers`). The difference here is simply that we're working with algebraic containers and we have some theory at our disposal. + +Let's look at some of these as examples: + +```js +// either :: (a -> c) -> (b -> c) -> Either a b -> c + +// idToMaybe :: Identity a -> Maybe a +const idToMaybe = x => Maybe.of(x.__value) + +// idToIO :: Identity a -> IO a +const idToIO = x => IO.of(x.__value) + +// eitherToTask :: Either a b -> Task a b +const eitherToTask = either(Task.rejected, Task.of) + +// ioToTask :: IO a -> Task () a +const ioToTask = x => new Task((reject, resolve) => resolve(x.unsafePerform())) + +// maybeToTask :: Maybe a -> Task () a +const maybeToTask = x => x.isNothing() ? Task.rejected() : Task.of(x.__value) + +// arrayToMaybe :: [a] -> Maybe a +const arrayToMaybe = x => Maybe.of(x[0]) +``` + +See the idea? We're just changing one functor to another. We are permitted to lose information along the way so long as the value we'll `map` doesn't get lost in the shape shift shuffle. That is the whole point: `map` must carry on, according to our definition, even after the transformation. + +One way to look at it is that we are transforming our effects. In that light, we can view `ioToTask` as converting synchronous to asynchronous or `arrayToMaybe` from nondeterminism to possible failure. Note that we cannot convert asynchronous to synchronous in JavaScript so we cannot write `taskToIO` - that would be a supernatural transformation. + +## Feature Envy + +Suppose we'd like to use some features from another type like `sortBy` on a `List`. *Natural transformations* provide a nice way to convert to the target type knowing our `map` will be sound. + +```js +const {List} = require('immutable') + +// arrayToList :: [a] -> List a +const arrayToList = List + +const doListyThings = compose(sortBy(h), filter(g), arrayToList, map(f)) +const doListyThings_ = compose(sortBy(h), filter(g), map(f), arrayToList) // law applied +``` + +A wiggle of our nose, three taps of our wand, drop in `arrayToList`, and voilà! Our `[a]` is a `List a` and we can `sortBy` if we please. + +Since `immutable` has optimized/fused operations, we can move the `map(f)` to the left of our *natural transformation* as shown in `doListyThings_`. + +## Isomorphic JavaScript + +When we can completely go back and forth without losing any information, that is considered an *isomorphism*. That's just a fancy word for "holds the same data". We say that two types are *isomorphic* if we can provide the "to" and "from" *natural transformations* as proof: + +```js +// promiseToTask :: Promise a b -> Task a b +const promiseToTask = x => new Task((reject, resolve) => x.then(resolve).catch(reject)) + +// taskToPromise :: Task a b -> Promise a b +const taskToPromise = x => new Promise((resolve, reject) => x.fork(reject, resolve)) + +const x = Promise.resolve('ring') +taskToPromise(promiseToTask(x)) == x + +const y = Task.of('rabbit') +promiseToTask(taskToPromise(y)) == y +``` + +Q.E.D. `Promise` and `Task` are *isomorphic*. We can also write a `listToArray` to complement our `arrayToList` and show that they are too. As a counter example, `arrayToMaybe` is not an *isomorphism* since it loses information: + +```js +// maybeToArray :: Maybe a -> [a] +const maybeToArray = x => x.isNothing() : [] : [x.__value] + +// arrayToMaybe :: [a] -> Maybe a +const arrayToMaybe = x => Maybe.of(x[0]) + +const x = ['elvis costello', 'the attractions'] + +// not isomorphic +maybeToArray(arrayToMaybe(x)) // ['elvis costello'] + +// but is a natural transformation +compose(arrayToMaybe, map(replace('elvis', 'lou')))(x) // Maybe('lou costello') +// == +compose(map(replace('elvis', 'lou'), arrayToMaybe))(x) // Maybe('lou costello') +``` + +They are indeed *natural transformations*, however, since `map` on either side yields the same result. I mention *isomorphisms* here, mid-chapter while we're on the subject, but don't let that fool you, they are an enormously powerful and pervasive concept. Anyways, let's move on. + +## A broader definition + +These structural functions aren't limited to type conversions by any means. + +Here are a few different ones: + +```js +// reverse :: [a] -> [a] + +// join :: (Monad m) => m (m a) -> m a + +// head :: [a] -> a + +// of :: a -> f a +``` + +The natural transformation laws hold for these functions too. One thing that might trip you up is that `head :: [a] -> a` can be viewed as `head :: [a] -> Identity a`. We are free to insert `Identity` wherever we please whilst proving laws since we can, in turn, prove that `a` is isomorphic to `Identity a` (see, I told you *isomorphisms* were pervasive). + +## One nesting solution + +Back to our comedic type signature. We can sprinkle in some *natural transformations* throughout the calling code to coerce each varying type so they are uniform and, therefore, `join`able. + +```js +// getValue :: Selector -> Task Error (Maybe String) +// postComment :: String -> Task Error Comment +// validate :: String -> Either ValidationError String + +// saveComment :: () -> Task Error Comment +const saveComment = compose(chain(postComment), chain(eitherToTask), map(validate), chain(maybeToTask), getValue('#comment')) +``` + +So what do we have here? We've simply added `chain(maybeToTask)` and `chain(eitherToTask)`. Both have the same effect; they naturally transform the functor our `Task` is holding into another `Task` then `join` the two. Like pigeon spikes on a window ledge, we avoid nesting right at the source. As they say in the city of light, "Mieux vaut prévenir que guérir" - an ounce of prevention is worth a pound of cure. + +## In summary + +*Natural transformations* are functions on our functors themselves. They are an extremely important concept in category theory and will start to appear everywhere once more abstractions are adopted, but for now, we've scoped them to a few concrete applications. As we saw, we can achieve different effects by converting types with the guarantee that our composition will hold. They can also help us with nested types, although they have the general effect of homogenizing our functors to the lowest common denominator, which in practice, is the functor with the most volatile effects (`Task` in most cases). + +This continual and tedious sorting of types is the price we pay for having materialized them - summoned them from the ether. Of course, implicit effects are much more insidious and so here we are fighting the good fight. We'll need a few more tools in our tackle before we can reel in the larger type amalgamations. Next up, we'll look at reordering our types with *Traversable*. + +[Chapter 12: Traversing the Stone](ch12.md) + + +## Exercises + +```js +require('../../../part2_exercises/support'); +const {map, compose, prop, chain, sortBy, identity, split, join} = require('ramda'); +const Task = require('data.task'); + +// Exercise 1 +// ========== +// Write a natural transformation that converts `Either b a` to `Maybe a` + +// ex1 :: Either b a -> Maybe a +const ex1 = identity // write me + + +// Exercise 2 +// ========== +// Use the eitherToTask natural transformation change ex2's type signature +// from :: Number -> Task Error (Either Error String) +// to :: Number -> Task Error String + +// findUser :: Number -> Task Error (Either Error User) +const findUser = x => x > 0 ? Task.of(Either.of({id: x, name: 'userface'})) : Task.of(new Left('not found')) + +// eitherToTask :: Either a b -> Task a b +const eitherToTask = either(Task.rejected, Task.of) + +// ex2 :: Number -> Task Error (Either Error User) +const ex2 = compose(map(map(prop('name'))), findUser) // update me + + + +// Exercise 3 +// ========== +// Using split and join, write the isomorphism between String and [Char]. + +// to :: String -> [Char] +const to = identity // write me + +// from :: [Char] -> String +const from = identity // write me + +// ex3 :: String -> String +const ex3 = compose(from, sortBy(identity), to) +``` diff --git a/ch12.md b/ch12.md new file mode 100644 index 00000000..cfdf0fe0 --- /dev/null +++ b/ch12.md @@ -0,0 +1,250 @@ +# Chapter 12: Traversing the Stone + +So far, in our cirque du conteneur, you've seen us tame the ferocious [functor](ch8.md#my-first-functor), bending it to our will to perform any operation that strikes our fancy. You've been dazzled by the juggling of many dangerous effects at once using function [application](ch10.md) to collect the results. Sat there in amazement as containers vanished in thin air by [joining](ch9.md) them together. At the side effect sideshow, we've seen them [composed](ch8.md#a-spot-of-theory) into one. And most recently, we've ventured beyond what's natural and [transformed](ch11.md) one type into another before your very eyes. + +And now for our next trick, we'll look at traversals. We'll watch types soar over one another as if they were trapeze artists holding our value intact. We'll reorder effects like the trolleys in a tilt-a-whirl. When our containers get intertwined like the limbs of a contortionist, we can use this interface to straighten things out. We'll witness different effects with different orderings. Fetch me my pantaloons and slide whistle, let's get started. + +## Types n' types + +Let's get weird: + +```js +// readFile :: FileName -> Task Error String + +// firstWords :: String -> String +const firstWords = compose(join(' '), take(3), split(' ')) + +// tldr :: FileName -> Task Error String +const tldr = compose(map(firstWords), readFile) + +map(tldr, files) +// [Task('hail the monarchy'), Task('smash the patriarchy')] +``` + +Here we read a bunch of files and end up with a useless array of tasks. How might we fork each one of these? It would be most agreeable if we could switch the types around to have `Task Error [String]` instead of `[Task Error String]`. That way, we'd have one future value holding all the results, which is much more amendable to our async needs than several future values arriving at their leisure. + +Here's one last example of a sticky situation: + +```js +// getAttribute :: String -> Node -> Maybe String +// $ :: Selector -> IO Node + +// getControlNode :: IO (Maybe (IO Node)) +const getControlNode = compose(map(map($)), map(getAttribute('aria-controls')), $) +``` + +Look at those `IO`s longing to be together. It'd be just lovely to `join` them, let them dance cheek to cheek, but alas a `Maybe` stands between them like a chaperone at prom. Our best move here would be to shift their positions next to one another, that way each type can be together at last and our signature can be simplified to `IO (Maybe Node)`. + +## Type Feng Shui + +The *Traversable* interface consists of two glorious functions: `sequence` and `traverse`. + +Let's rearrange our types using `sequence`: + +```js +sequence(Array, Maybe(['the facts'])) // [Maybe('the facts')] +sequence(Task.of, Map({a: Task.of(1), b: Task.of(2)})) // Task(Map({a: 1, b: 2})) +sequence(IO.of, Right(IO.of('buckle my shoe'))) // IO(Right('buckle my shoe')) +sequence(Either.of, [Right('wing')]) // Right(['wing']) +sequence(Task.of, Left('wing')) // Task(Left('wing')) +``` + +See what has happened here? Our nested type gets turned inside out like a pair of leather trousers on a humid summer night. The inner functor is shifted to the outside and vice versa. It should be known that `sequence` is bit particular about its arguments. It looks like this: + +```js +// sequence :: (Traversable t, Applicative f) => (a -> f a) -> t (f a) -> f (t a) +const sequence = curry((of, x) => x.sequence(of)) +``` + +Let's start with the second argument. It must be a *Traversable* holding an *Applicative*, which sounds quite restrictive, but just so happens to be the case more often than not. It is the `t (f a)` which gets turned into a `f (t a)`. Isn't that expressive? It's clear as day the two types do-si-do around each other. That first argument there is merely a crutch and only necessary in an untyped language. It is a type constructor (our *of*) provided so that we can invert map-reluctant types like `Left` - more on that in a minute. + +Using `sequence`, we can shift types around with the precision of a sidewalk thimblerigger. But how does it work? Let's look at how a type, say `Either`, would implement it: + +```js +Right.prototype.sequence = function(of) { + return this.__value.map(Right) +} +``` + +Ah yes, if our `__value` is a functor (it must be an applicative, in fact), we can simply `map` our constructor to leap frog the type. + +You may have noticed that we've ignored the `of` entirely. It is passed in for the occasion where mapping is futile, as is the case with `Left`: + +```js +Left.prototype.sequence = function(of) { + return of(this) +} +``` + +We'd like the types to always end up in the same arrangement, therefore it is necessary for types like `Left` who don't actually hold our inner applicative to get a little help in doing so. The *Applicative* interface requires that we first have a *Pointed Functor* so we'll always have a `of` to pass in. In a language with a type system, the outer type can be inferred from the signature and does not need to be explicitly given. + +## Effect assortment + +Different orders have different outcomes where our containers are concerned. If I have `[Maybe a]`, that's a collection of possible values whereas if I have a `Maybe [a]`, that's a possible collection of values. The former indicates we'll be forgiving and keep "the good ones", while the latter means it's an "all or nothing" type of situation. Likewise, `Either Error (Task Error a)` could represent a client side validation and `Task Error (Either Error a)` could be a server side one. Types can be swapped to give us different effects. + +```js +// fromPredicate :: (a -> Bool) -> Either a a + +// partition :: (a -> Bool) -> [a] -> [Either a a] +const partition = f => map(fromPredicate(f)) + +// validate :: (a -> Bool) -> [a] -> Either a [a] +const validate = f => traverse(Either.of, fromPredicate(f)) +``` + +Here we have two different functions based on if we `map` or `traverse`. The first, `partition` will give us an array of `Left`s and `Right`s according to the predicate function. This is useful to keep precious data around for future use rather than filtering it out with the bathwater. `validate` instead will only move forward if everything is hunky dory. By choosing a different type order, we get different behavior. + +## Waltz of the types + +Time to revisit and clean our initial examples. + +```js +// readFile :: FileName -> Task Error String + +// firstWords :: String -> String +const firstWords = compose(join(' '), take(3), split(' ')) + +// tldr :: FileName -> Task Error String +const tldr = compose(map(firstWords), readFile) + +traverse(Task.of, tldr, files) +// Task(['hail the monarchy', 'smash the patriarchy']) +``` + +Using `traverse` instead of `map`, we've successfully herded those unruly `Task`s into a nice coordinated array of results. This is like `Promise.all()`, if you're familiar, except it isn't just a one-off, custom function, no, this works for any *traversable* type. These mathematical apis tend to capture most things we'd like to do in an interoperable, reusable way, rather than each library reinventing these functions for a single type. + +Let's clean up the last example for closure (no, not that kind): + +```js +// getAttribute :: String -> Node -> Maybe String +// $ :: Selector -> IO Node + +// getControlNode :: IO (Maybe Node) +const getControlNode = compose(chain(traverse(IO.of, $)), map(getAttribute('aria-controls')), $) +``` + +Instead of `map(map($))` we have `chain(traverse(IO.of, $))` which inverts our types as it maps then flattens the two `IO`s via `chain`. + +## No law and order + +Well now, before you get all judgemental and bang the backspace button like a gavel to retreat from the chapter, take a moment to recognize that these laws are useful code guarantees. Tis' my conjecture that the goal of most program architecture is an attempt to place useful restrictions on our code to narrow the possibilities, to guide us into the answers as designers and readers. + +An interface without laws is merely indirection. Like any other mathematical structure, we must expose properties for our own sanity. This has a similar effect as encapsulation since it protects the data, enabling us to swap out the interface with another law abiding citizen. + +Come along now, we've got some laws to suss out. + +### Identity + +```js +const identity1 = compose(sequence(Identity.of), map(Identity.of)) +const identity2 = Identity.of + +// test it out with Right +identity1(Right('stuff')) +// Identity(Right('stuff')) + +identity2(Right('stuff')) +// Identity(Right('stuff')) +``` + +This should be straightforward. If we place an `Identity` in our functor, then turn it inside out with `sequence` that's the same as just placing it on the outside to begin with. We chose `Right` as our guinea pig as it is easy to try the law and inspect. An arbitrary functor there is normal, however, the use of a concrete functor here, namely `Identity` in the law itself might raise some eyebrows. Remember a [category](ch5.md#category-theory) is defined by morphisms between its objects that have associative composition and identity. When dealing with the category of functors, natural transformations are the morphisms and `Identity` is, well identity. The `Identity` functor is as fundamental in demonstrating laws as our `compose` function. In fact, we should give up the ghost and follow suit with our [Compose](ch8.md#a-spot-of-theory) type: + +### Composition + +```js +const comp1 = compose(sequence(Compose.of), map(Compose.of)) +const comp2 = (Fof, Gof) => compose(Compose.of, map(sequence(Gof)), sequence(Fof)) + + +// Test it out with some types we have lying around +comp1(Identity(Right([true]))) +// Compose(Right([Identity(true)])) + +comp2(Either.of, Array)(Identity(Right([true]))) +// Compose(Right([Identity(true)])) +``` + +This law preserves composition as one would expect: if we swap compositions of functors, we shouldn't see any surprises since the composition is a functor itself. We arbitrarily chose `true`, `Right`, `Identity`, and `Array` to test it out. Libraries like [quickcheck](https://hackage.haskell.org/package/QuickCheck) or [jsverify](http://jsverify.github.io/) can help us test the law by fuzz testing the inputs. + +As a natural consequence of the above law, we get the ability to [fuse traversals](https://www.cs.ox.ac.uk/jeremy.gibbons/publications/iterator.pdf), which is nice from a performance standpoint. + +### Naturality + +```js +const natLaw1 = (of, nt) => compose(nt, sequence(of)) +const natLaw2 = (of, nt) => compose(sequence(of), map(nt)) + +// test with a random natural transformation and our friendly Identity/Right functors. + +// maybeToEither :: Maybe a -> Either () a +const maybeToEither = x => x.__value ? new Right(x.__value) : new Left() + +natLaw1(Maybe.of, maybeToEither)(Identity.of(Maybe.of('barlow one'))) +// Right(Identity('barlow one')) + +natLaw2(Either.of, maybeToEither)(Identity.of(Maybe.of('barlow one'))) +// Right(Identity('barlow one')) +``` + +This is similar to our identity law. If we first swing the types around then run a natural transformation on the outside, that should equal mapping a natural transformation, then flipping the types. + +A natural consequence of this law is: + +```js +traverse(A.of, A.of) === A.of +``` + +Which, again, is nice from a performance standpoint. + + +## In summary + +*Traversable* is a powerful interface that gives us the ability to rearrange our types with the ease of a telekinetic interior decorator. We can achieve different effects with different orders as well as iron out those nasty type wrinkles that keep us from `join`ing them down. Next, we'll take a bit of a detour to see one of the most powerful interfaces of functional programming and perhaps even algebra itself: [Monoids bring it all together](ch13.md) + +## Exercises + +```js +require('../../../part2_exercises/support'); +const {curry, flip, head, always, map, compose, chain, sequence, traverse} = require('ramda'); +const Task = require('data.task'); +const {Map} = require('immutable-ext'); +const fs = require('fs'); +const futurize = require('futurize').futurize(Task); +const [readFile, readdir] = [futurize(fs.readFile), futurize(fs.readdir)]; +const readfile = curry(flip(readFile)) + +// Exercise 1 +// ========== +// Use the traversable interface to change the type signature of ex1 +// from :: Map Route Route -> Map Route (Task Error JSON) +// to :: Map Route Route -> Task Error (Map Route JSON) + +const httpGet = route => Task.of(`json for ${route}`) + +// ex1 :: Map Route Route -> Map Route (Task Error JSON) +const ex1 = map(httpGet, Map({'/': '/', '/about': '/about'})) + + +// Exercise 2 +// ========== +// Using traversable, update ex2 (and its signature) to only start the game if all players are valid + +// validate :: Player -> Either String Player +const validate = player => player.name ? new Right(player) : new Left('must have name') + +// ex2 :: [Player] -> [Either Error Player] +const ex2 = compose(map(always('game started!')), map(validate)) + + + +// Exercise 3 +// ========== +// Use traversable to rearrange and flatten the nested Tasks + +// first :: [a] -> Maybe a +const first = compose(Maybe.of, head) + +// ex3 :: String -> String +const ex3 = compose(map(map(readfile('utf-8'))), map(first), readdir) + +``` diff --git a/code/part3_exercises/answers/natural_transformations/natural_transformation_exercises.js b/code/part3_exercises/answers/natural_transformations/natural_transformation_exercises.js new file mode 100644 index 00000000..1687c960 --- /dev/null +++ b/code/part3_exercises/answers/natural_transformations/natural_transformation_exercises.js @@ -0,0 +1,43 @@ +require('../../../part2_exercises/support'); +const {map, compose, prop, chain, sortBy, identity, split, join} = require('ramda'); +const Task = require('data.task'); + +// Exercise 1 +// ========== +// Write a natural transformation that converts `Either b a` to `Maybe a` + +// ex1 :: Either b a -> Maybe a +var ex1 = either(() => Maybe.of(null), Maybe.of) + +// Exercise 2 +// ========== +// Use the eitherToTask natural transformation change ex2's type signature +// from :: Number -> Task Error (Either Error String) +// to :: Number -> Task Error String + +// findUser :: Number -> Task Error (Either Error User) +const findUser = x => x > 0 ? Task.of(Either.of({id: x, name: 'userface'})) : Task.of(new Left('not found')) + +// eitherToTask :: Either a b -> Task a b +const eitherToTask = either(Task.rejected, Task.of) + +// ex2 :: Number -> Task Error (Either Error User) +const ex2 = compose(map(prop('name')), chain(eitherToTask), findUser) + + + +// Exercise 3 +// ========== +// Using split and join, write the isomorphism between String and [Char]. + +// to :: String -> [Char] +const to = split('') +// from :: [Char] -> String +const from = join('') + + +// ex3 :: String -> String +const ex3 = compose(from, sortBy(identity), to) + + +module.exports = {ex1, ex2, ex3} diff --git a/code/part3_exercises/answers/natural_transformations/natural_transformation_exercises_spec.js b/code/part3_exercises/answers/natural_transformations/natural_transformation_exercises_spec.js new file mode 100644 index 00000000..e5774012 --- /dev/null +++ b/code/part3_exercises/answers/natural_transformations/natural_transformation_exercises_spec.js @@ -0,0 +1,24 @@ +require('../../../part2_exercises/support'); +var E = require('./natural_transformation_exercises'); +var assert = require("chai").assert; +const Task = require('data.task'); +const {identity} = require('ramda'); + +describe("Natural Transformation Exercises", function(){ + + it('Exercise 1', function(){ + assert.deepEqual(Maybe.of('one eyed willy'), E.ex1(new Right('one eyed willy'))); + assert.deepEqual(Maybe.of(null), E.ex1(new Left('some error'))); + }); + + it('Exercise 2', function(done){ + Task.of(test1 => test2 => [test1, test2]) + .ap(E.ex2(2).map(x => assert.equal(x, 'userface'))) + .ap(E.ex2(0).fold(identity, identity).map(x => assert.equal(x, 'not found'))) //recover error + .fork(() => { throw('ex2 failed') }, () => done()) + }); + + it('Exercise 3', function(){ + assert.equal('emorst', E.ex3('sortme')) + }); +}); diff --git a/code/part3_exercises/answers/traversable/traversable_exercises.js b/code/part3_exercises/answers/traversable/traversable_exercises.js new file mode 100644 index 00000000..1876ea29 --- /dev/null +++ b/code/part3_exercises/answers/traversable/traversable_exercises.js @@ -0,0 +1,45 @@ +require('../../../part2_exercises/support'); +const {curry, flip, head, always, map, compose, chain, sequence, traverse} = require('ramda'); +const Task = require('data.task'); +const {Map} = require('immutable-ext'); +const fs = require('fs'); +const futurize = require('futurize').futurize(Task); +const [readFile, readdir] = [futurize(fs.readFile), futurize(fs.readdir)]; +const readfile = curry(flip(readFile)) + +// Exercise 1 +// ========== +// Use the traversable interface to change the type signature of ex1 +// from :: Map Route Route -> Map Route (Task Error JSON) +// to :: Map Route Route -> Task Error (Map Route JSON) + +const httpGet = route => Task.of(`json for ${route}`) + +// ex1 :: Map Route Route -> Map Route (Task Error JSON) +const ex1 = traverse(Task.of, httpGet, Map({'/': '/', '/about': '/about'})) + + +// Exercise 2 +// ========== +// Using traversable, update ex2 (and its signature) to only start the game if all players are valid + +// validate :: Player -> Either String Player +const validate = player => player.name ? new Right(player) : new Left('must have name') + +// ex2 :: [Player] -> [Either Error Player] +const ex2 = compose(map(always('game started!')), traverse(Either.of, validate)) + + + +// Exercise 3 +// ========== +// Use traversable to rearrange and flatten the nested Tasks + +// first :: [a] -> Maybe a +const first = compose(Maybe.of, head) + +// ex3 :: String -> String +const ex3 = compose(chain(traverse(Task.of, readfile('utf-8'))), map(first), readdir) + + +module.exports = {ex1, ex2, ex3} diff --git a/code/part3_exercises/answers/traversable/traversable_exercises_spec.js b/code/part3_exercises/answers/traversable/traversable_exercises_spec.js new file mode 100644 index 00000000..001442eb --- /dev/null +++ b/code/part3_exercises/answers/traversable/traversable_exercises_spec.js @@ -0,0 +1,31 @@ +require('../../../part2_exercises/support'); +var E = require('./traversable_exercises'); +var assert = require("chai").assert; +const Task = require('data.task'); +const {identity} = require('ramda'); +const {Map} = require('immutable-ext'); + +describe("Traversable Exercises", function(){ + + it('Exercise 1', function(){ + E.ex1.fork(e => { throw 'failed ex1' }, result => + assert.deepEqual(result.get('/'), 'json for /') + ) + }); + + it('Exercise 2', function(){ + const res1 = E.ex2([{name: 'sayid'}, {name: 'carla'}]) + assert.equal(typeof res1.__value, 'string', 'return an either') + assert.equal(res1.__value, 'game started!') + const res2 = E.ex2([{name: ''}, {name: 'carla'}]) + assert.equal(res2.__value, 'must have name') + }); + + it('Exercise 3', function(done) { + E.ex3('.').fork(e => { throw 'failed ex3'}, result => { + assert.equal(typeof result.__value, 'string', 'return a maybe, not a task') + assert(result.__value.match(/traverse/)) + done() + }) + }); +}); diff --git a/code/part3_exercises/exercises/natural_transformations/natural_transformation_exercises.js b/code/part3_exercises/exercises/natural_transformations/natural_transformation_exercises.js new file mode 100644 index 00000000..51d80cc5 --- /dev/null +++ b/code/part3_exercises/exercises/natural_transformations/natural_transformation_exercises.js @@ -0,0 +1,44 @@ +require('../../../part2_exercises/support'); +const {map, compose, prop, chain, sortBy, identity, split, join} = require('ramda'); +const Task = require('data.task'); + +// Exercise 1 +// ========== +// Write a natural transformation that converts `Either b a` to `Maybe a` + +// ex1 :: Either b a -> Maybe a +const ex1 = identity // write me + + +// Exercise 2 +// ========== +// Use the eitherToTask natural transformation change ex2's type signature +// from :: Number -> Task Error (Either Error String) +// to :: Number -> Task Error String + +// findUser :: Number -> Task Error (Either Error User) +const findUser = x => x > 0 ? Task.of(Either.of({id: x, name: 'userface'})) : Task.of(new Left('not found')) + +// eitherToTask :: Either a b -> Task a b +const eitherToTask = either(Task.rejected, Task.of) + +// ex2 :: Number -> Task Error (Either Error User) +const ex2 = compose(map(map(prop('name'))), findUser) // update me + + + +// Exercise 3 +// ========== +// Using split and join, write the isomorphism between String and [Char]. + +// to :: String -> [Char] +const to = identity // write me + +// from :: [Char] -> String +const from = identity // write me + +// ex3 :: String -> String +const ex3 = compose(from, sortBy(identity), to) + + +module.exports = {ex1, ex2, ex3} diff --git a/code/part3_exercises/exercises/natural_transformations/natural_transformation_exercises_spec.js b/code/part3_exercises/exercises/natural_transformations/natural_transformation_exercises_spec.js new file mode 100644 index 00000000..e5774012 --- /dev/null +++ b/code/part3_exercises/exercises/natural_transformations/natural_transformation_exercises_spec.js @@ -0,0 +1,24 @@ +require('../../../part2_exercises/support'); +var E = require('./natural_transformation_exercises'); +var assert = require("chai").assert; +const Task = require('data.task'); +const {identity} = require('ramda'); + +describe("Natural Transformation Exercises", function(){ + + it('Exercise 1', function(){ + assert.deepEqual(Maybe.of('one eyed willy'), E.ex1(new Right('one eyed willy'))); + assert.deepEqual(Maybe.of(null), E.ex1(new Left('some error'))); + }); + + it('Exercise 2', function(done){ + Task.of(test1 => test2 => [test1, test2]) + .ap(E.ex2(2).map(x => assert.equal(x, 'userface'))) + .ap(E.ex2(0).fold(identity, identity).map(x => assert.equal(x, 'not found'))) //recover error + .fork(() => { throw('ex2 failed') }, () => done()) + }); + + it('Exercise 3', function(){ + assert.equal('emorst', E.ex3('sortme')) + }); +}); diff --git a/code/part3_exercises/exercises/traversable/traversable_exercises.js b/code/part3_exercises/exercises/traversable/traversable_exercises.js new file mode 100644 index 00000000..fdff3d23 --- /dev/null +++ b/code/part3_exercises/exercises/traversable/traversable_exercises.js @@ -0,0 +1,45 @@ +require('../../../part2_exercises/support'); +const {curry, flip, head, always, map, compose, chain, sequence, traverse} = require('ramda'); +const Task = require('data.task'); +const {Map} = require('immutable-ext'); +const fs = require('fs'); +const futurize = require('futurize').futurize(Task); +const [readFile, readdir] = [futurize(fs.readFile), futurize(fs.readdir)]; +const readfile = curry(flip(readFile)) + +// Exercise 1 +// ========== +// Use the traversable interface to change the type signature of ex1 +// from :: Map Route Route -> Map Route (Task Error JSON) +// to :: Map Route Route -> Task Error (Map Route JSON) + +const httpGet = route => Task.of(`json for ${route}`) + +// ex1 :: Map Route Route -> Map Route (Task Error JSON) +const ex1 = map(httpGet, Map({'/': '/', '/about': '/about'})) + + +// Exercise 2 +// ========== +// Using traversable, update ex2 (and its signature) to only start the game if all players are valid + +// validate :: Player -> Either String Player +const validate = player => player.name ? new Right(player) : new Left('must have name') + +// ex2 :: [Player] -> [Either Error Player] +const ex2 = compose(map(always('game started!')), map(validate)) + + + +// Exercise 3 +// ========== +// Use traversable to rearrange and flatten the nested Tasks + +// first :: [a] -> Maybe a +const first = compose(Maybe.of, head) + +// ex3 :: String -> String +const ex3 = compose(map(map(readfile('utf-8'))), map(first), readdir) + + +module.exports = {ex1, ex2, ex3} diff --git a/code/part3_exercises/exercises/traversable/traversable_exercises_spec.js b/code/part3_exercises/exercises/traversable/traversable_exercises_spec.js new file mode 100644 index 00000000..001442eb --- /dev/null +++ b/code/part3_exercises/exercises/traversable/traversable_exercises_spec.js @@ -0,0 +1,31 @@ +require('../../../part2_exercises/support'); +var E = require('./traversable_exercises'); +var assert = require("chai").assert; +const Task = require('data.task'); +const {identity} = require('ramda'); +const {Map} = require('immutable-ext'); + +describe("Traversable Exercises", function(){ + + it('Exercise 1', function(){ + E.ex1.fork(e => { throw 'failed ex1' }, result => + assert.deepEqual(result.get('/'), 'json for /') + ) + }); + + it('Exercise 2', function(){ + const res1 = E.ex2([{name: 'sayid'}, {name: 'carla'}]) + assert.equal(typeof res1.__value, 'string', 'return an either') + assert.equal(res1.__value, 'game started!') + const res2 = E.ex2([{name: ''}, {name: 'carla'}]) + assert.equal(res2.__value, 'must have name') + }); + + it('Exercise 3', function(done) { + E.ex3('.').fork(e => { throw 'failed ex3'}, result => { + assert.equal(typeof result.__value, 'string', 'return a maybe, not a task') + assert(result.__value.match(/traverse/)) + done() + }) + }); +}); diff --git a/code/part3_exercises/package.json b/code/part3_exercises/package.json new file mode 100644 index 00000000..842f6674 --- /dev/null +++ b/code/part3_exercises/package.json @@ -0,0 +1,30 @@ +{ + "name": "MAG_Part2_Exercises", + "version": "0.0.1", + "description": "Exercises for Part 2 of the Book", + "main": "index.js", + "dependencies": { + "chai": "^3.5.0", + "data.task": "^3.0.0", + "futurize": "^1.2.0", + "immutable": "^3.8.2", + "immutable-ext": "^1.1.2", + "ramda": "^0.23.0" + }, + "devDependencies": { + "mocha": "^3.2.0" + }, + "scripts": { + "test": "node_modules/.bin/mocha exercises/**/*_spec.js" + }, + "repository": { + "type": "git", + "url": "https://github.com/DrBoolean/mostly-adequate-guide" + }, + "author": "", + "license": "ISC", + "bugs": { + "url": "https://github.com/DrBoolean/mostly-adequate-guide/issues" + }, + "homepage": "https://github.com/DrBoolean/mostly-adequate-guide" +} diff --git a/images/id_to_maybe.png b/images/id_to_maybe.png new file mode 100644 index 00000000..8c93cb80 Binary files /dev/null and b/images/id_to_maybe.png differ diff --git a/images/natural_transformation.png b/images/natural_transformation.png new file mode 100644 index 00000000..2141f24f Binary files /dev/null and b/images/natural_transformation.png differ