Functional Architecture Patterns
Functional Architecture Patterns
By Brian Lonsdorf on FrontendMasters @drboolean
The paradox is that architecture starts to break down when the app grows big enough, but in a one day course, it's hard to go that far.
Architecture is subjectiver, can have different goals:, modular, extendable, performance, maintenance, readabality etc. You must chose your north star.
Concept of grouping and naming procedures and grouping them into clases or modules. Same for data. This leads to lot of names.
Domain Driven Design by Eric Evans. A great book.
It's very hard to come up with good names, this book gives good strategy for it.
You need to know the domain when you start coding. And a lot of times you come with names like converter
or processors
.
05:04 - Functional Architecture Patterns on Livestream Let's take a step back. Procedures. We're going to compose the procedures. But you need to laws so that you can compose with confidence. To have confindence in idempondence, side effects, order of execution.
06:56 - Functional Architecture Patterns on Livestream Shows some laws, associative, communicatevie, identity, distributive
06:56 - Functional Architecture Patterns on Livestream Shows some laws, associative, communicatevie, identity, distributive.
Functions with defined contracts
You map the contracts to mathematical concepts.
Shows example of User
class.
07:52 - Functional Architecture Patterns on Livestream
But now, to get the full name, we create joinwithSpace
that has all the laws, e.g. associativity so you can combine it in different groups and it will give the same result.
08:24 - Functional Architecture Patterns on Livestream
so joinwithSpace
is a reusable utility. He introduces the concept of joinable. Any type that has .join
, e.g. an array. Now joinwithSpace
joinwithSpace = joinable => joinable.join(' ')
joinwithSpace([user.firstName, user.lastName])
will program to the interface joinable. This is the same thing as encapsulation. joinable can only do one thing and doesn’t know anything else, unlike the OO user
object.
10:18 - Functional Architecture Patterns on Livestream
The identity
as abstraction, it can only do one thing.
does in runjs
const {Either} = require('types')
const identity = a => a
Either.of(2).fold(identity, identity)
This is how he gets the value out. So he doesn't need to write a handler to get the value out. first it's a bit weird to see. So identity is used in a lot of places in the functors and monads.
13:09 - Functional Architecture Patterns on Livestream
Highly generalized functions - the guiding principle, to go down and abstract out to the smallest possible piece.
13:41 - Functional Architecture Patterns on Livestream Composition slide Benefits:
- infinite use cases
- simple easy to understand pices
- reuse
Drawbacks:
- harder to change the implementation
- harder for user to compose
Abstract algebra: you have several pieces, but they all obey some laws so you can combine them with confidence (so solves the drawbacks)
15:37 - Functional Architecture Patterns on Livestream
The other side where you have just a black box. Flexibility in implementation, but less use cases to support.
And sooner or later you will be passing in flags or ifelse
statements, because you don't (can't) satisfy all the use cases.
So the conditions get pushed into the "box".
We will focus on compostions.
17:17 - Functional Architecture Patterns on Livestream
Definition of group slide. (Screenshot) This is how our documentation could look like.
favor composable functions, mostly
mostly!
18:48 - Functional Architecture Patterns on Livestream Shapes slide cicrles, triangles, stars... You model each with monads, eg. task, maybe, either, functors do compose monads don't compose, so we will focus on normalising the shapes (shows circle interviened) 19:35 - Functional Architecture Patterns on Livestream
Normalize effect types throughout the app
guiding principle.
Monoids
20:56 - Functional Architecture Patterns on Livestream
Will build a validation library.
Defining a semigroup
1 + 2 + 6
=> associativity, yu can group the additions as you want
closed : doesn't change the type, will still return a number.
2 * 5 * 8
=> again associativity, and closed
22:57 - Functional Architecture Patterns on Livestream
but 10 / 4 / 2
is not associative or closed.
by the way closed and associative = parallel
more rules
true && true && false
again associative
or set intersection
[1, 2, 3] ^ [2, 5, 6]
(made up operator) : this is associative as well.
same for disjunction or union.
again closed means we don't change the type, integer stays integer etc.
Semigroups are everywhere.
We can do max
and min
the same way, or comparisons too are associative and closed.
So we will define this as a data type.
const Sum = x => ({
x,
concat: other =>
Sum(x + other.x)
})
const res = Sum(3).concat(Sum(5)) // Sum(8)
we're making an interface that has a .concat
and satisfies the above laws (associativity and closed).
You can do the same with Product
, concat: other=> Product(x * other.x)
Shows the same for Any
. Any(x || other.x)
We lift these value into a type, so we can program against an interface.
String has built in .concat
.
Monoids
30:22 - Functional Architecture Patterns on Livestream
Now will explain how it works as monoids.
A monoid is a semigroup with an identity
// Monoid = Semigroup + Identity
We call it .empty
.
For Product
it would be multiplying by 1. Just an identity function.
So you can do Product.empty().concat(Product(10))
Sum.empty = () => Sum(0)
So why the empty
matters? It's a starting value. Can be used with reduce.
32:43 - Functional Architecture Patterns on Livestream
Shows the array reduce.
const res = [1,2,3,4,5].map(Sum).reduce((acc, n) => acc.concat(n))
So even if we don't have a data process, the empty
makes sure that we can still give back a value and not blow up.
35:32 - Functional Architecture Patterns on Livestream
creates the monoid for Any
.
concat: other => Any(x || other.x)
Any.empty = () => Any(false)
The All
monoid:
All.empty = () => All(true)
(because concat: other => All(x && other.x)
38:05 - Functional Architecture Patterns on Livestream
foldMap
introduction
const res = [true, true].map(All).reduce((acc, n) => acc.concat(n), All.empty());
// we can simplify this syntax with foldMap:
const { List } = require('immutable-ext') // it has foldMap on the list
const res = List([true, true]).foldMap(All, All.empty())
console.log(res.toJS()) // toJS comes from the immutable library imported by immutable-ext
// actually just run
console.log(res)
A counter example of semigroups that cannot be promoted to monoids.
Intersection
const Intersection = x => ({
x,
concat: other => Intersection(_.intersection(x, other.x))
})
But what would be the empty, starting value? Cannot find one... An empty list would never intersect. Or you would need a list of every possible everything...
So Intersection
is a semigroup but not a monoid. But Union
can be promoted.
const res = List([true, true, false]).foldMap(All, All.empty())
// res: true
42:17 - Functional Architecture Patterns on Livestream
Functors
He sometimes uses interchangebly monoids and semigroups because the most important is .concat
.
an example with functors:
const {Id, Task, Either} = require('../lib/types')
const {Left, Right} = Either
// copies the previously created monoids here...
const id = x => x
// you can do things like:
Id.of(Sum(2)).concat(Id.of(Sum(3))) // Id(Sum(5))
console.log(res.fold(id, id)) // { x: 5, concat: [Function: concat]}
So Id
is a monoid if what it holds is a monoid. Whatever it's holding.
You always have to program to the interface (not just simple operations, but using the interface of the types)
We can stack these as much as we want:
const res = Id.of(Right(Task.of(Sum(2)))).concat(Id.of(Sum(3)))
And this will cascade with whatever it's holding.
As long as we are concating with the same shape, it works!
More examples of concating functors together:
const res = Right('hello').concat(Right('world')) // Right('hello world')
res.fold(console.log, console.log) // hello world
// Left always short circuits :
const res = Right('hello').concat(Left('world')) // Left('world')
res.fold(console.log, console.log) // world
48:41 - Functional Architecture Patterns on Livestream
// Task will run these in parallel
const res = Task.of('hello').concat(Task.of('world'))
res.fold(console.log, console.log) // hello world
const res = Task.of(['hello']).concat(Task.of(['world']))
res.fold(console.log, console.log) // ['hello', 'world']
// Rejected is like Left, only gives the failed branch
const res = Task.of(['hello']).concat(Task.rejected(['world']))
res.fold(console.log, console.log) // 'world'
Same behaviour as Left. Or like Promise.all. Stops if we have a single failure. But later will show an example of being even more flexible in handling failures.
Promise.all is like a traversable we can flip the "Task" holding a list to a List of Tasks (or vice versa).
Here, we are folding down, and combining them all, but more powerful as we can combine them in several ways.
The closest thing to a silver bullet as you can get (talking about monoids)
51:19 - Functional Architecture Patterns on Livestream
tryCatch(() => readFileSync()) // Right || Left
// so instead of concat giving the left like this:
Right('hi').concat(Left('bye')) // Left('bye')
// there's a construct called Alternative that captures choice
// but we can also do it with monoids by wrapping the above:
const Alternative = ex => ({ // ex meaning holding an Either of x
ex,
concat: other =>
Alternative(other.ex.isLeft ? ex : ex.concat(other.ex))
})
// reminder: never fall out of the type, when concating so wrap
// the result in Alternative
//
// in the above implementation, we can make choices and instead of
// `ex.concat(other.ex)` we could just keep `other.ex`, we can decide
// but here, to keep the intuition of monoids we concat the two
const res = Alternative(Right('hi').concat(Alternative(Left('bye'))))
console.log(res.ex.fold(id, id)) // hi
// another test
const res = Alternative(Right('hi'))
.concat(Alternative(Right('!!!!!')))
.concat(Alternative(Left('bye')))
console.log(res.ex.fold(id, id)) // hi!!!!
with Alternative you can decide how to branch your code and and only concat the inside if it's a Right.
Will rewrite the next as a foldMap to simplify a bit, using List:
const {List} = require('immutable-ext')
const res = List([Right('hi'), Left('ab'), Right('!')])
.foldMap(Alternative, Alternative(Right(''))) //second argument is the initial value, here a Right of empty string
console.log(res.ex.fold(id, id)) // hi!
58:38 - Functional Architecture Patterns on Livestream
You can use Class
syntax if you want to make monoids. You will be making tem more often than functors.
CodePen:
CodePen example and exercises 1:00:51 - Functional Architecture Patterns on Livestream exercises time starting
back from the break: 1:16:19 - Functional Architecture Patterns on Livestream Going through the exercises. Using his own library to add foldmap to immutable Map.
First exercise
// Ex1: reimplement sum using foldMap and the Sum Monoid
// =========================
var sum = xs => List(xs).reduce((acc, x) => acc + x, 0)
// solution:
var sum = xs => List(xs).foldMap(Sum, Sum.empty())
QUnit.test("Ex1: sum", assert => {
assert.equal(String(sum([1,2,3])), "Sum(6)")
})
Q: How does this work exactly ?
Let's rewrite foldMap:
const foldMap = (t, empty, xs) => xs.reduce((acc, x) => acc.concat(t(x)) ,empty )
// or, we could *first* `map` it to the type, and then reduce it
const foldMap = (t, empty, xs) => xs.map(t).reduce((acc, x) => acc.concat(x) ,empty )
1:20:13 - Functional Architecture Patterns on Livestream
Now, there's also the fold
function:
We could have it like this:
var sum = xs => List(xs).map(Sum).fold(Sum.empty())
Now, fold
and map
are not specialised to lists! You can do it on trees, event streams, many different data structures. Even with Either
you can .fold.map
out of the either.
foldMap
is very useful, it comes from the foldable interface. It's one of the most useful methods in functional programming.
Pointed functors and the solution above
Immutable.js has pointed functors, so you can do List.of(2)
.
but List.of([1,2,3]) // List([1,2,3])
would actually put the array into the List
.
Second exercise
These are just "toy" exercises, to get the feeling for foldMap. The types cascade, so you can combine effects, branches and intense calculations with foldMap.
// Ex2: reimplement lessThanZero using foldMap and the Any Monoid
// =========================
// original
var anyLessThanZero = xs =>
List(xs).reduce((acc, x) => acc < 0 ? true : false, false)
// solution
var anyLessThanZero = xs =>
List(xs).reduce((acc, x) => acc.concat(x < 0 ? Any(true) : Any(false)), Any.empty())
// NOTE, that above we take a number (0) and convert it to a boolean, because Any takes a boolean
// or another solution from the audience
var anyLessThanZero = xs =>
List(xs).foldMap(x => Any(x < 0), Any.empty())
// or another solution from the audience
var anyLessThanZero = xs =>
List(xs).map(x => x < 0).foldMap(Any, Any.empty())
// or
var anyLessThanZero = xs =>
List(xs).map(x => Any(x < 0)).fold(Any.empty())
QUnit.test("Ex2: anyLessThanZero", assert => {
assert.equal(String(anyLessThanZero([-2, 0, 4])), "Any(true)")
assert.equal(String(anyLessThanZero([2, 0, 4])), "Any(false)")
})
Next exercise
// Ex3: Rewrite the reduce with a Max monoid (see Sum/Product/Any templates above)
// =========================
var max = xs =>
List(xs).reduce((acc, x) => acc > x ? acc : x, -Infinity)
// solution
// making the Max monoid
const Max = x =>
({
x: x,
concat: other => Max(x > other.x ? x : other.x),
toString: () => `Max(${x})`
})
Max.empty = () => Max(-Infinity)
var max = xs =>
List(xs).foldMap(Max, Max.empty()) // test passing!!
QUnit.test("Ex3: max", assert => {
assert.equal(String(max([-2, 0, 4])), "Max(4)")
assert.equal(String(max([12, 0, 4])), "Max(12)")
})
Next exercise
1:28:31 - Functional Architecture Patterns on Livestream
// Ex4 (Bonus): Write concat for Tuple
// =========================
const Tuple = (_1, _2) => ({
_1,
_2,
concat: o => undefined, // write me
});
// write it
const Tuple = (_1, _2) => ({
_1,
_2,
concat: o =>
Tuple(_1.concat(o._1), _2.concat(o._2))
});
QUnit.test('Ex4: tuple', assert => {
const t1 = Tuple(Sum(1), Product(2));
const t2 = Tuple(Sum(5), Product(2));
const t3 = t1.concat(t2);
assert.equal(String(t3._1), 'Sum(6)');
assert.equal(String(t3._2), 'Product(4)');
});
1:30:16 - Functional Architecture Patterns on Livestream
More examples
const getAppAplerts = () => fetch('/alerts').then(x => x.json())
const getDirectMessages = () => fetch('/dm').then(x => x.json())
getAppAplerts().concat(getDirectMessages())
// Promise([{id:1, msg: 'Policy update'}, {id:2, msg: 'hi from spain'}])
In a pure functional setting you would be using Task and not Promise... It's associative and closed, so for example can be done parallel.
Another example
const getPost = () => fetch('/post')
.then(x => x.json())
.then(Map)
const getComments = () => fetch('/comments')
.then(x => x.json())
.then(comments => Map({comments}))
getPost().concat(getComments())
// Promise(Map({id:3, body: 'Redux is over', comments: []}))
Map is a way to define a semigroup on an object. If there's a merge conflict on a key it will just concat the merge conflict.
1:32:03 - Functional Architecture Patterns on Livestream
another example
try/catch is not very pure.
const tryCatch = fn => args => {
try {
return Right(fn(args))
} catch(e) {
return Left(e)
}
}
const readFile = tryCatch(fs.readFileSync)
const filepaths = ['one.txt', 'two.txt', 'three.txt']
filepaths.foldMap(readFile, Right(''))
// Right('Everything Brian tells you is a lie dont listen to him')
If any of the file reads would blow up, we would just get the Left
.
Same thing but async
const readFile = promisify(fs.readFile)
const filepaths = ['one.txt', 'two.txt', 'three.txt']
filepaths.foldMap(readFile, Promise.resolve(''))
// Promise('Everything Brian tells you is a lie dont listen to him')
now it's asynchronous.
const getWords = str => str.match(/\w+/g)
filepaths.foldMap(path => readFile(path).then(getWords), Promise.resolve([]))
// Promise(['I', 'survived', 'off', 'of', 'the', 'painted', 'easter', 'eggs'])
an example of doing null checks
const nullCheck = x =>
x != null ? Right(x) : Left('got null')
const getWords = str => nullCheck(str.match(/\w+/g))
filepaths.foldMap(path => readFile(path).then(getWords), Promise.resolve(Right([])))
// Promise(Right(['I', 'survived', 'off', 'of', 'the', 'painted', 'easter', 'eggs']))
// but, if there's a null:
// Promise(Left('got null'))
Another example
const filepaths = ['db.json', 'host.json']
const readCfg = path => tryCatch(() => Map(require(path)))
filepaths.foldMap(readCfg, Right(Map()))
// Right(Map({hostnameY 'localhost', port: 8888, dbName: 'testDb'}))
Tree concat example
const Report = el =>
Map({
elementcount: Sum(1),
classes: Set(el.classList),
tags: Set(el.tagName),
maxHeight: Max(el.height)
})
Report.empty = () =>
Map({
elementcount: Sum.empty(),
classes: Set(),
tags: Set(),
maxHeight: Max.empty()
})
const getReport = root =>
Tree(root, x => Array.from(x.children)).foldMap(Report, Report.empty())
// Tree is a functor/foldable monoid
getReport(document.body)
//Map({
// elementcount: Sum(1292),
// classes: I.Set(({'slds-button', ...})),
// tags: I.Set({'a', 'button', ...}),
// maxHeight: Max(592)
//});
Map will combine and do automatic conflict resolution
Each element combine differently, but foldMap
can still concat them because of they all obey the same rules.
And to fold a list of documents:
docs.foldMap(doc => getReport(doc.body), Report.empty())
//Map({
// elementcount: Sum(21292),
// classes: I.Set(({'slds-button', ...})),
// tags: I.Set({'a', 'button', ...}),
// maxHeight: Max(831)
//});
1:35:53 - Functional Architecture Patterns on Livestream
Some rules
x.concat(y.concat(z)) == x.concat(y).concat(z)
x.concat(M.empty()) == M.empty().concat(x) == x
associativity, identity rules
Homomorphisms
f(M.empty()) == M.empty()
f(x.concat(y)) == f(x).concat(f(y))
We can take a monoid, and do a type transformation to another monoid, and the properties will still hold.
[].length == 0
xs.concat(ys).length == xs.length + ys.length
MyMessage message;
message.ParseFromString(str1 + str2);
MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);
so we can parse one message and another one and merge them or, we can combine the messages and parse them.
1:36:44 - Functional Architecture Patterns on Livestream
Cayley's Theorem
const xs = ['a', 'b', 'c'].map(x => Endo(y => x.concat(y)))
// [Endo(y => 'a'.concat(y)), Endo(y => 'b'.concat(y))]
xs.map(x => x.run(''))
// ['a', 'b', 'c']
Monads
Monads are monoids in the category of endofunctors
Because .chain
is a monoidal operation.
1:37:10 - Functional Architecture Patterns on Livestream
Making a validation library
Validations are things that combine.
isPresent(obj.name).concat(isEmail(obj.email))
We should get the combined errors or the success.
First, let's the API we want to end up with:
const validations = {
name: isPresent,
email: isEmail.concat(isPresent)
}
validate(validations, obj)
Writing the validate
function.
const validations = { name: isPresent, email: isPresent }
const obj = {name: 'brian', email: 'brian@brian.com'}
const res = validate(validations, obj)
console.log('V',res)
So first, the validate
function:
const isPresent = x => !!x
const validate = (spec, obj) =>
List(Object.keys(spec)).foldMap(
key => {
spec[key](obj[key]) ? Right(First(obj)) : Left(`${key} bad`)
},
Either.of(First(obj))
)
It should work, because we're returning an Either
and it does concat
.
In a second update, we added the First
wrapping above, but it would not actually collect all the errors, it would short circuit on the first error.
Or, if we wrap all in an array, that does concat
const validate = (spec, obj) =>
List(Object.keys(spec)).foldMap(
key => {
spec[key](obj[key]) ? Right([obj]) : Left([`${key} bad`])
},
Either.of([obj])
)
res.fold(console.log, console.log)
With this implementation, we still only get one error, and all the others are ignored.
Let's make the a success and failure types to collect all the validation results. These two will be a "subclass" of some other validation type, just like Right and Left and Either. So it's closed it will always return either a success or failure, just like the Either sub types. You always get back an Either, but it can be a Right or a Left.
const Success = x => ({
isFail: false,
x,
fold: (f,g) => g(x), // fold out, run the success case
concat: other =>
other.isFail ? other : Success(x) // so we collect all the failures
})
const Fail= x => ({
isFail: true,
fold: (f,g) => f(x), // get out of the type, run the failure case
x,
concat: other =>
other.isFail ? Fail(x.concat(other.x)) : Fail(x) // This is like the opposite of Either, the failures are concating
})
// and to use them:
const validate = (spec, obj) =>
List(Object.keys(spec)).foldMap(
key => {
spec[key](obj[key]) ? Success([obj]) : Fail([`${key} bad`])
},
Success([obj])
)
res.fold(console.error, console.log)
1:55:58 - Functional Architecture Patterns on Livestream
This is the basics of a pretty solid validation library.
improving the validation library
Instead of concating the results, we can concat the entire validation itself.
we could wrap it with Validation so we have a Validation holding the function and now we can combine these
const isPresent = Validation(x => !!x)
Or, demand the isPresent
returns a Success
or Fail
const isPresent = Validation(x => !!x ? Success(x): Fail(['needs to be present']))
But we're missing the key
for the validation message. So refactor one more time, accepting the key
const isPresent = Validation((key, x) =>
!!x
? Success(x)
: Fail([`${key} needs to be present`]))
Notice, the array in Fail
. Because we want to concat all the Fails and Array does it automatically (free monad).
Creating the generic Validation monoid
First, we update the validate
function, to use Validation
which will have a run
method.
const validate = (spec, obj) =>
List(Object.keys(spec)).foldMap(
key => {
// 1. we add .run here
// spec[key].run(obj[key]) ? Success([obj]) : Fail([`${key} bad`])
// 2. and can remove all the rest finally
spec[key].run(key, obj[key])
},
Success([obj])
)
// now we can concat the different Validations
const validations = {name: isPresent, email: isPresent.concat(isEmail)}
Writing the isEmail
validaton:
const isEmail = Validation((key, x) =>
!!/@/.test(x)
? Success(x)
: Fail([`${key} must be an email`]))
and finally the Validation
monoid:
const Validation = run => ({
run,
concat: other =>
Validation((key, x) => run(key, x).concat(other.run(key, x))) // we're concating running our validation with running the other validation with the same key and x
})
Now everything is working well! It lists all the possible validation errors or just the object.
2:01:59 - Functional Architecture Patterns on Livestream
More validation possibilities
Doing an Or
combination:
const validations = {name: isPresent, email: Or(isPresent).concat(Or(isEmail))}
And
and Or
form a semiring, they interact and distribute between each other.
The Or
would mean that you should give an email, but it's OK if it fails.
Validation is finished now. Exports it to be used later in the course.
Function modeling
2:03:25 - Functional Architecture Patterns on Livestream
Actually we already did function modeling with validation (in the Validation
funtion.
Instead of modeling a data and a type of data, we were modeling a function.
We provided a run
argument and we would run it.
const Validation = run => ({
run,
concat: other =>
Validation((key, x) => run(key, x).concat(other.run(key, x)))
})
Let's see more examples of function modeling:
const toUpper = x => x.toUpperCase()
const exclaim = x => x.concat('!')
// we want to combine them...
// run the two functions and concat their results
// but also return the same type
const Fn = run => ({
run,
// 2. let's add `map` to make if a functor
map: f => Fn(x => f(run(x))),
concat: other =>
Fn(x => run(x).concat(other.run(x)))
})
So functions are like functors, two "boxes" and we can combine them.
concat
can "open up" the box and concat what's inside.
const res = Fn(toUpper).concat(Fn(exclaim)).run('fp sux')
console.log(res) // FP SUXfp sux!
So above, both functions run, they returned a String that has .concat
and concated them.
And we can even map
over the result
const res = Fn(toUpper).concat(Fn(exclaim)).map(x => x.slice(3)).run('fp sux')
console.log(res) // SUXfp sux!
functions as monads
Monads are about nesting
Fn(x => Fn(y => x, y))
// we will flatten these two with monads
let's write a function monad, continuing from above
const Fn = run => ({
run,
// add the chain method
// we need to return a function on the "outside"
chain: f => Fn(x => f(run(x)).run(x)),
map: f => Fn(x => f(run(x))),
concat: other =>
Fn(x => run(x).concat(other.run(x)))
})
// example:
// now, we have another argumetn `y` below!
const res = Fn(toUpper).chain(upper => Fn(y => exclaim(upper))).run('hi')
console.log(res) // HI!
So what's interesting is that in the .chain
above, f(run(x))
run with the 'hi'
value, but then the .run(x)
on it again run with the 'hi'
, this is why it uppercased and exclaimed it. (we didn't use the y
variable).
If we would do, we would get
const res = Fn(toUpper).chain(upper => Fn(x => exclaim(x))).run('hi')
console.log(res) // hi!
or, we could get both vars:
const res = Fn(toUpper).chain(upper => Fn(x => [upper, exclaim(x)])).run('hi')
console.log(res) // ['HI', 'hi!']
This is why ha said it was reader because x
was carried through and we could get access to it any time.
As we start transforming our input, we can still get back to the original value.
The Reader Monad
Fn.of = x => Fn(() => x)
const res = Fn.of('hello')
.map(toUpper)
.chain(upper => Fn(x => [upper, exclaim(x)]))
.run('hi')
console.log(res) // ['HELLO', 'hi!']
We got two arguments!
2:14:04 - Functional Architecture Patterns on Livestream
So we can now have a pattern, where the second argument is provided later when running the app:
Fn.of = x => Fn(() => x)
const res = Fn.of('hello')
.map(toUpper)
.chain(upper => Fn(config => [upper, exclaim(config)]))
console.log(res.run({port:3000})) // ['HELLO', {port; 3000}]
add an .ask
method
Fn.ask = Fn(x => x)
const res = Fn.of('hello')
.map(toUpper)
.chain(upper =>
Fn.ask.map(config => [upper, config]))
console.log(res.run({port:3000}))
So this is the reader monad, and is extremely useful in functional architectures.
Some libraries like zeo or neo have started working on this more in detail. You can do dependency incjection.
console.log(res.run({db, strategy})) // just pass in your config
More functions we can model
State is exactly like this (won't present it). we can thread a state through our program and modify it, it's the exact same pattern. The difference is that you can modify the original state you pass into it. He doesn't find it as useful.
What if we make a function composition as a way of concating values?
For this we need a type Endo
// this is where we want to arrive:
[toUpper, exclaim].foldMap(Endo, Endo.empty().run('hello')) // HELLO!
It's called endo beause it only works with endomorphism ie where the type doesn't change between input and output:
a -> a
String -> String
Task -> Task
This guarantees that we can compose the functions because the types won't change.
Create the Endo function
Based on the Fn
above.
since we have endomorphism, we cannot have a map
or chain
method.
so it cannot be a functor.
const Endo = run => ({
run,
concat: other =>
Endo(x => run(other.run(x))) // "we run the other one and then run mine with the result"
})
// 2. Endo.empty definition (empty doesn't take arguments)
Endo.empty = () => Endo(x => x)
const res = List([toUpper, exclaim])
.foldMap(Endo, Endo.empty())
.run('hello')
console.log(res)
Contravariant functors
Always return the same type
a -> String
Can do it with sort
functions, predicate
functions...
Predicate functions: a -> Bool
(always return a boolean)
So we can always map over the argument.
// (acc, a) -> acc
// so `acc` is fixed, cannot map over its type
// but we can change a -> b
const Reducer = run => ({
run,
contramap: f =>
// instead of writing map that maps over the output, we map over the **input**
Reducer((acc, x) => run(acc, f(x))),
// so with `f(x)` we transform our value _before_ it got to the run function
concat: other =>
Reducer((acc, x) => other.run(run(acc, x), x))
})
Reducer(login).concat(Reducer(changePage)).run({user: {}, env: {...}}) // so it's a kind of payload (in the `run`)
// so we can contramap:
Reducer(login.contramap(payload => payload.user))
.concat(Reducer(changePage).contramap(payload => payload.currentPage))
.run(state, {user: {}, currentPage: {...}})
We just combined two reducers, both of which have been contramapped. So the each take one input but transform them to what they are looking for.
So one use case is to keep the entire payload, but plucking parts off for the different inner functions (like above).
We're combining different functions, they take different inputs, but I only run run
once.
It's almost like a before
hook, while .map
is more like an after
hook.
When you want to combine functions, you may want to manipulate stuff before they get there...
2:27:45 - Functional Architecture Patterns on Livestream so it's really when you are combining functions into one, and they just receive one function. Also when you want to pre-compose instead of post-compose when you are building your application and you have a fixed output and variable input.
Function modeling exercise
3:33:55 - Functional Architecture Patterns on Livestream
The proper Endo function!!
// Definitions
const Endo = run => ({
run,
concat: other => Endo(x => other.run(run(x)))
});
Endo.empty = () => Endo(x => x);
We have a bunch functions that work with strings and we can turn them into Endos.
// Ex1:
// =========================
const classToClassName = html => html.replace(/class\=/gi, 'className=');
const updateStyleTag = html => html.replace(/style="(.*)"/gi, 'style={{$1}}');
const htmlFor = html => html.replace(/for=/gi, 'htmlFor=');
const ex1 = html => htmlFor(updateStyleTag(classToClassName(html))); //rewrite using Endo
// ***** Solution ******
const ex1 = html =>
Endo(htmlFor)
.concat(Endo(updateStyleTag))
.concat(Endo(classToClassName))
.run(html)
// So we are just capturing function compositions via monoids
// ***** Solution 2 ******
// put them in a List
const ex1 = html =>
List.of(htmlFor, updateStyleTag, classToClassName)
.foldMap(Endo, Endo.empty())
.run(html)
// or just a plain array and reduce
const ex1 = html =>
[htmlFor, updateStyleTag, classToClassName]
.reduce((acc, x) => acc.concat(Endo(x)), Endo.empty())
.run(html)
// but foldMap is a common pattern that you will see
// often in other languages
QUnit.test('Ex1', assert => {
const template = `
<div class="awesome" style="border: 1px solid red">
<label for="name">Enter your name: </label>
<input type="text" id="name" />
</div>
`;
const expected = `
<div className="awesome" style={{border: 1px solid red}}>
<label htmlFor="name">Enter your name: </label>
<input type="text" id="name" />
</div>
`;
assert.deepEqual(expected, ex1(template));
});
Also note that the other of functions passed into the list does not matter.
Predictes
We will do contramap.
3:37:41 - Functional Architecture Patterns on Livestream
// Ex2: model a predicate function :: a -> Bool and give it contramap() and concat(). i.e. make the test work
// =========================
const Pred = undefined; // todo
const Pred = run => ({
run,
// we cannot have a .map, since it always has
// to return a bool
contramap: f =>
Prod(x => run(f(x))),
concat: other =>
Pred(x => run(x) && other.run(x)) // both return true|false so we can do && which is equivalent of concat for Bool
})
QUnit.test('Ex2: pred', assert => {
const p = Pred(x => x > 4)
.contramap(x => x.length)
.concat(Pred(x => x.startsWith('s')));
const result = ['scary', 'sally', 'sipped', 'the', 'soup'].filter(p.run);
assert.deepEqual(result, ['scary', 'sally', 'sipped']);
});
Again a high level explanation of contramap
Take the following predicates:
// takes a number:
const greaterThanFour = Pred(x => x >4)
// takes a string:
const sStart = Pred(x => x.startsWith('s'))
So one returns a string, the other a number, and to check the length, I don't want to transform the original value, but I can do this transformation from string to number just before I call the predicate. I add a hook to be able to change the value before I pass to a function.
Contravarian functor when you have a .contramap
functor.
If you have a .contramap
and also .map
it's called a profunctor.
There's a more detailed graph on the fantasy-land GitHub repo /figures/dependencies.png
Last exercise
Builds on the previous one, add matchesAny
predicate.
// Ex3:
// =========================
const extension = file => file.name.split('.')[1];
const matchesAny = regex => str => str.match(new RegExp(regex, 'ig'));
const matchesAnyP = pattern => Pred(matchesAny(pattern)); // Pred(str => Bool)
// TODO: rewrite using matchesAnyP. Take advantage of contramap and concat
const ex3 = file =>
matchesAny('txt|md')(extension(file)) &&
matchesAny('functional')(file.contents);
// ***** Solution *****
const ext3 = file =>
matchesAnyP('txt|md').contramap(extension)
.concat(matchesAnyP('functional').contramap(f => f.contents))
.run(file)
QUnit.test('Ex3', assert => {
const files = [
{ name: 'blah.dll', contents: '2|38lx8d7ap1,3rjasd8uwenDzvlxcvkc' },
{
name: 'intro.txt',
contents: 'Welcome to the functional programming class'
},
{ name: 'lesson.md', contents: 'We will learn about monoids!' },
{
name: 'outro.txt',
contents:
'Functional programming is a passing fad which you can safely ignore'
}
];
assert.deepEqual([files[1], files[3]], files.filter(ex3));
});
The implications of this architecture
On most applications you plug together different systems and you need to constantly transform the types to match the expected inputs. With monoids you don't need adapters, you only have one piece and combine these.
3:47:09 - Functional Architecture Patterns on Livestream So above we are normalising our types into Predicates and then it's easy to combine them.
Getting back to the previous error with Endo
From before, this was not the proper implementation of Endo:
const Endo = run => ({
run,
concat: other =>
Endo(x => run(other.run(x)))
})
Endo.empty = () => Endo(x => x)
const res = List([toUpper, exclaim])
// .foldMap(Endo, Endo.empty())
// he was tryng to much to pass the empty string:
.foldMap(Endo, Endo.empty(''))
.run('hello')
console.log(res)
He was trying to add an empty string to Endo.empty('')
BUT, Endo gets its arguments later, when you run .run
And the definition of Endo.empty
is the first value you pass to it (on .run
)
3:48:25 - Functional Architecture Patterns on Livestream
Reducer and redux architecture
const Reducer = run => ({
run,
contramap: f =>
Reducer((acc, x) => run(acc, f(x))),
concat: other =>
Reducer((acc, x) => other.run(run(acc, x), x)) // this returns a new accumulator, which gets passed to the next one(`other`)
})
const checkCreds = (email, pass) =>
email === 'admin' && pass === 123
const login = (state, payload) =>
payload.email
? Object.assign({}, state, {loggedIn: checkCreds(payload.email, payload.pass)})
: state // "we always have to return an accumulator"
const setPrefs = (state, payload) =>
payload.prefs
? Object.assign({}, state, {prefs: payload.prefs})
: state
// we put both of them in a Reducer, so we can combine them
const reducer = Reducer(login).concat(Reducer(setPrefs))
const state = {loggedIn: false, prefs: {}}
const payload = {email: 'admin', pass: 123, prefs: {bgcolor: '#000'}}
console.log(reducer.run(state, payload))
Now, we can play around with the types and this is why it's great that we can model functions.
// (acc, a) -> acc
// now, play around with the laws, each line is
// equivalent to the previous one:
// (a, acc) -> acc // (we can flip around the functions)
// a -> acc -> acc (isomorphism), we can take one argument at a time
// a -> (acc -> acc) // this is Endo !!
// a -> Endo(acc -> acc)
// Fn(a -> Endo(acc -> acc))
const Reducer = run => ({
run,
contramap: f =>
Reducer((acc, x) => run(acc, f(x))),
concat: other =>
Reducer((acc, x) => other.run(run(acc, x), x)) // this returns a new accumulator, which gets passed to the next one(`other`)
})
3:53:15 - Functional Architecture Patterns on Livestream so now we can do:
// 1.
// const reducer = Reducer(login).concat(Reducer(setPrefs))
const reducer = Fn(login).concat(Fn(setPrefs))
// 2. then flip and curry the arguments for login
// const login = (state, payload) =>
const login = payload => Endo(state =>
payload.email
? Object.assign({}, state, {loggedIn: checkCreds(payload.email, payload.pass)})
: state
)
continue the modeling:
const reducer = Fn(login).map(Endo).concat(Fn(setPrefs))
// and now you run it like this:
console.log(reducer.run(payload).run(state))
So now we separated the two, payload
and state
.
Or, you can do it like this, unwrapping the Endo
in login and mapping it in reducer
:
const login = payload => state =>
payload.email
? Object.assign({}, state, {loggedIn: checkCreds(payload.email, payload.pass)})
: state
// add the .map in the end:
const reducer = Fn(login).map(Endo).concat(Fn(setPrefs).map(Endo))
3:55:31 - Functional Architecture Patterns on Livestream
We can even factor state
out.
Transformers
Monad transformers and functor composition
const Compose = (F, G) => {
const M = fg => ({
extract: () => fg,
map: f => M(fg.map(g => g.map(f)))
})
M.of = x => M(F.of(G.of(x)))
return M
}
So Compose
returns M
and if we want to put it in M
(M.of
), we put in G, then F.
So how does this work?
const TaskEither = Compose(Task, Either) // Task in the outside, either in the inside
TaskEither.of(2)
.map(two => two * 10)
.map(twenty => twenty + 1)
// it's a Compose Task of Either, so need to extract it
// to get out the value
.extract()
// so now it's a Task holding an either
.fork(console.error, either =>
either.fold(console.log, console.log)
)
so notice that above, I only had to do .map
not several.
3:59:06 - Functional Architecture Patterns on Livestream
on the extract
Extract is similar to fold
but doesn not take the function, it just gets out the value
So functors compose:
// Id is also a functor...
const TaskEither = Compose(Id, Compose(Task, Either))
4:00:52 - Functional Architecture Patterns on Livestream
If I chain
however, I would need to create another TaskEither
to do it.
So you cannot always, mechanically compose to monads.
TaskEither.of(2)
.chain(...) // ???
.map(twenty => twenty + 1)
.extract()
.fork(console.error, either =>
either.fold(console.log, console.log)
)
We cannot add .chain
to our Compose
.
We can combine Task and Either into a monoid.
But typically you want to be able to .map
and .chain
to be able to compose their insides.
So the above code was a demonstration that funtors compose but monads do not.
We will define a monad transformer.
A demo where a transformer is useful
const _ = require('lodash')
const users = [{id: 1, name: 'Brian'}, {id: 2, name: 'Marc'}, {id: 3, name: 'Odette'}]
const following = [{user_id: 1, follow_id: 3}, {user_id: 1, follow_id: 2}, {user_id: 2, follow_id: 2}]
// so here we have the Either inside a Task
const find = (table, query) =>
Task.of(Either.fromNullable(_.find(table,query)))
// we want to find the followers of user_id:1
const app = () =>
find(users, {id: 1}) // Task(Either(User))
.chain(eu => // eu : EitherUser
// since we call .chain, we have to return another Task
eu.fold(Task.rejected, u => find(following, {follow_id: u.id}))
).chain(eu =>
eu.fold(Task.rejected, fo => find(users, {id: fo.user_id}))
)
.fork(console.error, eu =>
eu.fold(console.error, console.log)
)
app()
So this is a demonstration that the code becomes very complex with all the .chain
s and folds to look up values in the linked table.
Solution with monad transformers
// the require was there from the beginning:
const { TaskT, Task, Either } = require('../lib/types')
// make a Task transformer
const TaskEither = TaskT(Either)
// TaskT knows how to .chain.chain
// each Monad should have it's own transformer,
// eg. EitherT, IdT
// because each monad can only know how to deal
// with its own effects
// to recover Task, just pass the Id functor
const Task = Task(Id)
// (it's just an interesting property we can exploit)
// so now to refactor:
//const find = (table, query) =>
// Task.of(Either.fromNullable(_.find(table,query)))
const find = (table, query) =>
TaskEither.lift(Either.fromNullable(_.find(table,query)))
// note, that above, we're using .lift so
// that we don't end up Task(Either(Either(x))) wrapped Eithers
// so .lift is like .of but doesn't duplicate the inner type
const app = () =>
find(users, {id: 1})
// and now refactoring
.chain(u => find(following, {follow_id: u.id})) // Task(Either(User))
.chain(f => find(users, {id; fo.user_id})) // Task(Either(User))
// so still fork the either etc etc
.fork(console.error, eu =>
eu.fold(console.error, console.log)
)
4:11:14 - Functional Architecture Patterns on Livestream
This is a typical "stack" when you are working in functional programming in many apps:
const { FnT, TaskT, Task, Either, EitherT } = require('../lib/types')
const FnTask = FnT(Task)
const App = EitherT(FnTask)
// App :: Either(Fn(Task))
const res = App.of(2).map(x => x + 1) // App(3)
// but at this point App is holding an Either, holding a Function holding a Task
res.fold(console.error, fn =>
fn.run({myEnv: true})
.fork(console.error, console.log)
)
continues..., this works as well:
// const res = App.of(2).map(x => x + 1) // App(3)
const res = App.of(2).chain(x => App.of(x + 1))
res.fold(console.error, fn =>
fn.run({myEnv: true})
.fork(console.error, console.log)
)
continues..., how to deal when we have an Either inside?
// const res = App.of(2).chain(x => App.of(x + 1))
const res = App.of(2).chain(x => Either.of(x + 1))
// Either is the outer type, so we need to put
// the types inside it:
const res = App.of(2).chain(x => Either.of(FnTask.of(x + 1)))
// but it's still not an App "type"
// need to "lift" it:
const res = App.of(2).chain(x => App.lift(Either.of(FnTask.of(x + 1))))
res.fold(console.error, fn =>
fn.run({myEnv: true})
.fork(console.error, console.log)
)
4:16:56 - Functional Architecture Patterns on Livestream At the above point Brian got lost in the code so let's simplify it:
const FnTask = FnT(Task)
// const App = EitherT(FnTask)
const App = FnT(FnTask)
// now we have two "environments" playing at once
const TaskEither = TaskT(Either)
TaskEither
.of(2)
.chain(two => TaskEither.lift(Either.of(two + two)))
.fork(console.error, x => x.fold(console.log, console.log))
Let's break it down one more level:
const EitherId = EitherT(Id)
const TaskEither = TaskT(EitherId)
TaskEither
.of(2)
// .chain(two => TaskEither.lift(Either.of(two + two)))
.chain(two => TaskEither.lift(EitherId.of(two + two)))
.fork(console.error, x =>
x.fold(console.log, y => console.log(y.extract()))
)
Actually the above code does not run, and Brian didn't manage to get it working quickly, but the point anyway is that this is not a good pattern.
4:20:57 - Functional Architecture Patterns on Livestream
Free monads
When we want to model a function that has state, environment and async, and error handling -> you get into situations like above, you have to bee good in lifting types out.
4:39:41 - Functional Architecture Patterns on Livestream Coming back to the previous exercises. The bug was coming from his types "definitions" file.
const TaskEither = TaskT(Either)
const App = FnT(TaskEither)
App
.of(2)
.chain(two => App.lift(TaskEither.of(two + two)))
.chain(four => App.lift(TaskEither.lift(Either.of(four))))
.chain(four => App.lift(Task.of(four).map(Either.of)))
.run({})
.fork(console.log, fi => fi.fold(console.log, console.log))
now play with the above:
App
.of(2)
.map(two => two * 2) // .map is simple
// but chain is more complex, we have to think about
// what our function is going to return
// for example .chain(doDbThing) -> we have to think about
// how to get out the types
// you have to manually put the shapes together
// for example in the next line we have .lift into our App
.chain(two => App.lift(TaskEither.of(two + two)))
// here we have Either.of(four) so need to lift that to
// TaskEither
.chain(four => App.lift(TaskEither.lift(Either.of(four))))
// here, we're returning a Task and neet do .map that...
.chain(four => App.lift(Task.of(four).map(Either.of)))
.run({})
.fork(console.log, fi => fi.fold(console.log, console.log))
Exercises
// Ex1:
// =========================
const FnEither = FnT(Either)
const {Right, Left} = Either
// TODO: Use FnEither.ask to get the cfg and return the port
const ex1 = () =>{}
const ex1 = () =>
FnEither.ask.map(config => config.port)
// "we just pulled the config out thin air"
// (meaning it was provided in the .run() below)
QUnit.test("Ex1", assert => {
const result = ex1(1).run({port: 8080}).fold(x => x, x => x)
assert.deepEqual(8080, result)
})
// Ex1a:
// =========================
const fakeDb = xs => ({find: (id) => Either.fromNullable(xs[id])})
const connectDb = port =>
port === 8080 ? Right(fakeDb(['red', 'green', 'blue'])) : Left('failed to connect')
// TODO: Use ex1 to get the port, connect to the db, and find the id
const ex1a = id =>{}
const ex1a = id =>
// ex1 is FnEither holding a port
ex1().chain(port =>
FnEither.lift(connectDb(port)) // lift, because connectDb is also an Either
)
.chain(db => FnEither.lift(db.find(id)))
QUnit.test("Ex1a", assert => {
assert.deepEqual('green', ex1a(1).run({port: 8080}).fold(x => x, x => x))
assert.deepEqual('failed to connect', ex1a(1).run({port: 8081}).fold(x => x, x => x))
})
Brian repeats that monad transformers are pretty painful (especially in JavaScript).
// Ex2:
// =========================
const posts = [{id: 1, title: 'Get some Fp'}, {id: 2, title: 'Learn to architect it'}, {id: 3}]
const postUrl = (server, id) => [server, id].join('/')
const fetch = url => url.match(/serverA/ig) ? Task.of({data: JSON.stringify(posts)}) : Task.rejected(`Unknown server ${url}`)
const ReaderTask = FnT(Task)
// Use ReaderTask.ask to get the server for the postUrl
const ex2 = id =>
fetch(postUrl(server, id)).map(x => x.data).map(JSON.parse) // <--- get the server variable from ReaderTask
const ex2 = id =>
ReaderTask.ask.chain(server =>
ReaderTask.lift(fetch(postUrl(server, id)).map(x => x.data).map(JSON.parse))
)
QUnit.test("Ex2", assert => {
ex2(30)
.run('http://serverA.com')
.fork(
e => console.error(e),
posts => assert.deepEqual('Get some Fp', posts[0].title)
)
})
Next exercise
4:48:01 - Functional Architecture Patterns on Livestream
// Ex3:
// =========================
const TaskEither = TaskT(Either)
const Api1 = ({
getFavoriteId: user_id =>
Task((rej, res) =>
res(user_id === 1 ? Right(2) : Left(null))
),
getPost: post_id =>
Task((rej, res) =>
res(Either.fromNullable(posts[post_id-1]))
)
})
const Api2 = ({
getFavoriteId: user_id =>
TaskEither((rej, res) =>
res(user_id === 1 ? Right(2) : Left(null))
),
getPost: post_id =>
TaskEither((rej, res) =>
res(Either.fromNullable(posts[post_id-1]))
)
})
// TODO: Rewrite ex3 using Api2
const ex3 = (user_id) =>
Api1.getFavoriteId(user_id)
.chain(epost_id =>
epost_id
.fold(
() => Task.of(Left()),
(post_id) => Api1.getPost(post_id)
)
)
.map(epost =>
epost.map(post => post.title)
)
const ex3 = (user_id) =>
Api1.getFavoriteId(user_id)
.chain(post_id => Api2.getPost(post_id))
.map(post => post.title)
So Api2 is **much** nicer
QUnit.test("Ex3", assert => {
ex3(1)
.fork(
e => console.error(e),
ename =>
ename.fold(
error => assert.deepEqual('fail', error),
name => assert.deepEqual('Learn to architect it', name)
)
)
})
Free monads
4:49:16 - Functional Architecture Patterns on Livestream
Usually they are not what you want, but they do solve a very sepecific problem.
(Reader or Dependency injection via .ask
)
The free monad is a way to take your functions and treat them as data types.
An example of using free
const { liftF } = require('../lib/free')
const { id } = require('../lib/types')
// for example:
httpGet = url => Task(..)
// instead we can return a data type
httpGet = url => HttpGet(url)
// and if this is a monad
// we can chain it as if it already did
// and just get the contents
HttpGet(url)
.chain(contents = HttpPost('/analytics', contents))
// you end up with a data structure representing the nested tree of computation
// you are creating an AST
// you are lifting your functions into data types
// lifting them as if they were running and interpret then later
// so we need to create an interpreter
// to interpret the above data structure (HttpGet.(...))
const { taggedSum } = require('daggy') // highly recommends this library
// it gives a shorter syntax to create the
// semigroup-like syntax we've been using
// HttpGet = x => ({x,...})
const interpret = () =>
More on daggy Also gives some functions to decompose several data types.
// taggedSum is like a type definition
const Http = taggedSum('Http', {Get: ['url'], Post: ['url', 'body']})
We defined two data types, Get
and Post
console.log(Http.Get('/home')) // { url: '/home' }
So it's just a shorter syntax for the object literals from before.
But it also gives a .cata
catamorphism :
But it also gives a .cata
catamorphism, aka pattern matching, and destructure the types and do actions on them:
Get('/home').cata({
Get: url => 'get',
Post: (url, body) => 'post'
})
4:54:02 - Functional Architecture Patterns on Livestream
Catamorphism helps you to simplify the checks, you don't have to write the if else
statements to check if the data has certain property to know it's type.
With daggy, we made our function with .Get
and .Post
into a data type.
And now we can add liftF
to lift it into "free".
4:54:39 - Functional Architecture Patterns on Livestream
"free": free monad, just lifts the data into a type. Like the array []
, it's a free monoid.
//httpGet = url => liftF(url)
httpGet = url => liftF(Http.Get(url)) // Free(HttpGet)
httpPost = (url, body) => liftF(Http.Post(url, body)) // Free(HttpPost)
// now run the app
const app = () =>
httpGet('/home') // we can .chain, (free monad above)
.chain(contents => httpPost('./analytics', contents))
const res = app()
console.log(res)
// {x: {url: '/home'}, f: [Function]}
// `f` is our continuation function
Free monads are just a data type, but they normalise everything so we can .chain
and .map
etc.
Now, let's pass this to an interpreter
Sidenote, he uses the Id
when he's architecting a solution and when everything is in order swaps it out for a task.
// Free(Stuff).foldMap() // and fold it to some other type, this is the interpreter
const interpret = x =>
x.cata({
Get: url => Id.of(`contents for ${url}`), // need to return a monad, Id
Post: (url, body) => Id.of(`posted ${body} to ${url}`)
})
const res = app().foldMap(interpret, Id.of)
console.log(res.extract())
// 'posted contents for /home to /analytics'
we've written our app with data, and in the end we decided which interpreter to use.
is catamorphism here a "fancy" switch or if else statement?
Yes.
Also daggy has some error checking and useful error messages when running the interpret
er.
So this is the free monad. When it's hard to separate the logic from the side-effects (his example when they were doing an app that posted to npm, GitHub and all) How to test it, mock the api calls, and daggy and cata was a great solution for that.
Lenses
5:02:01 - Functional Architecture Patterns on Livestream
Lenses are both very simple and can be a very complex, deep subject. They are also very flexible. Lenses are built on functors and just like functors you can do anything with a combination of lenses and functors. You could rewrite every app by lenses.
In other languages (mentions Haskell) they more nuanced and so even more complex topic.
The lenses library from Ramda is pretty naive, there's a more hardcore lenses library called optica.
Here we will just focus on the top level lens concepts.
Here's the common pattern he uses, where he namespaces his lenses with L
:
const {toUpper, view, over, lensProp, compose} = require('ramda')
const L = {
name: lensProp('name'),
street: lensProp('street'),
address: lensProp('address')
}
const user = {address: {street: {name: 'Maple'}}}
// to view a property
// lenses compose!
const res = view(compose(L.address, L.street, L.name), user) // again, lenses compose!
console.log(res) // Maple
We could jump into this deeply nested structure via lenses composition.
We can modify via over
const addrStreetName = compose(L.address, L.street, L.name)
const res = over(addrStreetName, toUpper, user)
console.log(res) // {address: {street: {name: 'MAPLE'}}}
Again, we could modify a deeply nested object, and get the whole thing back and also immutable. This can compose with monads:
const user = Task.of({address: {street: {name: Either.of('Maple')}}})
const addrStreetName = compose(L.address, L.street, L.name, L.mapped) // note the addition of L.mapped
const res = over(addrStreetName, toUpper, user)
console.log(res) // {address: {street: {name: 'MAPLE'}}}
This is very powerful. You can go into a deeply nested element, inside a monad, change a part and continue. Most people use profunctor lenses. Brian has a lens library on his GitHub.
Again, L.mapped
is the "magic" that allows to interact with the monads.
It's like treating properties as functors.
Lens also work with lists, lensProp(0)
.
5:09:21 - Functional Architecture Patterns on Livestream Lenses preserve the structure of your data.
Lenses compose backwards, left to right (?) (see above the compose(...)
)
Building an app
A CLI blog
5:24:32 - Functional Architecture Patterns on Livestream Graph on the screen. Map of interactions. There's start from there you can either go to create author or straight to menu. From the menu we can see all authors, latest articles or write. The items have arrows pointing to the next item.
Will create the main architecture in a functional way.
The starting setup:
const readline = require('readline').createInterface({input: process.stdin, output: process.stdout})
const getInput = q =>
Task((rej, res)) => readline.question(q, i => res(i.trim()))
// it would work like this:
getInput('sup?').map(answer => answer.toUpperCase())
.fork(console.error, console.log)
// sup? dunno => DUNNO
Starts to stetch out the function and see their types. First, just to see what we may need.
const {save, all, find} = reqire('../lib/db') // just some array based database, it wraps it in a Task
const AuthorTable = 'Authors'
const start = () =>
// check if we have any authors
all(AuthorTable) // this is a Task
.map(authors => authors.length ? menu : createAuthor) // menu and createAuthor are not yet implemented
We go these few functions and then see what we need to refactor later... implemeting createAuthor and continueing
// the Author type, see below
const Author = name => ({name})
const createAuthor = () =>
getInput('Name? ')
.map(name => Author(name)) // created an Author type
.chain(author => save(AuthorTable, author)) // saving to the database, this is a Task
.map(() => menu) // we're not handling the errors yet, later we can add our validation lib
He likes to create these data types, so that your data is defined and not just creating them ad hoc everywhere in your app.
Let's write menu
Note that we're just passing the continuation along
// () -> Task () -> Task () ->
// some recoursive task
// but we can turn it into any monad:
// () -> m () -> m () ->
// and we take the unit:
// a -> m a -> m a ->
// we can regroup:
// a -> m (a -> m (a -> m))
// then, there's a type called Fix:
// Fix f -> f (Fix f) // f is some functor, e.g. Task
// Fix Task -> Task (Fix Task)
// Fix is a promise that we will fix a "fix" point of this functor
// that we will keep recusing down until we hit a point
// but finally he will rule out Fix, because it must hold a traversable
// but Fix has a "close relative", called Free
// Free m -> Task (Free m) // monad
// it’s a travesible type
// Free m -> m (Free m) | Pure
// so the point is that we have a type that is compatible with the Free monad
const menu () =>
getInput('where do you want to go today? (createAuthor, write, latest, all)')
.map(route => router[route]) // we will need a router...
// () -> Task Task Task Task
// a recursive data structure of a Task holding a Task
const createAuthor = () =>
...
// also create the router
const router = {menu, createAuthor} // will add more later
now add the runner
const runApp = () =>
f().fork(console.error, runApp) // we have a loop
// the way we want to run it
runApp(start)
// Name?
5:39:18 - Functional Architecture Patterns on Livestream The "app" is now working. You can give a name when asked and move to the next step. Let's continue:
const AuthorTable = 'Authors'
// **** 3. add the posttable table :
const PostTable = 'Post'
const Author = name => ({name})
const Post = title,body => ({title,body})
const menu () =>
getInput('where do you want to go today? (createAuthor, write, latest, all)')
.map(route => router[route]) // we will need a router...
const createAuthor = () =>
...
// **** 5. add post formatting :
// will move this elsewhere later
// it doesn't really belong here
// will copy/paste (cut/paste) later it's easy
const formatPost = post =>
`${post.title}:\n ${post.body}`
// **** 5. then print :
const print = s => Task((_rej, res) => res(console.log(s)))
// **** 4. add the latest listing :
const latest = () =>
all(PostTable)
.map(posts => last(posts)) // `last` is from Ramda
.map(formatPost)
.chain(print)
.map(() => menu) // go back to menu when print is done
// **** 2. create write :
const write = () =>
getInput('Title: ')
.chain(title => // then ask for the body
getInput('Body: ')
.map(body => Post(title, body)) // put it into a data type (don't create new ones on the fly!)
)
.chain(post => save(PostTable, post))
.map(() => latest)
// **** 1. update route :
const router = {menu, createAuthor, write, latest}
const runApp = () =>
f().fork(console.error, runApp)
runApp(start)
Now it works, can tell name, write post, get the latest.
How do you test this?
These are all just tasks and writing to stdout (that's not so great).
Free monads were a great solution for this app, we could treat the interactions as data structures and interpret them with whatever interpreter we want. This could be a test interpreter or a live console print.
Free monads solution
5:47:37 - Functional Architecture Patterns on Livestream
So far, everything in our app is Task.
It would be better if we had specific actions, like httpGet
before. And we would have data types that represent actions.
// **** 1. bring in lift
const { liftF } = reqire('../lib/free') // lets turn "things" into free monads
// **** 2. add daggy
const {taggedSum} = reqire('daggy')
// **** 3. create a type to read/write to console
// this will be interpreted in different ways
// the "names" (`['q']` below), don't really matter, only for inspection
// at work he would write the names all out...
const Console = taggedSum('Console', {Question: ['q'], Print:['s']})
const Db = taggedSum('Db', {Save: ['table', 'record'], All:['table', 'query']})
// **** 11. use question instead of getInput
const menu () =>
// getInput('where do you want to go today? (createAuthor, write, latest, all)')
question('where do you want to go today? (createAuthor, write, latest, all)')
.map(route => router[route]) // we will need a router...
// **** 4. change the print, question method
const print = s => liftF(Console.Print(s))
const question = s => liftF(Console.Question(s))
// will interpret these (above lines) with getInput etc
const realPrint = s => Task((_rej, res) => res(console.log(s)))
const writeOutput = s => Task((_rej, res) => res(console.log(s)))
// **** 5. now the the same for db and
const dbAll = (table, query) => liftF(Db.All(table, query))
const dbSave = (table, record) => liftF(Db.Save(table, record))
// so there's a lot of repetition, but it's OK...
// **** 6. and update write
const write = () =>
question('Title: ')
.chain(title =>
question('Body :')
.map(body => Post(title, body))
)
.chain(post => dbSave(PostTable, post))
const createAuthor = () =>
question('Name? ')
.map(name => Author(name))
// update
//.chain(author => save(AuthorTable, author))
.chain(author => dbSave(AuthorTable, author))
.map(() => menu)
const start = () =>
// all(AuthorTable)
dbAll(AuthorTable)
.map(authors => authors.length ? menu : createAuthor)
// **** 9. add dbToTask
const dbToTask = x =>
x.cata({ Save: save, All: all, })
// save is holding the same arguments
// the above is equivalent to this:
x.cata({ Save: (table, record) => save(table, record), All: all, })
// save(table, record) returns us a Task
// **** 10.
const consoleToTask = x =>
x.cata({ Question: getInput, Print: writeOutput, })
// what we do is mapping our data types holding our arguments
// into functions
// **** 8. write the interpreter
// composable interpreters (with coproducts)
// for now, he only has two effects so won't do it
const interpreter = x =>
// we don't know if it's a console or a db, we check:
// would be easier in a typed language or with a different solution
x.table ? dbToTask(x) : consoleToTask(x)
// **** 7. we're now using a free monad
const runApp = f => // now, `f` returns a free monad and not a task
// we have to interpret it
// f().fork(console.error, runApp)
f()
.foldMap(interpret, Task.of)
.fork(console.error, runApp)
Free monads are working.
6:00:40 - Functional Architecture Patterns on Livestream
We can add interpretTest
// **** 2. add the functions
// we're returing an Id that is logging out
const consoleToId = x =>
x.cata({
Question: q => Id.of(`answer to ${q}`),
Print: s => Id.of(`writing the string of ${s}`), })
// **** 3.
const dbToId = x =>
x.cata({
Save: (table, r) => Id.of(`saving to ${r} ${table}`),
All: (table, q) => Id.of(`find all ${table} ${q}`),
})
const interpreter = x =>
x.table ? dbToTask(x) : consoleToTask(x)
// **** 1.
const interpretTest = x =>
x.table ? dbToId(x) : consoleToId(x)
const runApp = f =>
runApp(
f()
// **** 4. use interpretTest
.foldMap(interpretTest, Id.of).extract
)
6:05:22 - Functional Architecture Patterns on Livestream The above code does not work actually... He would mock out
const consoleToId = x =>
x.cata({
//Question: q => Id.of(`answer to ${q}`),
Question: q => Id.of(answers[q]), // mock answers
Print: s => Id.of(`writing the string of ${s}`), })
To stop a free monad
Note that in runApp
we are recoursively calling it.
const {liftF, Free, Pure} = require('../lib/free')
would use Pure
to stop it.
Talking about Fix
Free(F(Free(F)), Pure())
Building a habit "todont" app
A more typical front-end application. A "to do" type of list of things you should stop doing. Habits that you want to stop.
6:17:09 - Functional Architecture Patterns on Livestream
Webpack dev server, puts out the standard todo app template.
Uses React and a standard html sructure:
<body>
<div id="app"></div>
<script src="index.bundle.js"></script>
</body>
the ui.js
is a React app holding a few functional components.
The base:
const renderApp = (state, dispatch) => {
const app = <App state={state} dispatch={dispatch} />
ReactDOM.render(app, document.getElementbyId('app'))
}
export {renderApp}
The dispatch
function gets called with an event, similar to but not Redux.
It's called with an event name and a payload.
There's create
, view
, destroy
event names.
//..
<input
....
onKeyDown={e => e.key === 'Enter' ? dispatch('create', {name: e.currentTarget.value}): undefined}
/>
Starting "app":
import { renderApp } from './ui'
renderApp({page: 'list', habits: []}, (action, payload) => {})
6:18:59 - Functional Architecture Patterns on Livestream
We will be modeling the reduction process, just like before with the functions.
Again, stating app code:
const renderApp = (state, dispatch) => {
const app = <App state={state} dispatch={dispatch} />
ReactDOM.render(app, document.getElementbyId('app'))
}
export {renderApp}
import { renderApp } from './ui'
// **** 1.
const create = (state, habit) =>
Object.assign({}, state, {habits: [habit]})
const state = {page: 'list', habits: []}
// **** 3. create an "even loop" to start things off
// and wrap render in it
const appLoop = state =>
renderApp(state, (action, payload) => {
// **** 2. create new state and also call render app
return renderLoop(create(state, payload))
})
// **** 4. and start the app
appLoop({page: 'list', habits: []})
Now each action (typing, deleting, listing) have these payloads, inside appLoop
: 'create' {name: 'asdf'}
'destroy' {idx: 0}
, 'view' {idx: 0}
Now continues with a router
const renderApp = (state, dispatch) => {
const app = <App state={state} dispatch={dispatch} />
ReactDOM.render(app, document.getElementbyId('app'))
}
export {renderApp}
import { renderApp } from './ui'
import {over, lensProp, remove, append} from 'ramda'
// **** 5. create the Lenst namespace
const L = { habits: lensProp('habits') }
const create = (state, habit) =>
// **** 4. Append to the habit list, via Lenses!!
// Object.assign({}, state, {habits: [habit]})
over(L.habits, append(habit), state)
Object.assign({}, state, {habits: [habit]})
const state = {page: 'list', habits: []}
// **** 3. stub out the route functions
const destroy = (state, {idx}) =>
// **** 5. implement to remove only one habit
//Object.assign({}, state, {habits: []})
over(L.habits, remove(idx, 1), state)
const view = (state, {idx}) =>
Object.assign({}, state, {page: 'show', index: idx})
// **** 2. implement the router
const route = {create, destroy, view}
const appLoop = state =>
renderApp(state, (action, payload) => {
// **** 1. add the router
// return renderLoop(create(state, payload))
return appLoop(route[action](state, payload))
})
appLoop({page: 'list', habits: []})
Now the app is working pretty great. 6:26:39 - Functional Architecture Patterns on Livestream But!
Each time we need to deal with the state
and keep adding to it. Instead what if we could just return the state and it was merged for us?
Right now, it's up to the developer to always make sure to merge in the state with the current one (see Object.assign
above).
It would be nicer, if we could just return a monoid:
const view = (state, {idx}) =>
// Object.assign({}, state, {page: 'show', index: idx})
Merge({page: 'show', index: idx}) // Merge is a monoid!
// and also for the rest:
// though with the lenses it doesn't buy us much...
const create = (state, habit) =>
Merge(over(L.habits, append(habit), state))
const destroy = (state, {idx}) =>
Merge(over(L.habits, remove(idx, 1), state))
So let's implement the Merge
monoid
const Merge = x => ({
x,
concat: other =>
Merge(Object.assign({}, x, other.x))
})
// Merge({a:1}).concat(Merge({a: 2})) // -> we want Merge({a:2})
And update our main app loop:
const appLoop = state =>
renderApp(state, (action, payload) =>
// appLoop(route[action](state, payload)) // now these are returning us a Merge
appLoop(Merge(state).concat(route[action](state, payload)).x) // the `.x` takes it out from the two Merge
)
appLoop({page: 'list', habits: []})
6:30:44 - Functional Architecture Patterns on Livestream
It works, but...
you have to call Merge
everywhere. Let's factor this out.
const view = (state, {idx}) =>
// Merge({page: 'show', index: idx})
({page: 'show', index: idx})
const create = (state, habit) =>
// Merge(over(L.habits, append(habit), state))
over(L.habits, append(habit), state)
const destroy = (state, {idx}) =>
// Merge(over(L.habits, remove(idx, 1), state))
over(L.habits, remove(idx, 1), state)
// now we're just returning little bits of state
// and instead put them in the Merge in appLoop
const appLoop = state =>
renderApp(state, (action, payload) =>
// appLoop(Merge(state).concat(route[action](state, payload)).x)
appLoop(
Merge(state)
.concat(Merge(route[action](state, payload)))
.x
)
)
appLoop({page: 'list', habits: []})
This gives a very simple, clean action handlers, each just concered with a small part of the tree.
Next, optimising around the state
. Notice that we can flip the arguments around and also that in view state
is no longer used.
6:31:47 - Functional Architecture Patterns on Livestream
Instead we could ask
for the state:
// **** 3.
//const view = (state, {idx}) =>
// ({page: 'show', index: idx})
const view = ({idx}) =>
({page: 'show', index: idx})
const create = habit =>
// **** 1. "ask" for the state
ask.map(over(L.habits, append(habit)))
//const create = (state, habit) =>
// over(L.habits, append(habit), state)
// **** 2. simplify the others as well
//const destroy = (state, {idx}) =>
// over(L.habits, remove(idx, 1), state)
const destroy = ({idx}) =>
ask.map(over(L.habits, remove(idx, 1)))
const appLoop = state =>
renderApp(state, (action, payload) =>
appLoop(
Merge(state)
.concat(Merge(route[action](state, payload)))
.x
)
)
appLoop({page: 'list', habits: []})
Now create
is no longer a reducer, just a normal function.
implementing the ask
import {Fn} from '../lib/types'
const {ask} = Fn
// now, create, destroy, return the Fn type (ask.map...)
// do it for view:
const view = ({idx}) =>
Fn.of({page: 'show', index: idx})
// we can now just call our function with the payload
const appLoop = state =>
renderApp(state, (action, payload) =>
appLoop(
Merge(state)
//.concat(Merge(route[action](state, payload)))
.concat(Merge(route[action](payload).run(state))) // run it with state (.run(state)), and our function can choose to use state or not (`view`)
.x
)
)
appLoop({page: 'list', habits: []})
Now we're able to "pull state out of nowhere".
We could now pull out appLoop
into a separate module, apploog, pull create
, destroy
, view
into "controllers".
Move Merge
into a library.
We could separate all these out into separate files, but this is not the point here..
How to further improve our code?
Let's start by wrapping each our function with Fn
:
// **** 2. add setShowPage
const setShowPage = Fn(() => Fn.of({page: 'show'}))
// **** 3. rename `view` to `setIndex`
const setIndex = Fn(({idx}) =>
// **** 1. wrap it with Fn AND remove `page`
//({page: 'show', index: idx}) )
({index: idx}) )
const create = Fn(habit =>
ask.map(over(L.habits, append(habit))))
// wrap it with Fn
const destroy = Fn(({idx}) =>
ask.map(over(L.habits, remove(idx, 1))))
Now we can combine our functions:
const view = setIndex.concat(setShowPage)
And run it like so:
const appLoop = state =>
renderApp(state, (action, payload) =>
appLoop(
Merge(state)
//.concat(Merge(route[action](payload).run(state)))
.concat(Merge(route[action].run(payload).run(state)))
.x
)
)
So now view
just combines a lot of smaller pieces instead of being one huge function.
6:40:25 - Functional Architecture Patterns on Livestream
If destroy
would return a Task
:
const destroy = FnTask(({idx}) =>
ask.map(over(L.habits, remove(idx, 1)))
).chain(...)
or wrap with another function and ask for en environment
const destroy = Fn(({idx}) =>
ask(env =>
ask.map(over(L.habits, remove(idx, 1)))
)
)
///
concat(Merge(route[action].run(payload).run(state).run(env)))
That's a wrap. This course should have given a starting point to see the tools available and you can dig deeper if you need to...
Typescript
There are a few people who are pushing TypeScript into functional programming that make JavaScript feel like Scala. He will post their Twitter handles.
He was hesitating to start the class with TypeScript, it would help catch the bugs when you don't put the correct type in your functions.
But TypeScript doesn't like nested generics Promise<Either<Task<..>>>
. Here you need a lot of tricks.
He does recommend it, but if you plan to go very far with it (TypeScript) you may start to consider using a typed functional language in the first place (Elm, PureScript, Reason).
6:44:41 - Functional Architecture Patterns on Livestream End of video