Task Error [String]instead of
[Task Error String]. That way, we'd have one future value holding all the results, which is much more amenable to our async needs than several future values arriving at their leisure.
IOs longing to be together. It'd be just lovely to
jointhem, let them dance cheek to cheek, but alas a
Maybestands 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).
sequenceis bit particular about its arguments. It looks like this:
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.
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:
$valueis a functor (it must be an applicative, in fact), we can simply
mapour constructor to leap frog the type.
ofentirely. It is passed in for the occasion where mapping is futile, as is the case with
Leftwho 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
ofto 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.
[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.
traverse. The first,
partitionwill give us an array of
Rights according to the predicate function. This is useful to keep precious data around for future use rather than filtering it out with the bathwater.
validateinstead will give us the first item that fails the predicate in
Left, or all the items in
Rightif everything is hunky dory. By choosing a different type order, we get different behavior.
List, to see how the
validatemethod is made.
reduceon the list. The reduce function is
(f, a) => fn(a).map(b => bs => bs.concat(b)).ap(f), which looks a bit scary, so let's step through it.
reduce :: [a] -> (f -> a -> f) -> f -> f. The first argument is actually provided by the dot-notation on
$value, so it's a list of things. Then we need a function from a
f(the accumulator) and a
a(the iteree) to return us a new accumulator.
of(new List()), which in our case is
Right() :: Either e [a]. Notice that
Either e [a]will also be our final resulting type!
fn :: Applicative f => a -> f a
fromPredicate(f) :: a -> Either e a.
fn(a) :: Either e a
.map(b => bs => bs.concat(b))
Either.mappasses the right value to the function and returns a new
Rightwith the result. In this case the function has one parameter (
b), and returns another function (
bs => bs.concat(b), where
bis in scope due to the closure). When
Left, the left value is returned.
fn(a).map(b => bs => bs.concat(b)) :: Either e ([a] -> [a])
fis an Applicative here, so we can apply the function
bs => bs.concat(b)to whatever value
bs :: [a]is in
f. Fortunately for us,
fcomes from our initial seed and has the following type:
f :: Either e [a]which is by the way, preserved when we apply
bs => bs.concat(b). When
Right, this calls
bs => bs.concat(b), which returns a
Rightwith the item added to the list. When
Left, the left value (from the previous step or previous iteration respectively) is returned.
fn(a).map(b => bs => bs.concat(b)).ap(f) :: Either e [a]
List.traverse, and is accomplished with
ap, so will work for any Applicative Functor. This is a great example of how those abstraction can help to write highly generic code with only a few assumptions (that can, incidentally, be declared and checked at the type level!).
map, we've successfully herded those unruly
Tasks 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.
chain(traverse(IO.of, $))which inverts our types as it maps then flattens the two
Identityin our functor, then turn it inside out with
sequencethat's the same as just placing it on the outside to begin with. We chose
Rightas 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
Identityin the law itself might raise some eyebrows. Remember a category 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
Identityis, well identity. The
Identityfunctor is as fundamental in demonstrating laws as our
composefunction. In fact, we should give up the ghost and follow suit with our Compose type:
Arrayto test it out. Libraries like quickcheck or jsverify can help us test the law by fuzz testing the inputs.
joining 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
getJsonsto Map Route Route → Task Error (Map Route JSON)
startGame(and its signature) to only start the game if all players are valid