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

Thinking Functionally in Ruby

Tom Stuart
October 14, 2009

Thinking Functionally in Ruby

A talk about functional programming in general and Ruby's Enumerable module in particular.

Given at the October 2009 LRUG meeting (http://lrug.org/meetings/2009/09/18/october-2009-meeting/). There's a video of this talk at http://skillsmatter.com/podcast/ajax-ria/enumerators.

Tom Stuart

October 14, 2009
Tweet

More Decks by Tom Stuart

Other Decks in Programming

Transcript

  1. THINKING
    FUNCTIONALLY
    IN
    RUBY.
    @tomstuart

    View Slide

  2. Functional
    programming
    is a pretty
    neat idea.
    1:

    View Slide

  3. Enumerable
    contains
    some useful
    methods.
    2:

    View Slide

  4. I hope
    these things
    are connected.

    View Slide

  5. (Functional programming is a
    pretty neat idea)
    1.

    View Slide

  6. What is
    functional
    programming?

    View Slide

  7. A programming
    style.

    View Slide

  8. Two broad
    families of
    languages:

    View Slide

  9. 1. Lisp-like
    •Scheme
    •Common Lisp
    •Dylan
    •Clojure
    •Dynamically typed
    •Homoiconic (code is data)
    •Lists are fundamental

    View Slide

  10. 2. ML-like
    •Standard ML
    •Haskell
    •OCaml
    •Scala (sort of)
    •Statically typed (+ reconstruction)
    •Code is not data
    •ADTs and pattern matching

    View Slide

  11. Functions
    as values

    View Slide

  12. What is
    a function?

    View Slide

  13. View Slide

  14. View Slide

  15. View Slide

  16. 1
    7
    2
    3
    6
    4
    5 4
    2
    6
    8
    3
    7
    1

    View Slide

  17. Functions
    as values

    View Slide

  18. “First-class functions”
    Implies: higher-order
    functions
    “closures”, “lambdas”,
    “anonymous functions”

    View Slide

  19. No side
    effects

    View Slide

  20. Values are immutable
    All functions do is
    return their result
    No state
    No I/O

    View Slide

  21. (In reality,
    different languages
    accomplish this
    to varying degrees.)

    View Slide

  22. Implies: recursion
    Implies: persistent
    data structures

    View Slide

  23. So what?

    View Slide

  24. Unfortunately...

    View Slide

  25. Declarative
    programming
    is counterintuitive when
    youʼre accustomed to
    imperative
    programming
    WHAT!
    HOW!

    View Slide

  26. Copying is expensive

    View Slide

  27. But!

    View Slide

  28. Referential
    transparency
    “an expression can be replaced with its value”
    • Good for efficiency
    • caching/memoisation/inlining
    • ultimately more efficient
    • Good for programmer understanding
    • state is incredibly hard to deal with
    • ultimately more intuitive
    • Good for code reuse and testing

    View Slide

  29. Highly expressive
    Deeply satisfying
    Strongly compositional

    View Slide

  30. Future-proof
    •Mooreʼs Law is running out of steam
    •similar transistor density, more cores
    •The futureʼs concurrent
    •Concurrent access to mutable state is hard...
    •but not if you donʼt have mutable state!
    •Parallelising an algorithm is hard...
    •but itʼs easier if your code isnʼt overspecified

    View Slide

  31. OK seriously:
    so what?

    View Slide

  32. Ruby can do
    some of this.
    Not really a functional language, but we
    can pretend and get some of the benefits.

    View Slide

  33. Use function values
    lambda { |x| x + 1 }
    blocks, procs

    View Slide

  34. Consider treating
    your values
    as immutable
    State is rarely worth it.
    It will kill you in the end.

    View Slide

  35. http://clojure.org/state

    View Slide

  36. Use the
    functional-flavoured
    parts of the
    standard library

    View Slide

  37. Be declarative.
    Think functionally.
    What, not how.

    View Slide

  38. 2.
    (Enumerable contains
    some useful methods)

    View Slide

  39. Enumerable#zip

    View Slide

  40. [1, 2, 3, 4].
    zip([5, 6, 7, 8])
    [1, 2, 3, 4].
    zip([5, 6, 7, 8])

    View Slide

  41. 2
    1 3 4
    6
    5 7 8

    View Slide

  42. 2
    1 3 4
    6
    5 7 8

    View Slide

  43. [ ]
    2
    1 3 4
    6
    5 7 8
    [ ]
    , , , ,
    [ ] [ ] [ ]
    , , ,

    View Slide

  44. [1, 2, 3, 4].
    zip([5, 6, 7, 8],
    [9, 10, 11, 12])

    View Slide

  45. Enumerable#select
    (a.k.a. #find_all)

    View Slide

  46. { |x| x.odd? }

    View Slide

  47. 1
    1
    ?

    View Slide

  48. 1
    1
    ?
    !

    View Slide

  49. 1
    1
    ?
    !
    2
    2
    ?

    View Slide

  50. 1
    1
    ?
    !
    2
    2
    ?
    "

    View Slide

  51. [1, 2, 3, 4].
    select { |x| x.odd? }

    View Slide

  52. 1 2 3 4
    2
    1 3 4
    ? ? ? ?

    View Slide

  53. 1 2 3 4
    2
    1 3 4
    ? ? ? ?
    ! "
    " !

    View Slide

  54. 1 3
    1 3
    ? ?
    ! !
    1 3

    View Slide

  55. 1 3
    [ ]
    ,

    View Slide

  56. Enumerable#partition

    View Slide

  57. [1, 2, 3, 4].
    partition { |x| x.odd? }

    View Slide

  58. 1 2 3 4
    2
    1 3 4
    ? ? ? ?

    View Slide

  59. 1 2 3 4
    2
    1 3 4
    ? ? ? ?
    ! "
    " !

    View Slide

  60. 1
    ?
    !
    4
    ?
    "
    2
    ?
    "
    3
    ?
    !

    View Slide

  61. 1
    ?
    !
    4
    ?
    "
    2
    ?
    "
    3
    ?
    !
    1 3 2 4

    View Slide

  62. 1 3
    [ ]
    , 2 4
    [ ]
    ,
    ,
    [ ]

    View Slide

  63. Enumerable#map
    (a.k.a. #collect)

    View Slide

  64. { |x| x * 3 }

    View Slide

  65. 2
    2
    6
    ×3

    View Slide

  66. 2
    2
    6
    ×3

    View Slide

  67. [1, 2, 3, 4].
    map { |x| x * 3 }

    View Slide

  68. 1 2 3 4
    2
    1 3 4
    6
    3 9 12
    ×3 ×3 ×3 ×3

    View Slide

  69. 1 2 3 4
    2
    1 3 4
    6
    3 9 12
    ×3 ×3 ×3 ×3

    View Slide

  70. 6
    3 9 12
    [ ]
    , , ,

    View Slide

  71. Enumerable#inject
    (a.k.a. #reduce)

    View Slide

  72. { |x, y| x + y }

    View Slide

  73. 3
    3 5
    5
    8
    +

    View Slide

  74. 3
    3
    5
    5 8
    +

    View Slide

  75. [1, 2, 3, 4].
    inject(0) { |x,y| x+y }
    [1, 2, 3, 4].
    inject(0) { |x,y| x+y }

    View Slide

  76. 1
    2
    3
    4
    1
    2
    3
    4
    0
    1
    3
    6
    10
    +
    +
    +
    +

    View Slide

  77. 1
    1 2
    3 3
    6 4
    0
    1
    2
    3
    4
    0
    1
    3
    6 10
    +
    +
    +
    +

    View Slide

  78. module Enumerable
    def inject(initial)
    result = initial
    for element in self
    result = yield(result, element)
    end
    result
    end
    end

    View Slide

  79. a.k.a. “left fold”
    (foldl, fold_left)

    View Slide

  80. 1 2 3 4
    0

    View Slide

  81. 10
    (((0 + 1) + 2) + 3) + 4

    View Slide

  82. ...versus “right fold”
    (foldr, fold_right)

    View Slide

  83. 1 2 3 4 0

    View Slide

  84. 10
    1 + (2 + (3 + (4 + 0)))

    View Slide

  85. The initial argument is
    optional...

    View Slide

  86. [1, 2, 3, 4].
    inject { |x,y| x+y }

    View Slide

  87. [1, 2, 3, 4].
    inject { |x,y| x+y }
    [2, 3, 4].
    inject(1) { |x,y| x+y }

    View Slide

  88. ...but only if the
    output is the same
    type as the input...

    View Slide

  89. >> ['El', 'rug'].
    inject(0) { |l,s| l + s.length }
    => 5
    >> ['El', 'rug'].
    inject { |l,s| l + s.length }
    TypeError: can't convert Fixnum into String
    from (irb):1:in `+'
    from (irb):1
    from (irb):1:in `inject'
    from (irb):1:in `each'
    from (irb):1:in `inject'
    from (irb):1

    View Slide

  90. ...and itʼs meaningful
    to get nil when the
    collection is empty

    View Slide

  91. >> [].inject { |x,y| x+y }
    => nil
    >> [].inject(0) { |x,y| x+y }
    => 0
    >> [].inject(1) { |x,y| x*y }
    => 1

    View Slide

  92. COMPOSE!

    View Slide

  93. [1, 2, 3, 4].
    map { |x| x * 3 }.
    inject(0) { |x| x+y }

    View Slide

  94. 1 2 3 4
    2
    1 3 4
    6
    3 9 12
    ×3 ×3 ×3 ×3
    0
    0
    3
    9
    18
    30
    +
    +
    +
    +

    View Slide

  95. 3
    6
    9
    12
    1 2 3 4
    2
    1 3 4
    6
    3
    9
    12
    ×3 ×3 ×3 ×3
    3
    9
    18
    0
    0
    3
    9
    18 30
    +
    +
    +
    +

    View Slide

  96. I am so
    excited.
    What now?

    View Slide

  97. Review your
    Ruby code.
    Func it up.

    View Slide

  98. result = ''
    for name in names
    unless result.empty?
    result << ', '
    end
    result << name
    end
    result

    View Slide

  99. result = ''
    for name in names
    unless result.empty?
    result << ', '
    end
    result << name
    end
    result
    names.join(', ')
    !

    View Slide

  100. def count_mines_near(x, y)
    count = 0
    for i in x-1..x+1
    for j in y-1..y+1
    count += 1 if mine_at?(i, j)
    end
    end
    count
    end

    View Slide

  101. def count_mines_near(x, y)
    count = 0
    for i in x-1..x+1
    for j in y-1..y+1
    count += 1 if mine_at?(i, j)
    end
    end
    count
    end
    def count_mines_near(x, y)
    ((x-1..x+1).entries * 3).sort.
    zip((y-1..y+1).entries * 3).
    select { |x, y| mine_at?(x, y) }.
    length
    end
    !

    View Slide

  102. def count_mines_near(x, y)
    ((x-1..x+1).entries * 3).sort.
    zip((y-1..y+1).entries * 3).
    select { |x, y| mine_at?(x, y) }.
    length
    end
    [1, 2, 3, 1, 2, 3, 1, 2, 3]
    [1, 1, 1, 2, 2, 2, 3, 3, 3]
    .sort =
    # = (2, 8)

    View Slide

  103. def count_mines_near(x, y)
    ((x-1..x+1).entries * 3).sort.
    zip((y-1..y+1).entries * 3).
    select { |x, y| mine_at?(x, y) }.
    length
    end
    [1, 2, 3, 1, 2, 3, 1, 2, 3]
    [1, 1, 1, 2, 2, 2, 3, 3, 3]
    [7, 8, 9, 7, 8, 9, 7, 8, 9]
    [[1, 7], [1, 8], [1, 9],
    [2, 7], [2, 8], [2, 9],
    [3, 7], [3, 8], [3, 9]]
    .sort =
    .zip(
    ) =
    # = (2, 8)

    View Slide

  104. def count_mines_near(x, y)
    ((x-1..x+1).entries * 3).sort.
    zip((y-1..y+1).entries * 3).
    select { |x, y| mine_at?(x, y) }.
    length
    end
    [1, 2, 3, 1, 2, 3, 1, 2, 3]
    [1, 1, 1, 2, 2, 2, 3, 3, 3]
    [7, 8, 9, 7, 8, 9, 7, 8, 9]
    [[1, 7], [1, 8], [1, 9],
    [2, 7], [2, 8], [2, 9],
    [3, 7], [3, 8], [3, 9]]
    .sort =
    .zip(
    ) =
    .select {…} =
    [[1, 8], [3, 7]] .length = 2
    # = (2, 8)

    View Slide

  105. def count_mines_near(x, y)
    ((x-500..x+500).entries * 1001).sort.
    zip((y-500..y+500).entries * 1001).
    select { |x, y| mine_at?(x, y) }.
    length
    end
    [ [1, 7], [1, 8], …, [1, 1007],
    [2, 7], [2, 8], …, [2, 1007],
    …, …, …,
    [1000, 7], [1000, 8], …, [1000, 1007],
    [1001, 7], [1000, 8], …, [1001, 1007]]

    View Slide

  106. [ [1, 7], [1, 8], …, [1, 1007],
    [2, 7], [2, 8], …, [2, 1007],
    …, …, …,
    [1000, 7], [1000, 8], …, [1000, 1007],
    [1001, 7], [1000, 8], …, [1001, 1007]]
    173 96 121 78 237
    705

    View Slide

  107. def numbers_near(n, radius)
    ((n - radius)..(n + radius)).entries
    end
    def squares_near(x, y, radius)
    diameter = (radius * 2) + 1
    (numbers_near(x, radius) * diameter).
    sort.
    zip(numbers_near(y, radius) * diameter)
    end
    def count_mines_near(x, y, radius = 1)
    squares_near(x, y, radius).
    select { |x, y| mine_at?(x, y) }.
    length
    end

    View Slide

  108. Learn a
    functional language
    •OCaml
    •Scheme (Google “SICP”)
    •Clojure
    •Scala
    •Haskell? Erlang?

    View Slide

  109. Thanks!
    @tomstuart / [email protected]

    View Slide

  110. http://www.flickr.com/photos/somemixedstuff/2403249501/
    http://en.wikipedia.org/wiki/File:Modified-pc-case.png

    View Slide