class: center, middle # Functional Programming Fundementals
Tom Duncalf |
tom@tomduncalf.com
|
@tomduncalf
--- ### Concepts we'll cover * Pure functions * Type signatures * First class functions * Currying * Composition * Functors *
Monads
--- ### The book this is based on
https://github.com/MostlyAdequate/mostly-adequate-guide
--- ### Pure functions -- * A pure function is a function that, given the same input(s), will always return the same output * No dependency on anything external to its arguments -- ``` let minPasswordLength = 8 // Not pure const isGoodPassword = function(password) { return password.length >= minPasswordLength } isGoodPassword('tom') // returns: false isGoodPassword('tomtomtom') // returns: true minPasswordLength = 3 isGoodPassword('tom') // returns: true isGoodPassword('tomtomtom') // returns: true ``` -- ``` // Pure const isGoodPassword = function(password, minLength) { return password.length >= minLength } isGoodPassword('tom', 8) // returns: false isGoodPassword('tomtomtom', 8) // returns: true ``` --- ### Pure functions * No observable side effects -- *
Side effect
: "any interaction with the world outside of our function" -- * making an HTTP call * interacting with a database * printing to the screen / logging * changing the file system * "Side effects are a primary cause of incorrect behavior" -- * How can we program without side effects?! -- * We can't - but functional programming techniques can help us contain them and execute them in a controlled way --- ### Why pure functions? -- *
Easy to reason about
: we know a pure function's dependencies, and that it won't change the outside world -- *
Cacheable
(memoization): we know the same input will always give the same output -- *
Testable
: we don't need to mock complex environmental dependencies -- *
Parallelisable
: do not need access to shared memory, no race conditions -- * Meets the mathematical definition of a function:
--- ### Type signatures -- * Specifies what types of inputs a function takes and what type it returns (String, Number, etc) -- * JS has no type checking but useful to document and think about types --- ### Hindley-Milner type signatures ``` // stringLength :: String -> Number const stringLength = function(inputString) { return inputString.length } ``` -- ``` // splitWords :: String -> [String] const splitWords = function(inputString) { return inputString.split(' ') } ``` -- ``` // splitByCharacter :: String -> String -> [String] const splitByCharacter = function(inputString, char) { return inputString.split(char) } ``` -- ``` // splitAndTransformWordsToNumbers :: String -> (String -> Number) -> [Number] const splitAndTransformWordsToNumbers = function(inputString, transformFn) { return inputString.split(' ').map(word => transformFn(word)) } ``` -- ``` // transformArray :: [a] -> (a -> b) -> [b] const transformArray = function(inputArray, transformFn) { return inputArray.map(el => transformFn(el)) } ``` --- ### First class functions -- * Functions are treated just like any other data type * Functions be passed as arguments to other functions, returned from functions, stored in variables, arrays, etc. * They are only called when invoked with `()` -- ``` // functionFunction :: String -> (String -> String) const functionFunction = function(string) { return function() { return 'Hello ' + string } } functionFunction('world') // returns: function() { ... } functionFunction('world')() // returns: 'Hello world' ``` -- ``` // This: const passwords = ['test', 'password123', 'hello'] passwords.map(password => isGoodPassword(password)) // returns [false, true, false] ``` -- ``` // ...is the same as: passwords.map(isGoodPassword) // returns [false, true, false] ``` --- ### Why first class functions? * Less code, more readable * Flexibility of functions accepting and returning other functions * Reduces the need to name things which allows us to write more generic code - a common theme in functional programming --- ### Curried functions -- * A curried function can be called with fewer arguments than it expects * If called with fewer arguments, will return a "partially applied" function that takes the remaining arguments -- ``` // add :: Number -> (Number -> Number) const add = function(x) { return function(y) { return x + y } } ``` -- ``` add(1) // returns: function(y) { return 1 + y } ``` -- ``` add(1)(2) // returns: 3 ``` -- ``` // addOne :: Number -> Number const addOne = add(1) // addTen :: Number -> Number const addTen = add(10) addOne(2) // returns: 3 addTen(2) // returns: 12 ``` --- ### Curried functions * Javascript functions are not curried by default -- * `curry` (for `n` arguments) is included in `lodash` and `ramda` -- ``` const R = require('ramda') // add :: Number -> Number -> Number const add = R.curry(function(x, y) { return x + y }) add(1) // returns: function(y) { return 1 + y } add(1)(2) // returns: 3 add(1, 2) // returns: 3 ``` -- * All functions in `ramda` and `lodash/fp` are curried by default and the data argument comes last so we can "pre-fill" the function ``` // R.test(pattern, inputString) is a curried function: RegExp -> String -> Boolean // isGmailAddress :: String -> Boolean const isGmailAddress = R.test(/\@gmail.com$/) isGmailAddress('tduncalf@gmail.com') // returns: true isGmailAddress('tom@tomduncalf.com') // returns: false // We couldn't do this (as easily...) if it were R.test(inputString, pattern) ``` --- ### Why currying? -- * Allows us to easily make new domain-specific functions from generic ones (which encourages us to reuse functions ) ``` // isGmailAddress :: String -> Boolean const isGmailAddress = R.test(/\@gmail.com$/) isGmailAddress('tduncalf@gmail.com') // returns: true ``` -- ``` // Compared to: const isGmailAddress = function(address) { return R.test(/\@gmail.com$/, address) } // or in ES6... const isGmailAddress = address => R.test(/\@gmail.com$/, address) ``` -- * Currying becomes useful when combined with composition --- ### Composition -- * Composition starts to demonstrate the power of working with pure, first class, curried functions -- ``` // compose :: (b -> c) -> (a -> b) -> (a -> c) const compose = function(f, g) { return function(x) { return f(g(x)) } } ``` * `f` and `g` are functions, `x` is the value being piped through them -- ``` // R.add :: Number -> Number -> Number R.add(1, 2) // 1 + 2, returns: 3 // R.multiply :: Number -> Number -> Number R.multiply(2, 3) // 2 * 3, returns: 6 ``` -- ``` // doublePlusOne :: Number -> Number const doublePlusOne = compose(R.add(1), R.multiply(2)) // expands to: R.add(1, R.multiply(2, x)) doublePlusOne(3) // R.add(1, R.multiply(2, 3)) = (2 * 3) + 1 = 7 doublePlusOne(5) // R.add(1, R.multiply(2, 5)) = (2 * 5) + 1 = 11 ``` --- ### Composition example * Return the average length of any words longer than 3 characters in an input sentence, formatted ``` // average :: [Number] -> Number // format :: Number -> String ``` -- Without composition: ``` // avgBigWordLength :: String -> String const avgBigWordLength = function(input) { const words = input.split(' ') const wordLengths = words.map(word => word.length) const filteredLength = wordLength.filter(length => length > 3) const averageLength = average(filteredLength) return format(averageLength) } ``` -- ``` // avgBigWordLength :: String -> String const avgBigWordLength = function(input) { return format(average(input.split(' ').map(w => w.length).filter(l => l > 3))) } ``` --- ### Composition example ``` // avgBigWordLength :: String -> String const avgBigWordLength = function(input) { return format(average(input.split(' ').map(w => w.length).filter(l => l > 3))) } ``` --- ### Composition example ``` // avgBigWordLength :: String -> String const avgBigWordLength = function(input) { return format(average(`input.split(' ')`.map(w => w.length).filter(l => l > 3))) } ``` --- ### Composition example ``` // avgBigWordLength :: String -> String const avgBigWordLength = function(input) { return format(average(input.split(' ')`.map(w => w.length)`.filter(l => l > 3))) } ``` --- ### Composition example ``` // avgBigWordLength :: String -> String const avgBigWordLength = function(input) { return format(average(input.split(' ').map(w => w.length)`.filter(l => l > 3)`)) } ``` --- ### Composition example ``` // avgBigWordLength :: String -> String const avgBigWordLength = function(input) { return format(`average`(input.split(' ').map(w => w.length).filter(l => l > 3))) } ``` --- ### Composition example ``` // avgBigWordLength :: String -> String const avgBigWordLength = function(input) { return `format`(average(input.split(' ').map(w => w.length).filter(l => l > 3))) } ``` --- ### Composition example ``` // avgBigWordLength :: String -> String const avgBigWordLength = function(input) { return format(average(input.split(' ').map(w => w.length).filter(l => l > 3))) } ``` With functions: ``` // avgBigWordLength :: String -> String const avgBigWordLength = function(input) { return format(average(R.filter(R.lt(3), R.map(R.length, R.split(' ', input))))) } ``` -- With composition (and curried functions): ``` // avgBigWordLength :: String -> String const avgBigWordLength = R.compose(format, average, R.filter(R.lt(3)), R.map(R.length), R.split(' ')) ``` --- ### Composition example ``` // avgBigWordLength :: String -> String const avgBigWordLength = function(input) { return format(average(input.split(' ').map(w => w.length).filter(l => l > 3))) } ``` With composition (and curried functions): ``` // avgBigWordLength :: String -> String const avgBigWordLength = `R.compose`(format, average, R.filter(R.lt(3)), R.map(R.length), R.split(' ')) ``` ``` avgBigWordLength('Hello world how are you?') // input is String ``` --- ### Composition example ``` // avgBigWordLength :: String -> String const avgBigWordLength = function(input) { return format(average(input.split(' ').map(w => w.length).filter(l => l > 3))) } ``` With composition (and curried functions): ``` // avgBigWordLength :: String -> String const avgBigWordLength = R.compose(format, average, R.filter(R.lt(3)), R.map(R.length), `R.split(' ')`) ``` ``` avgBigWordLength('Hello world how are you?') // input is String // split: ['hello', 'world', 'how', 'are', 'you?'] [String] ``` --- ### Composition example ``` // avgBigWordLength :: String -> String const avgBigWordLength = function(input) { return format(average(input.split(' ').map(w => w.length).filter(l => l > 3))) } ``` With composition (and curried functions): ``` // avgBigWordLength :: String -> String const avgBigWordLength = R.compose(format, average, R.filter(R.lt(3)), `R.map(R.length)`, R.split(' ')) ``` ``` avgBigWordLength('Hello world how are you?') // input is String // split: ['hello', 'world', 'how', 'are', 'you?'] [String] // map(length): [5, 5, 3, 3, 4] [Number] ``` --- ### Composition example ``` // avgBigWordLength :: String -> String const avgBigWordLength = function(input) { return format(average(input.split(' ').map(w => w.length).filter(l => l > 3))) } ``` With composition (and curried functions): ``` // avgBigWordLength :: String -> String const avgBigWordLength = R.compose(format, average, `R.filter(R.lt(3))`, R.map(R.length), R.split(' ')) ``` ``` avgBigWordLength('Hello world how are you?') // input is String // split: ['hello', 'world', 'how', 'are', 'you?'] [String] // map(length): [5, 5, 3, 3, 4] [Number] // filter(lt(3)): [5, 5] [Number] ``` --- ### Composition example ``` // avgBigWordLength :: String -> String const avgBigWordLength = function(input) { return format(average(input.split(' ').map(w => w.length).filter(l => l > 3))) } ``` With composition (and curried functions): ``` // avgBigWordLength :: String -> String const avgBigWordLength = R.compose(format, `average`, R.filter(R.lt(3)), R.map(R.length), R.split(' ')) ``` ``` avgBigWordLength('Hello world how are you?') // input is String // split: ['hello', 'world', 'how', 'are', 'you?'] [String] // map(length): [5, 5, 3, 3, 4] [Number] // filter(lt(3)): [5, 5] [Number] // average: 5 Number ``` --- ### Composition example ``` // avgBigWordLength :: String -> String const avgBigWordLength = function(input) { return format(average(input.split(' ').map(w => w.length).filter(l => l > 3))) } ``` With composition (and curried functions): ``` // avgBigWordLength :: String -> String const avgBigWordLength = R.compose(`format`, average, R.filter(R.lt(3)), R.map(R.length), R.split(' ')) ``` ``` avgBigWordLength('Hello world how are you?') // input is String // split: ['hello', 'world', 'how', 'are', 'you?'] [String] // map(length): [5, 5, 3, 3, 4] [Number] // filter(lt(3)): [5, 5] [Number] // average: 5 Number // format: 'average is 5.00' String ``` -- * More compact and readable (demonstrates "pointfree" style) * Clear flow of data along "pipeline" * Encourages re-use and combination of small, generic functions --- ### Composition * But how do we handle potential errors? -- ``` // head :: [a] -> a const head = function(list) { return list[0] } head(['one', 'two', 'three']) // returns: 'one' ``` -- ``` // reverseHead :: [String] -> String const reverseHead = R.compose(R.reverse, head) reverseHead(['one', 'two', 'three']) // returns: 'eno' ``` -- ``` head([]) // returns: undefined ``` -- ``` reverseHead([]) // Exception: Cannot read property 'length' of undefined ``` -- * We don't want exceptions in our code -- * Could wrap `reverse` in a function with a null test, but we don't want to have to do this for every function --- ### Functors -- ``` const Container = function(x) { this.__value = x // treat this as private } // of :: a -> Container a Container.of = function(x) { return new Container(x) } ``` -- ``` Container.of(4) // returns: {"__value": 4}, but let's call this Container(4) Container.of('hello world') // returns: Container('hello world') ``` -- * Add a `map` function to allow us to apply functions to the value inside ``` // map :: (a -> b) -> Container a -> Container b Container.prototype.map = function(fn) { return Container.of(fn(this.__value)) } const four = Container.of(4) four.map(x => x + 2) // returns: Container(6) ``` -- * Can map as many times as we want, the value stays in its container: ``` Container.of(['one', 'two']).map(head).map(R.reverse) // returns: Container('eno') ``` --- ### Functors * "A functor is a type that implements `map` and obeys some laws" -- * Why would we want to contain a value and run `map` to get to it? -- * Abstraction of function application - with `map`, we ask the container to run the function for us rather than running it directly on the value * Allows a functor to provide useful behaviour while mapping --- ### Maybe ``` const Maybe = function(x) { this.__value = x } // of :: a -> Maybe a Maybe.of = function(x) { return new Maybe(x) } // map :: (a -> b) -> Maybe b Maybe.prototype.map = function(fn) { return this.isNothing() ? Maybe.of(null) : Maybe.of(fn(this.__value)); } // isNothing :: Boolean Maybe.prototype.isNothing = function() { return (this.__value === null || this.__value === undefined) } ``` -- ``` Maybe.of('hello world') // returns: Maybe('hello world') Maybe.of('hello world').map(R.reverse) // returns: Maybe('dlrow olleh') ``` -- ``` Maybe.of(null).map(R.reverse) // returns: Maybe(null) ``` -- ``` Container.of(null).map(R.reverse) // Exception: Cannot read property 'length' of null ``` --- ### Maybe * `Maybe` takes care of null checking for us -- * `Maybe` is typically used as the return type of functions that may fail to return a result ``` // safeHead :: [a] -> Maybe(a) const safeHead = function(list) { return Maybe.of(list[0]) } safeHead(['one', 'two', 'three']) // returns: Maybe('one'), head would return 'one' safeHead([]) // returns: Maybe(null), head would return undefined ``` -- * Gives safety by forcing calling code to handle the `null` case - we can only get at the value by calling `map`, so we can't avoid handling `null`s ``` safeHead(['one', 'two']).map(R.reverse) // returns: Maybe('eno') safeHead([]).map(R.reverse) // returns: Maybe(null) // We can't ignore the fact that the function returns a Maybe as it won't work: R.reverse(safeHead(['one', 'two'])) // returns: [] ``` --- ### Maybe * We can compose functors just like functions, but using `map` -- ``` // headReversedUpper :: [a] -> Maybe String const headReversedUpper = R.compose( R.map(R.toUpper), R.map(R.reverse), safeHead ) headReversedUpper(['one', 'two', 'three']) // returns: Maybe('ENO') headReversedUpper([]) // returns: Maybe(null) ``` -- * `null` propogrates through the composition without causing errors -- * The function being called doesn't have to know about functors - `map` "lifts" the function --- ### IO -- * A pure function can't have side effects -- * We can make a function which has a side effect pure by wrapping it in another function, delaying execution: ``` // getFromStorage :: String -> (_ -> String) const getFromStorage = function(key) { return function() { return window.localStorage[key] } } ``` -- * We always get the same output for the same input (a function that gets that key from localStorage, rather than the actual value) -- ``` // window.localStorage = { test: 'hello world' } getFromStorage('test') // returns: function() { return window.localStorage['test'] } ``` -- ``` const getTest = getFromStorage('test') // function() { return window.localStorage['test'] } getTest() // returns: 'hello world' ``` -- * But how can we make this useful for managing side effects? --- ### IO * IO wraps its value in a function ``` const IO = function(x) { this.__value = x } IO.of = function(x) { return new IO(function() { return x }) } ``` -- ``` // currentUrl :: IO String const currentUrl = IO.of(window.location.href) // returns: {__value: [Function]} ``` -- ``` const currentUrl = new function() { this.__value = function() { return window.location.href } } ``` -- ``` currentUrl.__value() // returns: http://www.google.com ``` * Calling code is responsible for running side effects when a function returns `IO` - keeps our code pure --- ### IO * IO wraps its value in a function ``` const IO = function(x) { this.`unsafePerformIO` = x } IO.of = function(x) { return new IO(function() { return x }) } ``` ``` // currentUrl :: IO String const currentUrl = IO.of(window.location.href) // returns: {`unsafePerformIO`: [Function]} ``` ``` const currentUrl = new function() { this.`unsafePerformIO` = function() { return window.location.href } } ``` ``` currentUrl.`unsafePerformIO`() // returns: 'http://www.google.com' ``` * Calling code is responsible for running side effects when a function returns `IO` - keeps our code pure --- ### IO ``` IO.prototype.map = function(fn) { return new IO(R.compose(fn, this.unsafePerformIO)) } ``` -- * We can use `map` to build up a composed chain of actions, ready to be performed by calling `unsafePerformIO` ``` // currentUrl :: IO String const currentUrl = IO.of(window.location.href) // urlReversed :: IO String const urlReversed = currentUrl.map(R.reverse).map(R.concat('Reversed URL: ')) ``` -- * Internally, we have built up something like: ``` const urlReversed = new function() { this.unsafePerformIO = R.compose( R.concat('Reversed URL:'), R.reverse, function() { return window.location.href } ) } ``` -- ``` urlReversed.unsafePerformIO() // returns: 'Reversed URL: moc.elgoog.www//:ptth' ``` --- ### IO * IO enables us to control side effects by pushing responsibility for running them to calling code * The fact that the function has side effects apparent in the type signature --- ### Task * `Task` is designed to help us deal with asynchronous code -- ``` // Using Data.Task from folktale // readFile :: String -> Task Error String const readFile = filename => { return new Task(function(reject, result) { fs.readFile(filename, 'utf-8', (err, data) => { err ? reject(err) : result(data) }) }) } ``` ``` const linesInMyFile = readFile('my_file.txt').map(R.split('\n')).map(R.length) linesInMyFile.fork( error => console.error(error), result => console.log('There are ' + result + ' lines in my file') ) ``` -- * Looks familiar? `Promises` are similar to `Tasks`, with `then` instead of `map` (and without deferred execution) ``` const linesInMyFile = readFile('my_file.txt').then(R.split('\n')).then(R.length) .then(result => console.log('There are ' + result + ' lines in my file')) .catch(error => console.error(error)) ``` --- ### Monads -- * Nested functors lead to uncomfortable code: ``` // safeProp :: Key -> {Key: a} -> Maybe a const safeProp = R.curry((key, obj) => Maybe.of(obj[key])) // safeHead :: [a] -> Maybe a const safeHead = safeProp(0) // firstAddressStreet :: Object -> Maybe(Maybe(Maybe String)) const firstAddressStreet = R.compose( R.map(R.map(safeProp('street'))), R.map(safeHead), safeProp('addresses') ) ``` -- ``` firstAddressStreet({ addresses: [{ street: 'Test St.'}] }) ``` --- ### Monads * Nested functors lead to uncomfortable code: ``` // safeProp :: Key -> {Key: a} -> Maybe a const safeProp = R.curry((key, obj) => Maybe.of(obj[key])) // safeHead :: [a] -> Maybe a const safeHead = safeProp(0) // firstAddressStreet :: Object -> Maybe(Maybe(Maybe String)) const firstAddressStreet = R.compose( R.map(R.map(safeProp('street'))), R.map(safeHead), `safeProp('addresses')` ) ``` ``` firstAddressStreet({ addresses: [{ street: 'Test St.'}] }) // safeProp: Maybe([{ street: 'Test St.'}]) Maybe [Object] ``` --- ### Monads * Nested functors lead to uncomfortable code: ``` // safeProp :: Key -> {Key: a} -> Maybe a const safeProp = R.curry((key, obj) => Maybe.of(obj[key])) // safeHead :: [a] -> Maybe a const safeHead = safeProp(0) // firstAddressStreet :: Object -> Maybe(Maybe(Maybe String)) const firstAddressStreet = R.compose( R.map(R.map(safeProp('street'))), `R.map(safeHead)`, safeProp('addresses') ) ``` ``` firstAddressStreet({ addresses: [{ street: 'Test St.'}] }) // safeProp: Maybe([{ street: 'Test St.'}]) Maybe [Object] // map(safeHead): Maybe(Maybe({ street: 'Test St.' })) Maybe(Maybe Object) ``` --- ### Monads * Nested functors lead to uncomfortable code: ``` // safeProp :: Key -> {Key: a} -> Maybe a const safeProp = R.curry((key, obj) => Maybe.of(obj[key])) // safeHead :: [a] -> Maybe a const safeHead = safeProp(0) // firstAddressStreet :: Object -> Maybe(Maybe(Maybe String)) const firstAddressStreet = R.compose( `R.map(R.map(safeProp('street')))`, R.map(safeHead)`, safeProp('addresses') ) ``` ``` firstAddressStreet({ addresses: [{ street: 'Test St.'}] }) // safeProp: Maybe([{ street: 'Test St.'}]) Maybe [Object] // map(safeHead): Maybe(Maybe({ street: 'Test St.' })) Maybe(Maybe Object) // map(map(safeProp)): Maybe(Maybe(Maybe('Test St.'))) M(M(Maybe String))) ``` --- ### Monads * `join` removes a 'layer' when we have two nested functors of the same type ``` Maybe.prototype.join = function() { return this.isNothing() ? Maybe.of(null) : this.__value } IO.prototype.join = function() { return this.unsafePerformIO() } ``` ``` const mm = Maybe.of(Maybe.of('Hello world')) // Maybe(Maybe('Hello world')) mm.join() // Maybe('Hello world') const ioio = IO.of(IO.of(window.location.href)) // IO(IO('http://google.com')) ioio.join // IO('http://google.com') ``` -- * "Any functor which defines a `join` method, has an `of` method, and obeys a few laws is a monad" --- ### Monads ``` // firstAddressStreet :: Object -> Maybe(Maybe(Maybe String)) const firstAddressStreet = R.compose( R.map(R.map(safeProp('street'))), R.map(safeHead), safeProp('addresses') ) ``` -- ``` // join :: Monad m => m (m a) -> m a const join = function(nestedMonad) { return nestedMonad.join() } ``` -- ``` // firstAddressStreet :: Object -> Maybe String const firstAddressStreet = R.compose( join, R.map(safeProp('street')), join, R.map(safeHead), safeProp('addresses') ) ``` --- ### Monads ``` // safeProp :: Key -> {Key: a} -> Maybe a // safeHead :: [a] -> Maybe a // firstAddressStreet :: Object -> Maybe String const firstAddressStreet = R.compose( join, R.map(safeProp('street')), join, R.map(safeHead), safeProp('addresses') ) ``` ``` firstAddressStreet({ addresses: [{ street: 'Test St.'}] }) ``` --- ### Monads ``` // safeProp :: Key -> {Key: a} -> Maybe a // safeHead :: [a] -> Maybe a // firstAddressStreet :: Object -> Maybe String const firstAddressStreet = R.compose( join, R.map(safeProp('street')), join, R.map(safeHead), `safeProp('addresses')` ) ``` ``` firstAddressStreet({ addresses: [{ street: 'Test St.'}] }) // safeProp: Maybe([{ street: 'Test St.'}]) Maybe [Object] ``` --- ### Monads ``` // safeProp :: Key -> {Key: a} -> Maybe a // safeHead :: [a] -> Maybe a // firstAddressStreet :: Object -> Maybe String const firstAddressStreet = R.compose( join, R.map(safeProp('street')), join, `R.map(safeHead)`, safeProp('addresses') ) ``` ``` firstAddressStreet({ addresses: [{ street: 'Test St.'}] }) // safeProp: Maybe([{ street: 'Test St.'}]) Maybe [Object] // map(safeHead): Maybe(Maybe({ street: 'Test St.' })) Maybe(Maybe Object) ``` --- ### Monads ``` // safeProp :: Key -> {Key: a} -> Maybe a // safeHead :: [a] -> Maybe a // firstAddressStreet :: Object -> Maybe String const firstAddressStreet = R.compose( join, R.map(safeProp('street')), `join`, R.map(safeHead), safeProp('addresses') ) ``` ``` firstAddressStreet({ addresses: [{ street: 'Test St.'}] }) // safeProp: Maybe([{ street: 'Test St.'}]) Maybe [Object] // map(safeHead): Maybe(Maybe({ street: 'Test St.' })) Maybe(Maybe Object) // join: Maybe({ street: 'Test St.' }) Maybe Object ``` --- ### Monads ``` // safeProp :: Key -> {Key: a} -> Maybe a // safeHead :: [a] -> Maybe a // firstAddressStreet :: Object -> Maybe String const firstAddressStreet = R.compose( join, `R.map(safeProp('street'))`, join, R.map(safeHead), safeProp('addresses') ) ``` ``` firstAddressStreet({ addresses: [{ street: 'Test St.'}] }) // safeProp: Maybe([{ street: 'Test St.'}]) Maybe [Object] // map(safeHead): Maybe(Maybe({ street: 'Test St.' })) Maybe(Maybe Object) // join: Maybe({ street: 'Test St.' }) Maybe Object // map(safeProp): Maybe(Maybe('Test St.')) Maybe(Maybe String)) ``` --- ### Monads ``` // safeProp :: Key -> {Key: a} -> Maybe a // safeHead :: [a] -> Maybe a // firstAddressStreet :: Object -> Maybe String const firstAddressStreet = R.compose( `join`, R.map(safeProp('street')), join, R.map(safeHead), safeProp('addresses') ) ``` ``` firstAddressStreet({ addresses: [{ street: 'Test St.'}] }) // safeProp: Maybe([{ street: 'Test St.'}]) Maybe [Object] // map(safeHead): Maybe(Maybe({ street: 'Test St.' })) Maybe(Maybe Object) // join: Maybe({ street: 'Test St.' }) Maybe Object // map(safeProp): Maybe(Maybe('Test St.')) Maybe(Maybe String)) // join: Maybe('Test St.') Maybe String ``` --- ### Monads * `map` followed by `join` is a common pattern - abstracted into a function called `chain` (also know as `flatMap`, `bind` or `>>=`) ``` // chain :: Monad m => (a -> m b) -> m a -> m b const chain = R.curry((mapFn, monad) => return monad.map(mapFn).join() // or R.compose(R.join, R.map(mapFn))(monad) }) ``` -- ``` // safeProp :: Key -> {Key: a} -> Maybe a // safeHead :: [a] -> Maybe a // firstAddressStreet :: Object -> Maybe String const firstAddressStreet = R.compose( `join,` `R.map`(safeProp('street')), `join,` `R.map`(safeHead), safeProp('addresses') ) ``` --- ### Monads * `map` followed by `join` is a common pattern - abstracted into a function called `chain` (also know as `flatMap`, `bind` or `>>=`) ``` // chain :: Monad m => (a -> m b) -> m a -> m b const chain = R.curry((mapFn, monad) => return monad.map(mapFn).join() // or R.compose(R.join, R.map(mapFn))(monad) }) ``` ``` // safeProp :: Key -> {Key: a} -> Maybe a // safeHead :: [a] -> Maybe a // firstAddressStreet :: Object -> Maybe String const firstAddressStreet = R.compose( `chain`(safeProp('street')), `chain`(safeHead), safeProp('addresses') ) ``` -- * `Maybe` is a simple example, but functors can represent lots of things (side effects, async actions) and monads define how these can be chained together --- ### Summary -- * FP concepts don't have to be as theoretical as they are sometimes made out to be - useful even without understanding mathematical foundations -- * We can build up a powerful toolbox to write elegant, clean code, backed up by mathematical rules -- * We don't need to use a "pure" FP language to take advantage of functional programming concepts - e.g. functors can still help us express failures and side effects -- * Read the book if you want to know more! --- ### Links * The book:
Professor Frisby's Mostly Adequate Guide to Functional Programming
*
A great explanation of functors, applicatives and monads in pictures
* FP Libraries:
Ramda
,
Lodash
* Monad/functor implementations for JS:
monet.js
,
Folktale
,
Fantasy Land (Ramda)
*
These slides
--- ### Any questions?