Good afternoon!
I am thrilled to be here today.
My name is Cory and I am a senior web developer at Aumni.
why yes, we are hiring. thank you for asking
Monads are (in)famously said to be cursed.
This is has become something of a self fulfilling prophecy. Today, we are going to break that curse.
- Douglas Crockford
A monad may encapsulate values of a particular data type, creating a new type associated with a specific additional computation
https://en.wikipedia.org/wiki/Monad_(functional_programming)
String
Monad[String]
// String
'a string'
[] // <- Box
// [String]
// AKA string in a box
['a string']
// String -> Number
const strLength = (str) =>
str.length
strLength(['a string'])
🚫
strLength(['a string'])
['a string'].map(strLength)
✅
[🤷♀️].map(strLength)
[🤷♀️]
[🏃♀️].map(strLen)
[🏃♀️]
fetch('/string')
.map(strLen)
.then(strLen)
[🤷♀️].map(strLength)
const getVal = () =>
Promise.resolve(42)
getVal().then(add(5))
[5].map((num) => num + 2)
Promise.resolve(5)
.then((val) => val + 2)
(It's a mappable container for a value that encapsulates some bit of work within it.)
(It's a mappable container for a value that encapsulates some bit of work within it.)
but those other bits are not what's hard to understand about Monads.
but they demonstrate the "weird" part of monads quite nicely.
Monads are an abstraction over a complexity of a given kind, removing the need to understand or implement that kind of complexity from the user of it.
Monads give a consistent framework for abstractions of this kind so they can interoperate with each other predictably.
We give these kinds of abstractions names -- like "functor" and "monad" -- so that we can communicate that they follow certain rules in their implementation. If an encapsulation is a Functor or a Monad, we already know many things about it.
When forced to work within a strict framework the imagination is taxed to its utmost – and will produce its richest ideas. Given total freedom the work is likely to sprawl. - TS Eliot
Give me the freedom of a tight brief - David Ogilvy
One thing we know about all functors, including monads, is that it has a `map` (or a map-like) method.
This method must take a function that expects a single argument of the type that our Monad contains.
We also know that calling the `map` method returns a new Monad of the same type with a new value contained in it.
MonadA(x).map(x => y) => MonadA(y)
We don't always want to return a Monad of the same type after applying a function. In those cases, we can use a method called `chain` (sometimes `flatMap`) or other chain-like methods.
`chain` will simply return the result of calling the function on the contained value.
We can choose to dump out of our Monad context. 😰
Or change what Monad we wrap the new value in. 😍
MonadA(x).chain(f) => f(x)
MonadA(x).chain((x) => MonadB(x)) => MonadB(x)
There are other methods on functors and monads that are useful and important. Mostly, these can be categorized in three ways.
The Log Monad encapsulates logging.
Logging is a side effect since it is observable outside of the function execution (it prints to the screen).
In functional programming, side effects are encapsulated in Monads like Log and others.
const Log = (value) => ({
map: (fn) => {
const val = fn(value)
console.log(`Mapped ${value} to ${val}`)
return Log(val)
},
chain: (fn) => {
const val = fn(value)
console.log(`Chained ${value} to ${val}`)
return val
},
toString: () => `Log(${value})`
})
Log(42)
.map(add(5))
.map(less(3))
.chain(mul(2))
// 'Mapped 42 to 47'
// 'Mapped 47 to 44'
// 'Chained 44 to 88'
//> 88
The Maybe monad encapsulates nullish checks (null, undefined).
Maps are skipped for nullish values and executed for non-nullish values.
if (obj) {
if (obj.some) {
if (obj.some.deep) {
if (obj.some.deep.property != null) {
return obj.some.deep.property
}
}
}
return null
const Just = (val) => ({
map: (fn) => Maybe(fn(val))
})
const Nothing = () => ({
map: () => Nothing()
})
const Maybe = (value) =>
value == null
? Nothing()
: Just(value)
const obj = {
some: {
deep: {
property: 5
}
}
}
Maybe(obj)
.map(getProp('some'))
.map(getProp('deep'))
.map(getProp('property'))
// Just(5)
const obj = {}
Maybe(obj)
.map(getProp('some'))
.map(getProp('deep'))
.map(getProp('property'))
// Nothing()
return obj?.some?.deep?.property ?? null
const obj = {
some: {
deep: {
property: { url: 'http://api.aumni.fund' }
}
}
}
const head = ([head, ...tail]) => head
const tail = ([head, ...tail]) => tail
Maybe(obj)
.map((obj) => obj?.some?.deep?.property?.url)
.map(match(/^https:\/\/(.*)/))
.map(([, pathname]) => pathname)
// Just('api.aumni.fund')
const obj = {
some: {
deep: {
property: null
}
}
}
const head = ([head, ...tail]) => head
const tail = ([head, ...tail]) => tail
Maybe(obj)
.map((obj) => obj?.some?.deep?.property?.url)
.map(match(/^https:\/\/(.*)/))
.map(([, pathname]) => pathname)
// Nothing()
const url = obj?.some?.deep?.property.url ?? ''
const match = url.match(/^https:\/\/(.*)/)
let pathname = null
if (match !== null) {
pathname = match[1]
}
The Either monad encapsulates error handling (among other things).
Either it's a value (i.e. of type "Right"),
or it's an error (i.e. of type "Left").
If it's a value, we should be able to map over it the same as any Functor.
If it's an error, we skip over the mapping function and return the error value we have to the next functor.
const Right = (value) => ({
map: (fn) => Right(fn(value)),
val: () => value,
type: () => Right.type
})
Right.type = () => 'Right'
const attempt = (thunk) => {
try {
return Right(thunk())
} catch (e) {
return Left(e)
}
}
const Left = (value) => ({
map: () => Left(value),
val: () => value,
type: () => Left.type
})
Left.type = () => 'Left'
const json = `{
"data": [1, 2, 3]
}`
const eitherValOrError =
attempt(() => JSON.parse(json))
eitherValOrError
.map(getProp('data'))
// Right([1, 2, 3])
.map(sum)
// Right(6)
const json = `{
"data": [1, 2,
}`
const eitherValOrError =
attempt(() => JSON.parse(json))
eitherValOrError
.map(getProp('data'))
// Left(Error('SyntaxError'))
.map(sum)
// Left(Error('SyntaxError'))
const either = (onLeft) => (onRight) => (Either) => {
switch (Either.type()) {
case Right.type(): { return onRight(Either.val()) }
case Left.type(): { return onLeft(Either.val()) }
default: return Either.val()
}
}
const eitherSumOrError = attempt(() => JSON.parse(json))
.map(({data}) => data)
.map(sum)
const handleEither = either
(log('Houston, we have a problem: '))
(log('Grand total: '))
handleEither(eitherSumOrError(`
{ "data": [1, 2, }
`))
// Houston, we have a problem:
// Uncaught SyntaxError: Unexpected token } in JSON at position 15
handleEither(eitherSumOrError(`
{ "data": [1, 2, 3] }
`))
// Grand total: 6
Lists pretty common in programming, but there are a couple ways we can get in some unexpected states when we are working with them.
const List = (values) => ({
map: (fn) => List( ... ),
chain: () => ...,
toString: () => `[${values.join(', ')}]`
})
const List = (values) => ({
map: (fn) => List(values.map((item) => fn(item))),
toString: () => `[${values.join(', ')}]`
})
const List = (values) => ({
map: (fn) => List(values.map((item) => fn(item))),
filter: (fn) => List(values.filter(fn)),
concat: (arr) => List(values.concat(arr)),
chain: ???,
toString: () => `[${values.join(', ')}]`
})
const List = (values) => ({
map: (fn) => List(values.map((item) => fn(item))),
filter: (fn) => List(values.filter(fn)),
concat: (arr) => List(values.concat(arr)),
toString: () => `[${values.join(', ')}]`
})
const List = (values) => ({
map: (fn) => List(values.map((item) => fn(item))),
filter: (fn) => List(values.filter(fn)),
concat: (arr) => List(values.concat(arr)),
head: () => values[0],
toString: () => `[${values.join(', ')}]`
})
There is a lot of other really cool things we can do with lists when they are conceptualized in a Monad, such as working with infinite lists, asynchronous lists, observables, etc. If you are interested, I courage you to look deeper at what Monads can do for Lists, for You, and for your applications.
Thank you, friends
Are there any questions?
I hope you have a good day.
why yes, we are hiring. thank you for asking