Slide 1

Slide 1 text

✨ A FUNctional JavaScript Makeover Mike Schutte Detroit JavaScript Meetup June 18, 2018 @tmikeschu (55 slides) 1

Slide 2

Slide 2 text

! Goals ! • Identify and use currying and partial application • Speak to the pros and cons of "functions first, data last" • Reconsider how the order of arguments affects functions @tmikeschu (55 slides) 2

Slide 3

Slide 3 text

λ FP: The Essence λ 1. Break down a problem into a set of problems that are as small and/or simple as possible. 2. Write a FUNction for each small problem. 3. Stitch together those smaller FUNctions via composition to solve the larger problem at hand. @tmikeschu (55 slides) 3

Slide 4

Slide 4 text

OOP vs FP • OOP: everything is an object! • Collaborating classes/objects via messages • information first, transformations second • FP: everything is a function! • FUNction composition via type alignment • transformations first, information second @tmikeschu (55 slides) 4

Slide 5

Slide 5 text

! Javascript is...both! @tmikeschu (55 slides) 5

Slide 6

Slide 6 text

Definitions @tmikeschu (55 slides) 6

Slide 7

Slide 7 text

Pure FUNctions • Same argument(s) === same return value. ALWAYS. • No side-effects. // pure const increaseCount = (count, value) => count + value // impure let count = 0 const increaseCount = value => (count += value) @tmikeschu (55 slides) 7

Slide 8

Slide 8 text

Side-effects ! • mutating data (opposite of "immutability") • network requests • updating state • File I/O i.e., essential but sketchy things that can have unpredictable outcomes. @tmikeschu (55 slides) 8

Slide 9

Slide 9 text

the messy stuff is the fun stuff @tmikeschu (55 slides) 9

Slide 10

Slide 10 text

just isolate it @tmikeschu (55 slides) 10

Slide 11

Slide 11 text

.Methods() vs. FUNctions( ) • Methods are messages sent to objects • user.buildProfile() • Functions process inputs • buildProfile(user) @tmikeschu (55 slides) 11

Slide 12

Slide 12 text

Predicate functions • return true or false users.filter(user => !!user.firstName) @tmikeschu (55 slides) 12

Slide 13

Slide 13 text

Higher order FUNctions take one or more FUNctions as arguments AND/OR return a FUNction @tmikeschu (55 slides) 13

Slide 14

Slide 14 text

Array.prototype.map/reduce/ filter all higher order FUNctions. const ages = [10, 72, 90, 44] ages.map(/* oOo */ age => age % 2 === 0 /* FUNction argument! */) @tmikeschu (55 slides) 14

Slide 15

Slide 15 text

Pointfree style const double = number => number * 2 // ages.map(age => double(age)) ages.map(double) "Points" is a synonym for "arguments". Pointfree syntax omits anonymous functions used to delegate arguments. @tmikeschu (55 slides) 15

Slide 16

Slide 16 text

! Cryptic at first, but... • Less cognitive clutter • It forces us to think more about the transformation being done than about the data being transformed. • By giving the data a name, we "anchor" our thoughts to that data and restrict our understanding of a FUNction's ability. By leaving the data argument out, we can think in more creative and flexible ways. @tmikeschu (55 slides) 16

Slide 17

Slide 17 text

Currying • Convert multi-argument FUNctions to a series of FUNctions that take one argument each (unary) and return a FUNction that takes the next argument. // from const add = (x, y) => x + y // to const add = x => y => x + y @tmikeschu (55 slides) 17

Slide 18

Slide 18 text

Partial Application • A function without all of its required arguments is considered "partially applied" const add = x => y => x + y // partially applied: const increment = add(1) // y => 1 + y increment(10) // => 11 (fully applied) increment(20) // => 21 (fully applied) @tmikeschu (55 slides) 18

Slide 19

Slide 19 text

! ! ! const books = [ { title: "To Kill A Mockingbird", author: "Harper Lee", year: 1960 }, { title: "The Secret History", author: "Donna Tartt", year: 1992 }, { title: "Infinite Jest", author: "David Foster Wallace", year: 1996 }, { title: "Fight Club", author: "Chuck Palahniuk", year: 1996 }, ] @tmikeschu (55 slides) 19

Slide 20

Slide 20 text

Imperative ! const booksInYear = (books, year) => { let matches = [] for (book in books) { if (book.year === year) { matches.push(book) } } return matches } booksInYear(books, 1996) /* => [ { title: "Infinite Jest", author: "David Foster Wallace", year: 1996 }, { title: "Fight Club", author: "Chuck Palahniuk", year: 1996 } ]; */ @tmikeschu (55 slides) 20

Slide 21

Slide 21 text

Declarative ! /*const booksInYear = (books, year) => { let matches = [] for (book in books) { if (book.year === year) { matches.push(book) } } return matches }*/ const booksInYear = (year, books) => books.filter(book => book.year === year) @tmikeschu (55 slides) 21

Slide 22

Slide 22 text

/* Full Application -- less reusable */ const booksInYear = (year, books) => books.filter(book => book.year === year) booksInYear(1996, books) booksInYear(1996, otherBooks) /* Partial Application -- more reusable */ const booksInYear = year => books => books.filter(book => book.year === year) const in96 = booksInYear(1996) in96(books) in96(otherBooks) @tmikeschu (55 slides) 22

Slide 23

Slide 23 text

Why attach this to books and years? const booksInYear = year => books => books.filter(book => book.year === year) We are really just looking at equality between a property on an object and a given value. Books and years are not as reusable and generalizable as objects, properties, and values. @tmikeschu (55 slides) 23

Slide 24

Slide 24 text

Let's Remove Books! -> @tmikeschu (55 slides) 24

Slide 25

Slide 25 text

// const booksInYear = year => books => books.filter(book => book.year === year); const filterByYear = year => list => list.filter(item => year === item.year) const in96 = filterByYear(1996) const movies = [ { title: "Fargo", year: 1996 }, { title: "The Shape of Water", year: 2018 }, ] in96(movies) // => [ { title: "Fargo", year: 1996 } ] in96(books) /* => [ { title: "Infinite Jest", author: "David Foster Wallace", year: 1996 }, { title: "Fight Club", author: "Chuck Palahniuk", year: 1996 } ]; */ @tmikeschu (55 slides) 25

Slide 26

Slide 26 text

FUNctions first, data last... const filterByYear = year => list => list.filter(item => year === item.year) Our generic list data is coming last in the argument chain ( ! ), but then we are calling the list first with the filter method. @tmikeschu (55 slides) 26

Slide 27

Slide 27 text

Data Last -- Part 1 of 3 爻 Let's turn the .filter method into a filter FUNction: const filter = predicate => filterable => filterable.filter(predicate) const filterByYear = year => list => filter(item => year === item.year)(list) Now list comes last as an invoking argument, so we can change this to pointfree syntax: const filterByYear = year => filter(item => year === item.year) @tmikeschu (55 slides) 27

Slide 28

Slide 28 text

! We've removed references to books, movies, and even a general list, so we are free to think about filterByYear as a general purpose FUNctions that filters something based on a year property. We are still attached to the anchors of item and year, and providing a lot of how for our solution. const filterByYear = year => filter(item => year === item.year) @tmikeschu (55 slides) 28

Slide 29

Slide 29 text

Data Last -- Part 2 of 3 Let's turn the .year property into a prop FUNction: and let's turn the === operator into a equals FUNction: const prop = name => obj => obj[name] const equals = a => b => a === b const filterByYear = year => filter(item => equals(year)(prop("year")(item))) @tmikeschu (55 slides) 29

Slide 30

Slide 30 text

That looks disgusting! year => filter(item => equals(year)(prop("year")(item))) @tmikeschu (55 slides) 30

Slide 31

Slide 31 text

⚖ filter(item => equals(year)(prop("year")(item))) There is a weird tipping point of clarity with FP and pointfree style. To make this all worth it, we need to reach for one more critical tool in our FP toolkit: composition! @tmikeschu (55 slides) 31

Slide 32

Slide 32 text

Simplest composition inner to outer const exclaim = str => `${str}!!!` const toUpper = str => str.toUpperCase() const repeat = str => `${str} ${str}` const freakout = str => exclaim(toUpper(repeat(str))) freakout("hey") // => "HEY HEY!!!" @tmikeschu (55 slides) 32

Slide 33

Slide 33 text

pipe ! • pipe is a higher order FUNction. • It takes a list of one or more FUNctions, and returns a FUNction. • That return FUNction takes one or more arguments. • Those arguments start the "pipeline", where the output of the FUNction on the left is the input for the FUNction to its right. @tmikeschu (55 slides) 33

Slide 34

Slide 34 text

const pipe = (...FUNs) => startingValue => FUNs.reduce((returnValue, FUN) => FUN(returnValue), startingValue) const exclaim = str => `${str}!!!` const toUpper = str => str.toUpperCase() const repeat = str => `${str} ${str}` // const freakout = str => exclaim(toUpper(repeat(str))) const freakout = pipe( repeat, toUpper, exclaim, ) freakout("hey") // => "HEY HEY!!!" @tmikeschu (55 slides) 34

Slide 35

Slide 35 text

Naming FUNctions const pipe = (...FUNs) => startingValue => FUNs.reduce((returnValue, FUN) => FUN(returnValue), startingValue) const split = char => str => str.split(char) // => Array const reverseArr = arr => arr.reverse() // => Array const join = char => arr => arr.join(char) // => String //const reverseStr = str => pipe(split(''), reverse, join(''))(str); const reverseStr = pipe( split(""), reverse, join(""), ) // pointfree reverseStr("kayak") // => "kayak" reverseStr("Javascript") // => "tpircsavaJ" @tmikeschu (55 slides) 35

Slide 36

Slide 36 text

Data Last -- Part 3 of 3 const prop = name => obj => obj[name] const equals = a => b => a === b // FROM year => filter(item => equals(year)(prop("year")(item))) // TO year => filter(item => pipe( prop("year"), equals(year), )(item), ) @tmikeschu (55 slides) 36

Slide 37

Slide 37 text

Now that item is simply an invoked argument on the end the return FUNction from pipe, we can convert our filter FUNction to be pointfree: // year => filter(item => pipe(prop("year"), equals(year))(item)); year => filter( pipe( prop("year"), equals(year), ), ) @tmikeschu (55 slides) 37

Slide 38

Slide 38 text

To clean this up more, let's pull out filter's argument to a named FUNction. // FROM year => filter(item => equals(year)(prop("year")(item))) // gross // TO const yearEquals = year => pipe( prop("year"), equals(year), ) year => filter(yearEquals(year)) // nice @tmikeschu (55 slides) 38

Slide 39

Slide 39 text

BUT WAIT! @tmikeschu (55 slides) 39

Slide 40

Slide 40 text

Do we need year? // FROM const yearEquals = year => pipe( prop("year"), equals(year), ) // the specifics here are "year" and `year`, let's make those arguments in that order // TO const propEquals = name => value => pipe( prop(name), equals(value), ) @tmikeschu (55 slides) 40

Slide 41

Slide 41 text

// const yearEquals = year => propEquals("year")(year); // const yearEquals = propEquals("year"); // pointfree // in this case, the API for propEquals("year") is similar to yearEquals // so let's skip the const assignment propEquals("year") @tmikeschu (55 slides) 41

Slide 42

Slide 42 text

...and now we are just reading inner-to-outer, so let's pipe: // FROM year => filter(propEquals("year"))(year) // TO year => pipe( propEquals("year"), filter, )(year) @tmikeschu (55 slides) 42

Slide 43

Slide 43 text

...and now that year comes last, we can go pointfree! // FROM const filterByYear = year => pipe( propEquals("year"), filter, )(year) // TO const filterByYear = pipe( propEquals("year"), filter, ) // pointfree @tmikeschu (55 slides) 43

Slide 44

Slide 44 text

Review ! // beginning const booksInYear = (books, year) => { let matches = [] for (book in books) { if (book.year === year) { matches.push(book) } } return matches } // middle const filterByYear = year => filter(item => equals(year)(prop("year")(item)))(year) // wtgdf // end const filterByYear = pipe( propEquals("year"), filter, ) @tmikeschu (55 slides) 44

Slide 45

Slide 45 text

Remove the year anchor const filterBy = propName => pipe( propEquals(propName), filter, ) filterBy("year") @tmikeschu (55 slides) 45

Slide 46

Slide 46 text

All together ! // units const prop = name => obj => obj[name] const equals = a => b => a === b const pipe = (...FUNs) => startingValue => FUNs.reduce((returnValue, FUN) => FUN(returnValue), startingValue) const propEquals = name => value => pipe( prop(name), equals(value), ) const filter = predicate => filterable => filterable.filter(predicate) @tmikeschu (55 slides) 46

Slide 47

Slide 47 text

All together ! // compositions const filterBy = propName => pipe( propEquals(propName), filter, ) const in96 = filterBy("year")(1996) in96(movies) // => [ { title: "Fargo", year: 1996 } ] in96(books) /* => [ { title: "Infinite Jest", author: "David Foster Wallace", year: 1996 }, { title: "Fight Club", author: "Chuck Palahniuk", year: 1996 } ]; */ @tmikeschu (55 slides) 47

Slide 48

Slide 48 text

Now that is FUNctional. const prop = name => obj => obj[name] // FUNFUN const equals = a => b => a === b // FUNFUN const pipe = (...FUNs) => startingValue => FUNs.reduce((returnValue, FUN) => FUN(returnValue), startingValue) // FUNFUN const propEquals = name => value => pipe( prop(name), equals(value), ) // FUNFUNFUN const filter = predicate => filterable => filterable.filter(predicate) // FUNFUN const filterBy = propName => pipe( propEquals(propName), filter, ) // FUNFUNFUN const filterByYear = filterBy("year") // FUNFUN const in96 = filterByYear(1996) // FUN in96(movies) /* DATA */ // => [ { title: "Fargo", year: 1996 } ] in96(books) /* DATA */ /* => [ { title: "Infinite Jest", author: "David Foster Wallace", year: 1996 }, { title: "Fight Club", author: "Chuck Palahniuk", year: 1996 } ]; */ @tmikeschu (55 slides) 48

Slide 49

Slide 49 text

New Product Requirements! • A list of all the titles needs to be shown on an index page. • All titles should be lowercase, because it's hip @tmikeschu (55 slides) 49

Slide 50

Slide 50 text

No problem! ! const map = mapper => mappable => mappable.map(mapper) const toLower = str => str.toLowerCase() pipe( in96, map( pipe( prop("title"), toLower, ), ), )(books) /* => [ "infinite jest", "fight club" ]; */ @tmikeschu (55 slides) 50

Slide 51

Slide 51 text

No problem! ! const map = mapper => mappable => mappable.map(mapper) const toLower = str => str.toLowerCase() const lowerTitle = pipe( prop("title"), toLower, ) pipe( in96, map(lowerTitle), )(books) /* => [ "infinite jest", "fight club" ]; */ @tmikeschu (55 slides) 51

Slide 52

Slide 52 text

Tradeoffs ⚖ /* library code (e.g., Ramda) */ const pipe = (...FUNs) => startingValue => FUNs.reduce((returnValue, FUN) => FUN(returnValue), startingValue) const prop = name => obj => obj[name] // can be used for any object const equals = a => b => a === b const propEquals = name => value => pipe( prop(name), equals(value), ) // can be used for any object const filter = predicate => filterable => filterable.filter(predicate) /* library code (e.g., Ramda) */ @tmikeschu (55 slides) 52

Slide 53

Slide 53 text

Tradeoffs ⚖ // more explicit, easier to read // reusable for different sets of ([{ year }], year) const booksInYear = (books, year) => { let matches = [] for (book in books) { if (book.year === year) { matches.push(book) } } return matches } /* vs */ const filterBy = propName => pipe( propEquals(propName), filter, ) // can be used for any prop name const filterByYear = filterBy("year") // context specific helper const in96 = filterByYear(1996) // can be reused for anything "year-able" and"filter-able" @tmikeschu (55 slides) 53

Slide 54

Slide 54 text

! Goals ! • Identify and use currying and partial application • Speak to the pros and cons of "functions first, data last" • Reconsider how the order of arguments affects functions @tmikeschu (55 slides) 54

Slide 55

Slide 55 text

Thank you! Have UN out there @tmikeschu (55 slides) 55