class: center, middle ## A
Somewhat
Principled Introduction
to Functional Programming ### Jonathan Curran --- ## FP in JS According to the Internet* -
Pure functions
- Lambdas - Immutable data - avoid mutating state
- Array - map - filter - reduce
- something about FP being academic ---- - Function composition - Partial function application - Currying ---- - Reactive/Observable Streams --- class:center,middle  --- class:center,middle  --- # function ``` f(x) = y ``` .img[] --- class:only-code ``` // f :: Boolean -> Number function f(b) { if (b === true) return 1 else return 0 } ``` -- ``` // isEven :: Number -> Boolean function isEven(n) { return n % 2 === 0 } ``` --- # not a function ``` f(x) = y ``` .img[] --- class:only-code ``` // hi :: String (language code) -> String (greeting) function hi(lang) { if (lang === 'en') return 'hello' else if (lang === 'fr') return 'bonjour' } ``` -- ``` // hi :: String (language code) -> String (greeting) function hi(lang) { if (lang === 'en') return 'hello' else if (lang === 'fr') return 'bonjour' throw new Error('Unknown language: ' + lang) } ``` -- ``` // hi :: String (language code) -> String (greeting) function hi(lang) { if (lang === 'en') return 'hello' else if (lang === 'fr') return 'bonjour' return 'hello' } ``` --- # Array.prototype.map -- ``` const xs = [1, 2, 3, 4, 5] const ys = [] for (let i = 0; i < xs.length; i++) ys.push(xs[i] + 1) // ys = [2, 3, 4, 5, 6] ``` -- ``` // Array.prototype.map :: [a] ~> (a -> b) -> [b] ``` -- ``` const ys = xs.map(x => x + 1) ``` -- ``` const zs = xs.map(x => x + 1) .map(x => x * 2) // zs = [4, 6, 8, 10, 12] ``` --- # String.prototype.map? -- ``` // map :: String ~> (String -> String) -> String String.prototype.map = function(f) { let s = '' for (let c of this) s += f(c) return s } ``` -- ``` let x = 'foobar'.map(c => c) // x = 'foobar' let x = 'foobar'.map(c => 'aeiou'.split().includes(c) ? c.toUpperCase() : c) // x = 'fOObAr' let x = 'foobar'.map(c => String.fromCharCode(c.charCodeAt(0) + 1)) // x = 'gppcbs' let x = 'foobar'.map(c => c + '-') // x = 'f-o-o-b-a-r-' ``` --- # Object.prototype.map? -- ``` // map :: {...} ~> ([k, v] -> {j: w}) -> {...} Object.prototype.map = function(f) { return Object.assign({}, ...Object.entries(this).map(f)) } // Object.entries :: {...} -> [[k,v]] ``` -- ``` let x = {a: 1, c:42, d: 5} let y = x.map(([k, v]) => ({[k]: v*2})) // y = {a: 2, c: 84, d: 10} ``` --- # Function.prototype.map? -- Function composition! ``` const f = (x) => x + 10 const g = (y) => y * 2 // h === g . f const h = (x) => g(f(x)) ``` --- # A null detour ``` const x = getData() // getData :: () -> null | a if (x !== null) { return doStuff(x) // doStuff :: a -> b } return null ``` -- What if? ``` // map :: (null | a) ~> (a -> b) -> (null | b) ``` -- ``` const b = getData().map(doStuff) ``` -- ``` function maybeApply(data, f) { return data === null ? null : f(data) } ``` --- ### https://github.com/fantasyland/daggy ``` const Option = daggy.taggedSum('Option', { Some: ['value'], None: [], }) ``` -- ``` const a = Option.Some(42) // { value: 42 } const b = Option.None // {} ``` -- ``` // map :: Option a ~> (a -> b) -> Option b Option.prototype.map = function(f) { * return this.cata({ * Some: (value) => Option.Some(f(value)), * None: () => this, * }) } ``` -- ``` // ofNullable :: null | a -> Option a Option.ofNullable = function(value) { return value === null ? Option.None : Option.Some(value) } ``` --- # with our powers combined... ``` // getData :: () -> null | a // doStuff :: a -> b // result :: Option b const result = Option.ofNullable(getData()).map(doStuff) ``` --- ### map - applies a function to each element in the structure - preserves shape of structure - e.g. Array, Object, String, Function, Set, Map, Custom Data Types - returns a new structure - does not modify existing one -- # Functor --- # Sum Types --- ``` class Hello extends React.Component { constructor(props) { super(props) this.state = { name: null, error: null } this.loadData = this.loadData.bind(this) } render() { if (this.state.name === null) return (
Load
) if (this.state.error !== null) return (
Oops: {this.state.error}
) return (
Hello {this.state.name}!
) } loadData() { ... } } ``` --- ``` render({name, error, loadData}) { if (name === null) return (
Load
) if (error !== null) return (
Oops: {error}
) return (
Hello {name}!
) } ``` --- We're attempting: ``` render(Data) = View ``` ``` Data = Set { {name: null, error: null} | {name: 'Q' , error: null} | {name: null, error: 'oops'} } ``` -- Actually with: ``` Data = List [ [name: null , error: null] [error: null , name: null] | [name: 'Q' , error: null] | [error: null , name: 'Q'] | [name: null , error: 'oops'] | [error: 'oops', name: null] ] ``` --- ### What we need: ``` RemoteData = NotAsked | Loading | Failure error | Success data ``` -- ``` const RemoteData = daggy.taggedSum('RemoteData', { NotAsked: [], Loading: [], Failure: ['error'], Success: ['data'], }) ``` -- ``` render({remotedata, loadData}) { return remotedata.cata({ NotAsked: () => (
Load
), Loading: () => (
Loading...
), Failure: (error) => (
Oops: {error}
), Success: (data) => (
Hello {data}!
), }) } ``` --- # Chaining functionality ``` // getData :: () -> null | a // doStuff :: a -> b // result :: Option b const result = Option.ofNullable(getData()).map(doStuff) ``` -- ``` // getMoreData :: b -> Option c // getEvenMoreData :: d -> Option d ``` -- ``` // finalResult :: Option d let finalResult = result.cata({ None: () => this, Some: value => getMoreData(value).cata({ None: () => this, Some: (nextValue) => getEvenMoreData(nextValue), }) }) ``` ^ Don't write this...ever --- ### Let's do better ``` // chain :: Option a ~> (a -> Option b) -> Option b Option.prototype.chain = function(f) { return this.cata({ None: () => this, Some: (value) => f(value), }) } ``` -- ``` // finalResult :: Option d let finalResult = result.chain(getMoreData) .chain(getEvenMoreData) ``` --- # A bit about chaining - we do this very often in different contexts - nested null checks - nested callbacks - async/await - promises - structure/context containing a value - function taking a value and producing another value in context - ... get lost in weeds -- # Monad --- ### A quick word on reduce ``` // fold :: (b, a -> b) -> b -> [a] -> b function fold(f, initialValue, data) { let xs = initialValue for (let x of data) xs = f(xs, x) return xs } let z = fold((a, b) => a + b, 0, [1,2,3,4,5]) // z = 15 ``` -- ``` // map :: (a -> b) -> [a] -> [b] function map(f, data) { return fold((acc, x) => { acc.push(f(x)) return acc }, [], data) } let z = map(a => a + 10, [1,2,3,4]) // z = [11, 12, 13, 14] ``` --- ``` // filter :: (a -> Boolean) -> [a] -> [a] function filter(f, data) { return fold((acc, x) => { if (f(x)) acc.push(x) return acc }, [], data) } let z = filter(a => a > 2, [1,2,3,4]) // z = [3, 4] ``` --- Resources - We covered: - Functor - Monad - You should check out: - Applicative - Professor Frisby's Mostly Adequate Guide to Functional Programming - https://drboolean.gitbooks.io/mostly-adequate-guide/content/ - JS FP Libraries - lodash/FP - Folktale - Ramda - Fluture - Most.js - Learn an ML-based language - Elm - OCaml, Reason (Facebook), BuckeScript - Haskell - Category Theory for Programmers - https://bartoszmilewski.com/2014/10/28/category-theory-for-programmers-the-preface/ - Steal all good ideas! - Don't be scared of by academic-sounding stuff - these are things you don't know ... yet! --- class:center,middle ### jonathan at curran dot in ### https://joncfoo.neocities.org/