Functional abstractions in JavaScript: fishing fmaps with Kleisli
Introduction
In a previous post we used jsverify
to check that a JavaScript array
satisfies Functor's law (about identity and composition).
This time we are going to specialize our notion of Functor. Again we will look at their laws and validate them through property testing.
Specialized Functors provide a way to write sequential programs in a pure style (i.e., side effects not allowed).
Let's use an input validator as running example. In our programs we tend to do things like the following:
// Input const myStrings = ["Some", "cat", "superVeryLong"]; // Utils var handleInvalidStrings = (strings, predicate, fixInvalidString, addErrorMessageOnInvalidString) => strings.reduce( (accumulator, string) => { if (predicate(string)) { return { strings: accumulator.strings.concat(string), errors: accumulator.errors }; } else { const fixedString = fixInvalidString(string); const error = addErrorMessageOnInvalidString(string, fixedString); return { strings: accumulator.strings.concat(fixedString), errors: accumulator.errors.concat(error) } } }, {strings: [], errors: []} ) var capitalized = (s) => s.charAt(0) === s.charAt(0).toUpperCase(); var capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1); // https://stackoverflow.com/questions/1026069/how-do-i-make-the-first-letter-of-a-string-uppercase-in-javascript const makeErrorsIfNotCapitalized = (l) => handleInvalidStrings( l, capitalized, capitalize, (s, fixedS) => s + " is not capitalized, adjusting to " + fixedS); const makeErrorsIfTooLong = (l) => handleInvalidStrings( l, (s) => s.length <= 10, (s) => s.substring(0,10), (s,fixedS) => s + " is too long, cutting to " + fixedS); // Validation code const fixupAndAddErrors = (l) => { const r = makeErrorsIfNotCapitalized(l); const r1 = makeErrorsIfTooLong(r.strings); return {strings: r1.strings, errors: r.errors.concat(r1.errors)}; }; const logErrors = (l) => l.errors.forEach(e => console.log(e)); const getCapitalizedStrings = (l) => l.strings; // Examples logErrors(fixupAndAddErrors(myStrings)); console.log("-----"); console.log(getCapitalizedStrings(fixupAndAddErrors(myStrings)));
This just represents an input refinement that we may need for our programs: in the example code we want to signal invalid input strings and fix them to adhere to our requirements (i.e., strings that are capitalized and shorter than ten characters).
Now that the stage is set, let's discover the abstractions that we are using (without knowing).
There is a Functor in every Kleisli
In the code example we took in consideration, the main functionality
is given by fixupAndAddErrors
. This function is interesting as it
composes two effects: collecting capitalization and size errors.
One thing we may find interesting about the composition is that it
partially depends on the previous effect: look at how r1
depends on
the fixed strings returned in r
, and at how the final errors are the
composition of all the errors found so far.
... const r = makeErrorsIfNotCapitalized(l); const r1 = makeErrorsIfTooLong(r.strings); // composition of fixed strings return {strings: r1.strings, errors: r.errors.concat(r1.errors)}; // composition of errors ...
It turns out that customized composition of effects is a recurring
theme in functional programming. For this reason somebody put a lot of
effort in generalizing this behavior. The interface that implements
this behavior is called "fish". The name comes from the symbol chosen
for it in the Haskell language: >=>
, indeed, looks a bit like a
fish.
In Haskell the type signature of this operator is:
(>=>) :: (a -> m b) -> (b -> m c) -> a -> m c
Where in our example we could redefine the variables as:
(>=>) :: (list string -> m string) -> (list string -> m string) -> list string -> m string
The m
really represents our JavaScript object that contains both
strings and errors.
So, can we claim that fixupAndAddErrors
is a "fish" operator in
disguise?
Well, "fish" represents composition for the Kleisli functional
abstraction. This is the less known sibling of the Monad abstraction,
which is rather famous. The nice feature of Kleisli is that "fish"
expresses composition in a clearer way than Monad's "bind" 1. The other interface for Kleisli represents identity
and is called "return". This packages a basic value in a Kleisli
object. Kleisli comes with some laws. If fixupAndAddErrors
satisfies
these laws, we are really "fishing"!
First let's define "return":
const ret = (l) => ({strings: l, errors: []});
This function generates an object without errors out of a list of string.
Then let's redefine fixupAndAddErrors
and call it fish
before we present
the laws:
const fish = (f,g) => (l) => { const r = f(l); const r1 = g(r.strings); return {strings: r1.strings, errors: r.errors.concat(r1.errors)}; };
So what happened here? We want to compose multiple validators in a
general way. So first we have remove the validators details we had in
fixupAndAddErrors
, and second we have to inject these validators as
functions f
and g
. This way we can compose any validator function
as we please. Note: we are showing one of many implementations of
the fish
interface because our fish
will only work for validators
that can work on the {strings: [], errors: []}
objects.
Now let's define and check the laws for our brand new fish
:
const ret = (l) => ({strings: l, errors: []}); const fish = (f,g) => (l) => { const r = f(l); const r1 = g(r.strings); return {strings: r1.strings, errors: r.errors.concat(r1.errors)}; }; const equals = (x,y) => (JSON.stringify(x) === JSON.stringify(y)); var jsc = require("jsverify"); const fish_law1 = jsc.forall( "string -> { strings: array string; errors: array string }", "string -> { strings: array string; errors: array string }", "string -> { strings: array string; errors: array string }", "array string", (f,g,h,x) => equals( fish(fish(f,g), h)(x), fish(f, fish(g,h))(x) )) const fish_law2 = jsc.forall( "string -> { strings: array string; errors: array string }", "array string", (f,x) => equals( fish(ret,f)(x), f(x) )) const fish_law3 = jsc.forall( "string -> { strings: array string; errors: array string }", "array string", (f,x) => equals( fish(f,ret)(x), f(x) )) console.log("First fish law satisfied?", jsc.check(fish_law1)); console.log("Second fish law satisfied?", jsc.check(fish_law2)); console.log("Third fish law satisfied?", jsc.check(fish_law3));
Cool! According to jsverify
we are "fishing"!
But where is the Functor? Kleisli itself is a Functor specialized to
deal with effects. So let's define fmap
in terms of our fish
:
const ret = (l) => ({strings: l, errors: []}); const fish = (f,g) => (l) => { const r = f(l); const r1 = g(r.strings); return {strings: r1.strings, errors: r.errors.concat(r1.errors)}; }; const id = (x) => x const fmap = (f) => fish(id, (x) => ret(f(x))) console.log(fmap(ss => ["Before the fmap we had " + ss.length + " strings here"])({strings: ["a", "b"], errors: []}))
We can define fmap
by composing the identity function with the
composition of return and the fmap
transformation.
Naturally we have one last thing to do before feeling satisfied: let's
check that the Functor laws hold on this Kleisli flavoured fmap
!
var jsc = require("jsverify"); const equals = (x,y) => (JSON.stringify(x) === JSON.stringify(y)); const ret = (l) => ({strings: l, errors: []}); const compose = (fn1,fn2) => ( (arg) => fn2(fn1(arg))) const fish = (f,g) => (l) => { const r = f(l); const r1 = g(r.strings); return {strings: r1.strings, errors: r.errors.concat(r1.errors)}; }; const id = (x) => x const fmap = (f) => fish(id, (x) => ret(f(x))) const fmap_law1 = jsc.forall( "{strings: array string; errors: array string}", (x) => equals( fmap(id)(x), id(x)) ); const fmap_law2 = jsc.forall( "array string -> integer", "integer -> string", "{strings: array string; errors: array string}", (f,g,x) => equals( fmap(compose(f,g))(x), compose(fmap(f),fmap(g))(x) )); console.log("First functor law satisfied?", jsc.check(fmap_law1)); console.log("Second functor law satisfied?", jsc.check(fmap_law2));
And Functor is!
How easy it is to add a new validator with Kleisli
Let's look at our example before:
// Input const myStrings = ["Some", "cat", "superVeryLong"]; // Utils var capitalized = (s) => s.charAt(0) === s.charAt(0).toUpperCase(); var capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1); // https://stackoverflow.com/questions/1026069/how-do-i-make-the-first-letter-of-a-string-uppercase-in-javascript const makeErrorsIfNotCapitalized = (l) => l.reduce((a, s) => { if (capitalized(s)) return {strings: a.strings.concat(s), errors: a.errors}; return {strings: a.strings.concat(capitalize(s)), errors: a.errors.concat(s + " is not capitalized, adjusting to " + capitalize(s))};}, {strings: [], errors: []}) const makeErrorsIfTooLong = (l) => l.reduce((a, s) => { if (s.length <= 10) return {strings: a.strings.concat(s), errors: a.errors}; return {strings: a.strings.concat(s.substring(0,10)), errors: a.errors.concat(s + " is too long, cutting to " + s.substring(0,10))};}, {strings: [], errors: []}) // const flatten = ss => ss.flatMap(x => x); // Validation code const fixupAndAddErrors = (l) => { const r = makeErrorsIfNotCapitalized(l); const r1 = makeErrorsIfTooLong(r.strings); return {strings: r1.strings, errors: r.errors.concat(r1.errors)}; }; const logErrors = (l) => l.errors.forEach(e => console.log(e)); const getCapitalizedStrings = (l) => l.strings; // Examples logErrors(fixupAndAddErrors(myStrings)); console.log("-----"); console.log(getCapitalizedStrings(fixupAndAddErrors(myStrings)));
Now let's use Kleisli composition!
const ret = (l) => ({strings: l, errors: []}); const fish = (f,g) => (l) => { const r = f(l); const r1 = g(r.strings); return {strings: r1.strings, errors: r.errors.concat(r1.errors)}; }; // Input const myStrings = ["Some", "cat", "superVeryLong"]; // Utils var capitalized = (s) => s.charAt(0) === s.charAt(0).toUpperCase(); var capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1); // https://stackoverflow.com/questions/1026069/how-do-i-make-the-first-letter-of-a-string-uppercase-in-javascript const makeErrorsIfNotCapitalized = (l) => l.reduce((a, s) => { if (capitalized(s)) return {strings: a.strings.concat(s), errors: a.errors}; return {strings: a.strings.concat(capitalize(s)), errors: a.errors.concat(s + " is not capitalized, adjusting to " + capitalize(s))};}, {strings: [], errors: []}) const makeErrorsIfTooLong = (l) => l.reduce((a, s) => { if (s.length <= 10) return {strings: a.strings.concat(s), errors: a.errors}; return {strings: a.strings.concat(s.substring(0,10)), errors: a.errors.concat(s + " is too long, cutting to " + s.substring(0,10))};}, {strings: [], errors: []}) // const flatten = ss => ss.flatMap(x => x); // Validation code const fixupAndAddErrors = fish(makeErrorsIfNotCapitalized, makeErrorsIfTooLong); const logErrors = (l) => l.errors.forEach(e => console.log(e)); const getCapitalizedStrings = (l) => l.strings; // Examples logErrors(fixupAndAddErrors(myStrings)); console.log("-----"); console.log(getCapitalizedStrings(fixupAndAddErrors(myStrings)));
The change is minimal: we introduce the Kleisli operators and we use fish composition as follows
* /tmp/withKleisli.js 2019-05-11 17:46:00.995917150 +0200 --- /tmp/withoutKleisli.js 2019-05-11 17:45:59.386930470 +0200 ***** * 30,36 ** // const flatten = ss => ss.flatMap(x => x); // Validation code ! const fixupAndAddErrors = fish(makeErrorsIfNotCapitalized, makeErrorsIfTooLong); const logErrors = (l) => l.errors.forEach(e => console.log(e)); const getCapitalizedStrings = (l) => l.strings; --- 21,32 ---- // const flatten = ss => ss.flatMap(x => x); // Validation code ! ! const fixupAndAddErrors = (l) => { ! const r = makeErrorsIfNotCapitalized(l); ! const r1 = makeErrorsIfTooLong(r.strings); ! return {strings: r1.strings, errors: r.errors.concat(r1.errors)}; ! }; const logErrors = (l) => l.errors.forEach(e => console.log(e)); const getCapitalizedStrings = (l) => l.strings;
This change makes it easier to extend the code with another validator:
const ret = (l) => ({strings: l, errors: []}); const fish = (f,g) => (l) => { const r = f(l); const r1 = g(r.strings); return {strings: r1.strings, errors: r.errors.concat(r1.errors)}; }; // Input const myStrings = ["Some", "cat", "superVeryLong", "me"]; // Utils var capitalized = (s) => s.charAt(0) === s.charAt(0).toUpperCase(); var capitalize = (s) => s.charAt(0).toUpperCase() + s.slice(1); // https://stackoverflow.com/questions/1026069/how-do-i-make-the-first-letter-of-a-string-uppercase-in-javascript const makeErrorsIfNotCapitalized = (l) => l.reduce((a, s) => { if (capitalized(s)) return {strings: a.strings.concat(s), errors: a.errors}; return {strings: a.strings.concat(capitalize(s)), errors: a.errors.concat(s + " is not capitalized, adjusting to " + capitalize(s))};}, {strings: [], errors: []}) const makeErrorsIfTooLong = (l) => l.reduce((a, s) => { if (s.length <= 10) return {strings: a.strings.concat(s), errors: a.errors}; return {strings: a.strings.concat(s.substring(0,10)), errors: a.errors.concat(s + " is too long, cutting to " + s.substring(0,10))};}, {strings: [], errors: []}) const makeErrorsIfTooShort = (l) => { const prefix = "short-"; return l.reduce((a, s) => { if (s.length >= 3) return {strings: a.strings.concat(s), errors: a.errors}; return {strings: a.strings.concat(prefix + s), errors: a.errors.concat(s + " is too short, adding prefix resulting in " + prefix + s)};}, {strings: [], errors: []}) } // const flatten = ss => ss.flatMap(x => x); // Validation code const fixupAndAddErrors = fish(fish(makeErrorsIfNotCapitalized, makeErrorsIfTooLong), makeErrorsIfTooShort); const logErrors = (l) => l.errors.forEach(e => console.log(e)); const getCapitalizedStrings = (l) => l.strings; // Examples logErrors(fixupAndAddErrors(myStrings)); console.log("-----"); console.log(getCapitalizedStrings(fixupAndAddErrors(myStrings)));
Essentially we just need to compose a new validator with "fish": easy!
What Kleisli laws represent
Let us take a brief moment to get the intuition of what the Kleisli laws really mean.
You may have noticed that we have only two laws for Functors:
- a law that checks that
fmap
in combination with the identity function does not change the values - a law that checks that
fmap
does not break the contract of composition (i.e., composing two functionsa -> b
andb -> c
is equivalent to a new functiona -> c
)
Well the Kleisli (and Monad) laws aim to enforce the exact same
properties. They are just a bit weird because the operators we are
dealing with are not as simple as fmap
.
Indeed, two laws out of the three we have seen deal with identity:
const fish_law2 = jsc.forall( "string -> { strings: array string; errors: array string }", "array string", (f,x) => equals( fish(ret,f)(x), f(x) )) const fish_law3 = jsc.forall( "string -> { strings: array string; errors: array string }", "array string", (f,x) => equals( fish(f,ret)(x), f(x) ))
Here we are really saying that combining the ret
and fish
operator
produces a composition of the f
and the identity function.
And the other law focuses indeed on composition:
const fish_law1 = jsc.forall( "string -> { strings: array string; errors: array string }", "string -> { strings: array string; errors: array string }", "string -> { strings: array string; errors: array string }", "array string", (f,g,h,x) => equals( fish(fish(f,g), h)(x), fish(f, fish(g,h))(x) ))
You can see that the property is focusing on how functions compose
through the fish
operator. We really just want to compose f
, g
,
and h
, the complexity of the law comes from the fact that they carry
an effect with them, so we have to wrap them in the fish
call.
Conclusion
In summary we saw a functional abstraction useful to handle sequential
effects (like logging and error handling). We saw how Kleisli is a
specialization of a Functor: we defined fmap
through Kleisli's
fish
operator and checked it respects Functor laws. We have also
seen how to recognize a Kleisli by defining and checking some new
laws. After looking at specialization we shall see generalization! In
next post we will speak of Profunctors.
Happy coding!
Footnotes:
also known as flatMap