James Dabbs @jamesdabbs Santa Barbara JavaScript
- Prepackaged solutions to problems
- Prepackaged solutions to shapes of problems
- Prepackaged solutions to shapes of problems
- Shared language for solutions - presenter, strategy, visitor
Favor object composition over class inheritance -- Gang of Four
- Building up large, complex systems by assembling smaller, simpler ones
- To better understand how things can be composed
- To learn a shared language for describing patterns of composition
- Algebra
- Algebra - what can we do with
+
and*
?
- Algebra - what can we do with
+
and*
? - Geometry
- Algebra - what can we do with
+
and*
? - Geometry - what can we do with angles and distances?
- Algebra - what can we do with
+
and*
? - Geometry - what can we do with angles and distances?
- Category theory
- Algebra - what can we do with
+
and*
? - Geometry - what can we do with angles and distances?
- Category theory - what can we do with functions and composition?
A category is
A category is
- a collection of objects
A category is
- a collection of objects
- a collection of functions between those objects
A category is
- a collection of objects
- a collection of functions between those objects
- a means of composing those functions
subject to a few natural rules
Composition must have an identity id
.
Composition must have an identity id
. So
compose(f, id) = f = compose(id, f)
for any function f
Composition must be associative.
Composition must be associative. So
compose(f, compose(g, h)) = compose(compose(f, g), h)
for any functions f
, g
, h
.
"Composition is associative" is a fancy way of saying, "we can write compose(f, g, h)
and not worry about the order"
Objects: sets Functions: functions between sets Composition:
compose(f, g) = f ∘ g
Objects: JavaScript types Functions: JavaScript functions Composition:
compose(f, g) = x => f(g(x))
Since composition is associative, we can write
compose = (...fs) => x => fs.reduceRight((v, f) => f(v), x)
to compose arbitrarily many functions together in order.
Objects: JavaScript types
Functions: f : A -> [B]
f : A -> [B]
g : B -> [C]
Composition: ?
f : A -> [B]
g : B -> [C]
Composition:
compose(f, g) = a => f(a).flatMap(g)
compose = (...fs) => x => fs.reduceRight((v, f) => f(v), x)
pipe = (...fs) => x => fs.reduce((v, f) => f(v), x)
get = key => obj => obj[key]
uppercase = str => str.toUpperCase()
take = length => str => str.slice(0, length)
reverse = str => str.split('').reverse().join('')
pipe(
get('name'),
uppercase,
take(6),
reverse
)({ name: 'James Dabbs' })
const pipeline = pipe(
get('name'),
uppercase,
take(6),
reverse
)
pipeline({ name: 'James Dabbs' })
get('name') : Object -> String
uppercase : String -> String
take(6) : String -> String
reverse : String -> String
get('name') : Object -> String
take(6) : String -> String
get('name') : Object -> String
take(6) : String -> String
get : String -> Object -> String
take : Integer -> String -> String
const pipeline = pipe(
get('name'),
uppercase,
take(6),
reverse
)
pipeline({ name: 'James Dabbs' })
// controller : Request -> JSON
const controller = pipe(
validate,
perform,
present
)
// controller : Request -> JSON
const controller = pipe(
validate, // pure, but may fail validation
perform, // queries db
present // pure
)
const sendPasswordReset = pipe(
promptFor,
findUser,
generateResetToken,
deliverResetToken
)("Email")
prompt : String -> EmailAddress
findUser : EmailAddress -> User
generateResetToken : User -> ResetToken
deliverResetToken : ResetToken -> Email
prompt : String -> EmailAddress
findUser : EmailAddress -> User
generateResetToken : User -> ResetToken
deliverResetToken : ResetToken -> Email
prompt : String -> Either<string, EmailAddress>
findUser : EmailAddress -> Either<string, User>
generateResetToken : User -> Either<string, ResetToken>
deliverResetToken : ResetToken -> Either<string, Email>
prompt : String -> Either<string, EmailAddress>
findUser : EmailAddress -> Either<string, User>
generateResetToken : User -> Either<string, ResetToken>
deliverResetToken : ResetToken -> Either<string, Email>
prompt("Email").
chain(findUser).
map(generateResetToken).
chain(deliverResetToken)
type Either<A, B> = Left<A> | Right<B>
right(value).chain(f) = right(f(value))
left(error).chain(f) = left(error)
prompt : String -> EmailAddress | undefined
findUser : EmailAddress -> User | undefined
generateResetToken : User -> ResetToken | undefined
deliverResetToken : ResetToken -> Email | undefined
type Maybe<A> = Some<A> | None<A>
some(a).chain(f) = some(f(a))
none().chain(f) = none()
prompt : String -> Maybe<EmailAddress>
findUser : EmailAddress -> Maybe<User>
generateResetToken : User -> Maybe<ResetToken>
deliverResetToken : ResetToken -> Maybe<Email>
prompt("Email").
chain(findUser).
map(generateResetToken).
chain(deliverResetToken)
prompt : String -> Async<EmailAddress>
findUser : EmailAddress -> Async<User>
generateResetToken : User -> Async<ResetToken>
deliverResetToken : ResetToken -> Async<Email>
type Async<A> = Promise<A>
type Async<A> = Promise<A>
async(a).chain(f) = a.then(f)
Each of these examples have the following in common
Each of these examples have the following in common
- a type-level transformation
M
Each of these examples have the following in common
- a type-level transformation
M
- a way of chaining an
M<A>
with af : A -> M<B>
Each of these examples have the following in common
- a type-level transformation
M
- a way of chaining an
M<A>
with af : A -> M<B>
- a way of embedding an
A
into aM<A>
Each of these examples have the following in common
- a type-level transformation
M
- a way of chaining an
M<A>
with af : A -> M<B>
- a way of embedding an
A
into aM<A>
And because of this, each example is composable.
- A functor
M
which transforms a typeA
into a new typeM<A>
unit : A -> M<A>
bind : M<A> -> (A -> M<B>) -> M<B>
subject to a few natural rules
Maybe
- computation withundefined
Either
- computation with errorsList
- multi-valued computationAsync
- asyncronous computation
IO
- computations which perform anIO
actionReader
- computations with access to a read-only contextState
- computations with access to mutable state
type State<S, A> = S -> (A, S)
type State<S, A> = S -> (A, S)
f : A -> State<S, B> = A -> S -> (B, S)
g : B -> State<S, C> = B -> S -> (C, S)
type State<S, A> = S -> (A, S)
f : A -> State<S, B> = A -> S -> (B, S)
g : B -> State<S, C> = B -> S -> (C, S)
Need
compose(f, g) : A -> S -> (C, S)
- Shared language is crucial for shared understanding
- Shared language is crucial for shared understanding
- Category theory is the lingua franca of composition
- Shared language is crucial for shared understanding
- Category theory is the lingua franca of composition
- You could have invented a monad
- Shared language is crucial for shared understanding
- Category theory is the lingua franca of composition
- You could have invented a monad (maybe)
- Shared language is crucial for shared understanding
- Category theory is the lingua franca of composition
- You could have invented a monad (maybe)
- Learn you a Haskell
- https://blog.ploeh.dk/2017/10/04/from-design-patterns-to-category-theory/
- https://jrsinclair.com/articles/2019/elegant-error-handling-with-the-js-either-monad/
- https://medium.com/@luijar/kliesli-compositions-in-javascript-7e1a7218f0c4
- https://bartoszmilewski.com/2014/10/28/category-theory-for-programmers-the-preface/
- https://www.freecodecamp.org/news/pipe-and-compose-in-javascript-5b04004ac937/
- http://learnyouahaskell.com/
James Dabbs @jamesdabbs Santa Barbara JavaScript