Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The Way to Fantasyland

wuct
November 05, 2017

The Way to Fantasyland

This is the slide from my talk about functional programming and Fantasyland in JavaScript Developer Conference (JSDC) 2017 in Taiwan.

wuct

November 05, 2017
Tweet

More Decks by wuct

Other Decks in Programming

Transcript

  1. (1 + 2) + 3 = 1 + (2 +

    3) (1 * 2) * 3 = 1 * (2 * 3)
  2. (a ∨ b) ∨ c = a ∨ (b ∨

    c) (a ∧ b) ∧ c = a ∧ (b ∧ c)
  3. The name of the abstraction is Semigroup (1 + 2)

    + 3 = 1 + (2 + 3) (1 * 2) * 3 = 1 * (2 * 3) max(max(2, 4), 6) = max(2, max(4, 6)) min(min(2, 4), 6) = min(2, min(4, 6)) gcd(gcd(2, 4), 6) = gcd(2, gcd(4, 6)) lcm(lcm(2, 4), 6) = lcm(2, lcm(4, 6)) (a ∨ b) ∨ c = a ∨ (b ∨ c) (a ∧ b) ∧ c = a ∧ (b ∧ c) Value Operation
  4. Fantasyland Land Specification “Algebraic JavaScript Specification” An algebra is a

    set of values, a set of operators that it is closed under and some laws it must obey.
  5. #:: “is a member of” "fantasy" #:: String New types

    can be created via type constructors ["fantasy"] #:: Array String #[["fantasy"], ["land"#]] #:: Array (Array String) Type Signature Notation in Fantasyland
  6. #-> is an infix type constructor String #-> Number A

    function with multiple #-> is a curried function String #-> String #-> Number Type Signature Notation in Fantasyland
  7. Lowercase letters stand for type variables a #-> b #~>

    is an infix type constructor of a method a #~> b () stands for no argument () #-> a #=> expresses constraints on type variables Semigroup a #=> a #-> a Type Signature Notation in Fantasyland
  8. concat #:: Semigroup a #=> a #~> a #-> a

    Let’s Define Semigroup x.concat(y).concat(z) = x.concat(y.concat(z)) concat should satisfy the associative law…
  9. [1].concat([2]).concat([3]) "1".concat("2").concat("3") Array and String are already Semigroup by default

    In fact, we still need to add “fantasy-land/“ prefix to methods name, but it’s an implementation detail.
  10. const daggy = require('daggy') const Sum = daggy.tagged('Sum', ['value']) Sum(1)

    #// = { value: 1 } Sum(1).toString() #// = "Sum(1)" Daggy is an utility for creating constructors with named properties. Daggy The Utility
  11. const Sum = daggy.tagged('Sum', ['value']) Sum.prototype.concat = function(that) { return

    Sum(this.value + that.value) } Sum(1).concat(Sum(2)).concat(Sum(3)) #// = Sum(6) Sum as an Instance of Semigroup
  12. const All = daggy.tagged('All', [‘value']) All.prototype.concat = function(that) { return

    All(this.value #&& that.value) } All(false).concat(All(false)).concat(All(true)) #// = All(false) Boolean as an Instance of Semigroup (All)
  13. const Any = daggy.tagged('Any', [‘value']) Any.prototype.concat = function(that) { return

    Any(this.value #|| that.value) } Any(false).concat(Any(false)).concat(Any(true)) #// = Any(true) Boolean as an Instance of Semigroup (Any)
  14. const List = daggy.taggedSum('List', { Cons: ['head', 'tail'], #// Cons

    #:: a #-> List a #-> List a Nil: [] #// Nil #:: List a }) List.prototype.concat = function(that) { return this.cata({ Cons: (head, tail) #=> List.Cons(head, tail.concat(that)), Nil: () #=> that }) } List.Cons(1, List.Nil).concat(List.Cons(2, List.Nil)) #// = List.Cons(1, List.Cons(2, List.Nil)) Linked List as an Instance of Semigroup
  15. const First = daggy.tagged('First', ['value']) First.prototype.concat = function() { return

    this } First("A").concat(First("B").concat("C") #// = First("A") const Last = daggy.tagged('Last', ['value']) Last.prototype.concat = function(that) { return that } Last("A").concat(Last("B")).concat(Last("C")) #// = Last("B") More Semigroup Examples
  16. Let’s See a Real World Usage Example • You have

    a website providing several ways for users to sign up, e.g. email/password and social login. • An user creating two accounts accidentally wants to merge them now. • An user account can be an instance of Semigroup!
  17. const User = daggy.tagged('User', ['name', 'emails', 'isVerified', 'loginTimes']) User.prototype.concat =

    function(that) { return User( this.name.concat(that.name), this.emails.concat(that.emails), this.isVerified.concat(that.isVerified), this.loginTimes.concat(that.loginTimes) ) } User(First('CT'), ['[email protected]'], Any(false), Sum(6)) .concat( User(First('CT Wu'), ['[email protected]'], Any(true), Sum(13)) ) #// = User(First("CT"), ["[email protected]", "[email protected]"], Any(true), Sum(19)) User as an Instance of Semigroup
  18. We can code logic into types! • Types can be

    checked by compilers, so there’s no need for unit tests. • First, Any, Sum and other types we used here can be provided by libraries. • Functional programming languages, like PureScript, already providing these common types in the language.
  19. (A ∪ B) ∪ C = A ∪ (B ∪

    C) (A ∩ B) ∩ C = A ∩ (B ∩ C)
  20. Set as an instance of Semigroup • Union and intersection

    are associative operations. • However, we need to be capable of comparing two elements are equal or not. • Setoid is the algebra!
  21. Setoid equals #:: Setoid a #=> a #~> a #->

    Boolean equals should satisfy following laws: Reflexivity: a.equals(a) ##=== true Commutativity: a.equals(b) ##=== b.equals(a) Transitivity: if a.equals(b) and b.equals(c) then a.equals(c)
  22. Monoid empty #:: Monoid m #=> () #-> m …

    and the laws: Right Identity: m.concat(M.empty()) is equivalent to m Left Identity: M.empty().concat(m) is equivalent to m
  23. Some Monoid implementation… String.empty = () #=> "" Sum.empty =

    () #=> Sum(0) Product.empty = () #=> Product(1) All.empty = () #=> All(true) Any.empty = () #=> Any(false) Max.empty = () #=> Max(-Infinity) Min.empty = () #=> Min(Infinity) Array.empty = () #=> [] List.empty = () #=> List.Nil First.empty = () #=> #// ? Last.empty = () #=> #// ?
  24. Reduce Data by the Monoid operation foldArray #:: Monoid m

    #=> Array m #-> m foldArray = array #=> array.reduce((a, b) #=> a.concat(b), []) foldArray(["A", "B", "C"]) #// = "ABC" foldArray([Sum(1), Sum(2)]) #// = Sum(3) In this sense, we could fold other types by…
 foldList #:: Monoid m #=> List m #-> m foldTree #:: Monoid m #=> Tree m #-> m if they can be reduced.
  25. Foldable reduce #:: Foldable f #=> f a #~> ((b,

    a) #-> b, b) #-> b … and there is no law for Foldable!
  26. List.prototype.reduce = function(f, initial) { return this.cata({ Cons: (head, tail)

    #=> tail.reduce(f, f(initial, head)), Nil: () #=> initial, }) } List.Cons(1, List.Cons(2, List.Cons(3, List.Nil))) .reduce((a, b) #=> a + b, 0) #// = 6 Linked List as an Instance of Foldable
  27. fold #:: Foldable f #=> Monoid m #=> f m

    #-> m fold = M #=> foldable #=> foldable.reduce((acc, x) #=> acc.concat(x), M.empty()) fold(Sum)(List.Cons(Sum(1), List.Cons(Sum(2), List.Nil))) #// = Sum(3) fold(All)([All(true), All(false)]) #// = All(false) Fold Universally
  28. Let’s see another abstraction const inc = x #=> x

    + 1 [1].map(inc) #// = [2] Promise.resolve(1).then(inc) #// = Promise.resolve(2) Observable.of(1).map(inc) #// = Observable.of(2) They are similar … as usual, there is an abstraction over them already!
  29. Functor map #:: Functor f #=> f a #~> (a

    #-> b) #-> f b … and the laws Identity: u.map(x #=> x) ##=== u Composition: u.map(f).map(g) ##=== u.map(x #=> g(f(x)))
  30. The Intuition of Functor [1] #:: Array Int Promise.resolve(1) #::

    Promise Int Observable.of(1) #:: Observable Int 1 2 [2] #:: Array Int Promise.resolve(2) #:: Promise Int Observable.of(2) #:: Observable Int map(x #=> x + 1) Notice that the types of Promise and Observable are overly simplified here. We should also consider error handling in their types.
  31. No, because every element in a set should be a

    Setoid. map #:: Functor f #=> f a #~> (a #-> b) #-> f b b might be not a Setoid.
  32. Have You Seen This? if (a #!= undefined) { if

    (b #!= undefined) { if (c #!= undefined) { } } } Or these… TypeError: Cannot read property ‘…’ of undefined TypeError: … is not a function
  33. Another Useful Functor, Maybe const Maybe = daggy.taggedSum('Maybe', { Just:

    ['value'], Nothing: [] }) Maybe.prototype.map = function(f) { return this.cata({ Just: value #=> Maybe.Just(f(value)), Nothing: () #=> Maybe.Nothing }) } Maybe.Nothing.map(x #=> x.length) #// = Maybe.Nothing Maybe.Just([1, 2, 3]).map(x #=> x.length) #// = Maybe.Just(3)
  34. Function as an Instance of Functor Functor is an abstraction

    over unary type constructors… Functor f #=> f a #~> (a #-> b) #-> f b We know (#->) is a binary type constructor, 
 so (#->) r is an unary type constructor… ((#->) r a) #~> (a #-> b) #-> ((#->) r b) (r #-> a) #~> (a #-> b) #-> (r #-> b)
  35. Function as an Instance of Functor Function.prototype.map = function(f) {

    return x #=>f(this(x)) } const f = (x #=> x * 10).map(x #=> x + 7) f(8) #// = ?
  36. Compose is Just Functor Map map #:: Functor f #=>

    (a #-> b) #-> f a #-> f b map #:: (a #-> b) #-> (r #-> a) #-> (r #-> b) map = (f, g) #=> x #=> f(g(x)) #// = compose