$30 off During Our Annual Pro Sale. View Details »

Functional Programming in Ruby with Lambdas

Functional Programming in Ruby with Lambdas

The Problem

Object Oriented Design is powerful for organizing software behavior, but without the benefit of lambdas' code-as-data flexibility, in many cases it fails to reduce solutions to their simplest form.

Functional programming has been gaining popularity, and many Rubyists have embraced functional languages such as Elixir and Clojure. While Ruby generally isn't considered a functional language, it does have lambdas and a rich Enumerable library.

Although Ruby's Enumerable functionality is widely appreciated and used, its lambdas generally are not.

The Solution

This presentation introduces the developer to lambdas, and shows how they can be used to write software that is cleaner, simpler, and more flexible.

We'll go through lots of code fragments, exploring diverse ways of exploiting the power of lambdas, and identifying reusable functional design patterns along the way.

keithrbennett

October 06, 2020
Tweet

More Decks by keithrbennett

Other Decks in Programming

Transcript

  1. Functional Programming
    in Ruby with Lambdas
    Chicago Ruby Online Meetup

    October 6, 2020

    Keith R. Bennett

    Bennett Business Solutions, Inc.

    Github, LinkedIn, Twitter, Speakerdeck, StackOverflow

    (@)keithrbennett on GMail, LinkedIn, Speakerdeck, Twitter
    1

    View Slide

  2. Disclaimer
    2
    I am not a functional programming expert.
    The material I present today is based on
    my very limited experience
    with Clojure, Elixir, and Erlang,
    and my exploration
    in my primary and favorite language,
    Ruby.

    View Slide

  3. 3
    A lambda is a free floating function.
    It does not belong to,
    or is even associated with,
    an object or class.

    View Slide

  4. 4
    "Functions in a functional language
    are considered first class,
    meaning that functions can appear
    anywhere that any other language construct
    (such as variables)
    can appear.”
    –Neal Ford

    View Slide

  5. 5
    "...by enabling functions as return values,
    you create the opportunity to build
    highly dynamic, adaptable systems.”
    – Neal Ford

    View Slide

  6. 6
    Remember when
    you encountered code blocks for the first time?

    View Slide

  7. 6
    Remember when
    you encountered code blocks for the first time?
    Remember how they were confusing at first?

    View Slide

  8. 6
    Remember when
    you encountered code blocks for the first time?
    Remember how they were confusing at first?
    But you persevered and mastered them.

    View Slide

  9. 6
    Remember when
    you encountered code blocks for the first time?
    Remember how they were confusing at first?
    But you persevered and mastered them.
    Was it worth the effort?

    View Slide

  10. 6
    Remember when
    you encountered code blocks for the first time?
    Remember how they were confusing at first?
    But you persevered and mastered them.
    Was it worth the effort?
    I thought you’d say that.

    View Slide

  11. 7
    Lambdas are the next step in that progression.

    View Slide

  12. 8
    But it’s not idiomatic Ruby!

    View Slide

  13. 8
    But the benefits of using lambdas
    are so compelling
    that they justify deviating from idiomatic Ruby,
    or even changing it.
    But it’s not idiomatic Ruby!

    View Slide

  14. Benefits of Lambdas
    •Simplicity

    •Extensibility

    •Productivity

    •Fun
    9

    View Slide

  15. 10
    Unnecessary complexity is our enemy.

    View Slide

  16. 11
    As software developers,
    we strive to maximize the ratio:
    Functionality
    Complexity

    View Slide

  17. Why Do We Use Local Variables?
    12

    View Slide

  18. Why Do We Use Local Variables?
    12
    • To limit their scope. What is the value of that?

    View Slide

  19. Why Do We Use Local Variables?
    12
    • To limit their scope. What is the value of that?
    • Does it improve the simplicity of the code?

    View Slide

  20. Why Do We Use Local Variables?
    12
    • To limit their scope. What is the value of that?
    • Does it improve the reliability of the code?
    • Does it improve the simplicity of the code?

    View Slide

  21. 13
    If locality is so important and useful for data,
    why don’t we use it more for code?

    View Slide

  22. 13
    If locality is so important and useful for data,
    why don’t we use it more for code?
    We do have classes…

    View Slide

  23. 13
    If locality is so important and useful for data,
    why don’t we use it more for code?
    We do have classes…
    …but often so many methods…

    View Slide

  24. 13
    If locality is so important and useful for data,
    why don’t we use it more for code?
    We do have classes…
    …but often so many methods…
    …that they need further organizing.

    View Slide

  25. 14
    One Measure of Complexity:
    The Number of Possible Paths of Interaction
    Having n instance methods means n * (n-1) possible call paths
    from one instance method to another

    View Slide

  26. 15
    If only we could define method-like things
    that were nested inside a method,
    and local to it...

    View Slide

  27. We Can!
    16
    We can use lambdas as local nested functions:
    def integrated_result
    fetch_type_1_data = -> do
    # ...
    end
    fetch_type_2_data = -> do
    # ...
    end
    integrate_data = ->(data_1, data_2) do
    # ...digest and return data
    end
    integrate_data.(fetch_type_1_data.(), fetch_type_2_data.())
    end

    View Slide

  28. We Can!
    16
    We can use lambdas as local nested functions:
    Notice anything interesting about the structure?
    def integrated_result
    fetch_type_1_data = -> do
    # ...
    end
    fetch_type_2_data = -> do
    # ...
    end
    integrate_data = ->(data_1, data_2) do
    # ...digest and return data
    end
    integrate_data.(fetch_type_1_data.(), fetch_type_2_data.())
    end

    View Slide

  29. 17
    A Method with Nested Lambdas
    Can Easily Be Converted to a Class
    When we write our methods with nested lambdas (as needed),
    converting the method into a small class is trivially easy.
    We’ll want to do this if the method becomes too complex.
    class IntegratedResult
    private def fetch_type_1_data
    # ...
    end
    private def fetch_type_2_data
    # ...
    end
    private def integrate_data(data_1, data_2)
    # ...digest and return data
    end
    def run
    integrate_data(fetch_type_1_data(), fetch_type_2_data())
    end
    end

    View Slide

  30. 18
    In the following example, format refers to a lambda
    that is called multiple times
    to provide consistent formatting defined in only 1 place.
    Lambdas as Nested Functions: Formatters
    # Returns a multiline string like this:
    #
    # Start Time : 2019-04-09 17:50:16 +0800
    # End Time : 2019-04-09 20:20:16 +0800
    # Duration : 2.5
    def report_times
    times_str = ''
    format = ->(caption, value) do
    "%-11.11s: %s\n" % [caption, value]
    end
    times_str << format.("Start Time", times[:start])
    times_str << format.("End Time", times[:end])
    times_str << format.("Duration", times[:duration_hr])
    times_str << "\n\n"
    end

    View Slide

  31. 19
    You can implement rich functionality using hashes whose values are lambdas.
    This code is used by the rexe utility to offer
    a wide variety of output formats, extremely simply:
    Lambdas in Behavior Lookups
    Example: Formatters
    def formatters
    @formatters ||= {
    amazing_print: ->(obj) { obj.ai << "\n" },
    inspect: ->(obj) { obj.inspect + "\n" },
    json: ->(obj) { obj.to_json },
    marshal: ->(obj) { Marshal.dump(obj) },
    none: ->(_obj) { nil },
    pretty_json: ->(obj) { JSON.pretty_generate(obj) },
    pretty_print: ->(obj) { obj.pretty_inspect },
    puts: ->(obj) { sio = StringIO.new; sio.puts(obj); sio.string }
    to_s: ->(obj) { obj.to_s + "\n" },
    yaml: ->(obj) { obj.to_yaml },
    }
    end

    View Slide

  32. 20
    Similarly for parsers:
    Lambdas in Behavior Lookups
    Example: Parsers
    def input_parsers
    @input_parsers ||= {
    json: ->(string) { JSON.parse(string) },
    marshal: ->(string) { Marshal.load(string) },
    none: ->(string) { string },
    yaml: ->(string) { YAML.load(string) },
    }
    end
    And they are used like this:
    private def init_parser_and_formatters
    @input_parser = lookups.input_parsers[options.input_format]
    @output_formatter = lookups.formatters[options.output_format]
    @log_formatter = lookups.formatters[options.log_format]
    end

    View Slide

  33. Lambdas and Threads
    21
    Packaging code in lambdas is a very nice fit for the use of threads:
    fetch_type_1_data = -> { `uptime` }
    fetch_type_2_data = -> { `whoami` }
    integrate_data = ->(f1, f2) { puts f1; puts f2 }
    type_1_data, type_2_data = [
    Thread.new { fetch_type_1_data.() },
    Thread.new { fetch_type_2_data.() },
    ].map(&:value)
    integrate_data.(type_1_data, type_2_data)
    # Outputs:
    # 16:17 up 6 days, 16:37, 5 users, load averages: 2.79 4.13 5.64
    # kbennett

    View Slide

  34. 22
    Lambda Syntax
    The simplest lambda takes no parameters and returns nothing,
    and is shown below.
    In Ruby versions <= 1.8:
    In Ruby versions >= 1.9, the "stabby lambda" syntax is added:
    lambda {}
    lambda do
    end
    -> {}
    -> do
    end

    View Slide

  35. 23
    In Ruby versions <= 1.8,
    parameters are specified the same way as in a code block:
    Lambda Syntax
    lambda { |param1, param2| }
    lambda do |param1, param2|
    end

    View Slide

  36. 23
    In Ruby versions <= 1.8,
    parameters are specified the same way as in a code block:
    Lambda Syntax
    lambda { |param1, param2| }
    lambda do |param1, param2|
    end
    In the "stabby lambda" alternate syntax for Ruby versions >= 1.9,
    the parameter syntax is identical to method syntax:
    ->(param1, param2) {}
    ->(param1, param2) do
    end

    View Slide

  37. 24
    Lambdas are Assignable
    You can assign a lambda to a variable…
    greeter = ->(name) { "Hello, #{name}!" }
    # all Ruby Versions:
    greeter.call('Juan')
    greeter['Juan']
    # Ruby versions >= 1.9
    greeter.('Juan')
    # All produce: "Hello, Juan!"
    …and then call it:

    View Slide

  38. 25
    Lambdas are Closures
    Local variables defined in the scope in which a lambda is created
    will be accessible to those lambdas.
    This can be very helpful:
    n = 15
    # Create the lambda and call it immediately:
    -> { puts n }.()
    # 15

    View Slide

  39. 26
    However, this can be bad if there is a variable of the same name
    that you did not intend to use:
    Lambdas are Closures
    n = 15
    -> { n = "I just overwrote n." }.()
    puts n # I just overwrote n.

    View Slide

  40. 27
    Lambda Locals
    However, it is possible to define variables as local so that they do not
    override variables of the same name defined outside of the lambda:
    n = 15
    ->(;n) { n = 'I did NOT overwrite n.' }.()
    puts n # still 15

    View Slide

  41. 28
    Bindings provide further opportunity and risk of modifying variables
    external to the lambda on which other code may rely.
    This risk is especially great if more than one lambda shares the same
    binding.
    Lambdas and Bindings
    name = 'Joe'
    f = -> { puts "Name is #{name}" }
    f.() # 'Name is Joe'
    f.binding.eval("name = 'Anil'")
    f.() # 'Name is Anil'

    View Slide

  42. 29
    Using the lambda's [] alias for the 'call' method,
    we can implement a multilevel collection accessor
    that looks like the array and hash [] method:
    A Collection Accessor Lambda
    (See https://github.com/keithrbennett/trick_bag/blob/master/lib/trick_bag/collections/collection_access.rb)
    collection = [
    'a',
    {
    'color' => 'yellow',
    'length' => 30,
    },
    [nil, 'I am a string inside an array', 75]
    ]
    require 'trick_bag'
    accessor = TrickBag::CollectionAccess.accessor(collection)
    puts accessor['1.color'] # yellow
    puts accessor['2.2'] # 75

    View Slide

  43. 30
    Private Methods Aren’t Really Private
    They can be accessed using the send method:
    class ClassWithPrivateMethod
    private def my_private_method
    puts "Hey! You're invading my privacy!"
    end
    end
    ClassWithPrivateMethod.new.send(:my_private_method)
    # --> Hey! You're invading my privacy.

    View Slide

  44. 31
    Lambdas Local to a Method Really Are Private
    How could you possibly access my_local_lambda?
    It is a local variable whose lifetime is limited to that of the method in
    which it is created.
    class ClassWithLocalLambda
    def foo
    my_local_lambda = -> do
    puts "You can't find me."
    end
    # ...
    end
    end

    View Slide

  45. 32
    Self Invoking Anonymous Functions
    Although we normally call lambdas via variables or parameters that
    refer to them, there's nothing to prevent us from calling them
    directly:
    -> do
    source_dir_root = File.join(File.dirname(__FILE__), 'my_gem')
    all_files_spec = File.join(source_dir_root, '**', '*.rb')
    Dir[all_files_spec].each { |file| require file }
    end.()

    View Slide

  46. 32
    Self Invoking Anonymous Functions
    Although we normally call lambdas via variables or parameters that
    refer to them, there's nothing to prevent us from calling them
    directly:
    This hides the local variables so they are not visible outside the
    lambda.
    Coffeescript recommends this technique to prevent scripts from
    polluting the global namespace.
    -> do
    source_dir_root = File.join(File.dirname(__FILE__), 'my_gem')
    all_files_spec = File.join(source_dir_root, '**', '*.rb')
    Dir[all_files_spec].each { |file| require file }
    end.()

    View Slide

  47. 33
    Lambdas Are Great Event Handlers
    event_handler = ->(event) do
    puts "This event occurred: #{event}"
    end
    something.add_event_handler(event_handler)
    # Or, even more concisely,
    # by eliminating the intermediate variable:
    something.add_event_handler(->(event) do
    puts "This event occurred: #{event}"
    end)

    View Slide

  48. 34
    Customizable Behavior in Ruby
    Using Objects (Polymorphism)
    class EvenFilter
    def call(n)
    n.even?
    end
    end
    class OddFilter
    def call(n)
    n.odd?
    end
    end
    def filter_one_to_ten(filter)
    (1..10).select { |n| filter.(n) }
    end
    puts filter_one_to_ten(EvenFilter.new).to_s
    # [2, 4, 6, 8, 10]

    View Slide

  49. 35
    Customizable Behavior in Ruby
    Using Lambdas
    If we use lambdas instead,
    we can dispense with the ceremony and verbosity of classes.
    See how much simpler the code is!
    even_filter = ->(n) { n.even? }
    odd_filter = ->(n) { n.odd? }
    def filter_one_to_ten(filter)
    (1..10).select { |n| filter.(n) }
    end
    puts filter_one_to_ten(even_filter).to_s
    # [2, 4, 6, 8, 10]

    View Slide

  50. 36
    Eliminating Duplication
    See the repetition?
    Let's separate the logic from the data
    by creating a function that provides the logic
    and returns a lambda prefilled with the multiplier (data).
    There are 2 main ways to do this, Partial Application and Currying.
    doubler = ->(n) { 2 * n }
    tripler = ->(n) { 3 * n }
    quadrupler = ->(n) { 4 * n }

    View Slide

  51. 37
    Partial Application
    “Partial application (or partial function application)
    refers to the process of fixing a number of arguments to a function,
    producing another function of smaller arity.” - Wikipedia
    Here we have a lambda that fixes (embeds) the factor
    in the lambda it returns:
    fn_multiply_by = ->(factor) do
    ->(n) { factor * n }
    end
    tripler = fn_multiply_by.(3)
    tripler.(123) # => 369

    View Slide

  52. 38
    Partial Application
    This is the same, except it is a method and not a lambda
    that performs the partial application:
    def fn_multiply_by(factor)
    ->(n) { factor * n }
    end
    tripler = fn_multiply_by(3)
    tripler.(123) # => 369

    View Slide

  53. 39
    The ‘curry’ method returns a lambda that, when called with
    arguments, returns another lambda in which those arguments are
    prefilled (i.e. no longer need to be passed).
    Currying
    multiply_2_numbers = ->(x, y) { x * y }
    tripler = multiply_2_numbers.curry.(3)
    tripler.(123) # => 369
    # or
    multiply_2_numbers = ->(x, y) { x * y }
    currier = multiply_2_numbers.curry
    tripler = currier.(3)
    tripler.(123) # => 369

    View Slide

  54. 40
    Predicates
    “An operator or function that returns either true or false.”
    - Wiktionary
    -> { true }
    -> { false }
    -> (n) { n.even? }

    View Slide

  55. 41
    Predicates as Filters
    This method takes a message filter as a parameter, defaulting to
    a filter that returns true for all messages.
    def get_messages(count, timeout, filter = ->(_message) { true })
    messages = []
    while messages.size < count
    message = get_message(timeout)
    messages << message if filter.(message)
    end
    messages
    end

    View Slide

  56. 42
    Code Blocks as Filters
    Unlike the lambda approach, the code block approach is cryptic and
    not intention-revealing.
    “block_given” and “yield” are anonymous implementation details,
    and the parameter list does not reveal that a filter is being passed.
    def get_messages(count, timeout)
    messages = []
    while messages.size < count
    message = get_message(timeout)
    messages << message if (! block_given?) || yield(message)
    end
    messages
    end

    View Slide

  57. 43
    Separating Unrelated Concerns
    The BufferedEnumerable class in the trick_bag gem
    provides the logic for buffering an enumerable,
    but requires passing it the strategy it needs to fetch the objects.
    The BufferedEnumerable class can be seen at https://github.com/keithrbennett/trick_bag/blob/master/lib/trick_bag/enumerables/buffered_enumerable.rb.
    # Creates an instance with lambdas for fetch and fetch notify behaviors.
    # @param chunk_size the maximum number of objects to be buffered
    # @param fetcher lambda to be called to fetch to fill the buffer
    # @param fetch_notifier lambda to be called to when a fetch is done
    def self.create_with_lambdas(chunk_size, fetcher, fetch_notifier = nil)

    View Slide

  58. 44
    Parentheses are Mandatory
    Unlike Ruby methods,
    which can be called without parentheses,
    a lambda cannot be called without parentheses,
    since that would refer to the lambda object itself:
    2.1.2 :001 > lam = -> { puts 'hello' }
    => #
    2.1.2 :002 > lam
    => #
    2.1.2 :003 > lam.()
    hello

    View Slide

  59. 45
    The Proc Class:
    Lambda and non-Lambda Procs

    View Slide

  60. 45
    The Proc Class:
    Lambda and non-Lambda Procs
    Ruby's Proc class was confusing to me at first.

    View Slide

  61. 45
    The Proc Class:
    Lambda and non-Lambda Procs
    Ruby's Proc class was confusing to me at first.
    Instances can be either lambda or non-lambda.

    View Slide

  62. 45
    The Proc Class:
    Lambda and non-Lambda Procs
    Ruby's Proc class was confusing to me at first.
    Instances can be either lambda or non-lambda.
    And naming is hard; a non-lambda Proc is usually referred to as a "proc".

    View Slide

  63. 45
    The Proc Class:
    Lambda and non-Lambda Procs
    Ruby's Proc class was confusing to me at first.
    Instances can be either lambda or non-lambda.
    And naming is hard; a non-lambda Proc is usually referred to as a "proc".
    But lambdas are Proc’s.

    View Slide

  64. 45
    The Proc Class:
    Lambda and non-Lambda Procs
    Ruby's Proc class was confusing to me at first.
    Instances can be either lambda or non-lambda.
    And naming is hard; a non-lambda Proc is usually referred to as a "proc".
    But lambdas are Proc’s.
    So in spoken language, when something is called a Proc,
    you really don't know which it is.

    View Slide

  65. 45
    The Proc Class:
    Lambda and non-Lambda Procs
    Ruby's Proc class was confusing to me at first.
    Instances can be either lambda or non-lambda.
    And naming is hard; a non-lambda Proc is usually referred to as a "proc".
    But lambdas are Proc’s.
    So in spoken language, when something is called a Proc,
    you really don't know which it is.
    pry(main)> a_proc = proc {}
    #
    pry(main)> a_lambda = -> {}
    #
    pry(main)> [a_proc.class, a_lambda.class].inspect
    "[Proc, Proc]"
    pry(main)> [a_proc.lambda?, a_lambda.lambda?].inspect
    "[false, true]"

    View Slide

  66. 46
    Comparing Lambdas and Procs
    return in Lambdas
    In Ruby, a lambda's return returns from the lambda,
    and not from the enclosing method or lambda.
    def foo
    -> { return }.()
    puts "still in foo"
    end
    # > foo
    # still in foo

    View Slide

  67. 47
    Comparing Lambdas and Procs
    return in Non-Lambda Procs
    In contrast, a Ruby non-lambda Proc return returns from its enclosing scope:
    def foo
    proc { return }.()
    puts "still in foo"
    end
    # > foo
    # > (no output)

    View Slide

  68. 48
    Comparing Lambdas and Procs
    Arity Checking
    Lambdas have strict arity checking; blocks and non-lambda procs do not.
    # ------------------------------------ Lambdas
    two_arg_lambda = ->(a, b) {}
    two_arg_lambda.(1)
    # ArgumentError: wrong number of arguments (1 for 2)
    # ------------------------------------ Procs
    two_arg_proc = proc { |a, b| }
    two_arg_proc.(1)
    # No complaint.
    # ------------------------------------ Blocks
    def foo
    yield('x', 'y') # 2 args passed
    end
    foo { |x| } # No complaint
    foo { |x, y, z| } # No complaint

    View Slide

  69. 49
    Lambdas and Procs are Selfless
    Even though a lambda is an instance of class Proc,
    calling self will not return that instance.
    Instead, it will return its enclosing object,
    which, when not explicitly defined in a class (as in irb), is main.
    This is true for both lambda and non-lambda Procs.
    # Lambdas:
    -> { puts self }.()
    # main
    # Procs:
    (proc { puts self }).()
    # main

    View Slide

  70. 50
    Classes Can Be Defined in a Lambda
    Although classes cannot be defined in a method,
    they can be defined in a lambda.
    class ClassCreationTester
    def create_class
    class C1; end; puts C1 # fails to parse
    end
    def self.create_class
    class C2; end; puts C2 # fails to parse
    end
    # This works!:
    CLASS_CREATOR = -> { class C3; end; puts C3 }
    def test
    create_class
    self.create_class
    CLASS_CREATOR.call
    end
    end
    ClassCreationTester.new.test

    View Slide

  71. 51
    Transform Chains
    We can use Ruby's Enumerable functionality to simplify passing an object
    through multiple transformations.
    tripler = ->(n) { 3 * n }
    squarer = ->(n) { n * n }
    transfomers = [tripler, squarer]
    #144
    start_val = 4
    transfomers.inject(start_val) do |value, transform|
    transform.(value)
    end
    # 144

    View Slide

  72. 52
    Methods in RSpec
    Defining methods in RSpec can be problematic,
    but defining lambdas is simple.
    describe "#made_contact_with_all_cases_in_days?" do
    #…
    let(:create_case_contact) {
    ->(occurred_at, contact_made) {
    create(:case_contact, casa_case: casa_case, creator: volunteer,
    occurred_at: occurred_at, contact_made: contact_made)
    }
    context "when volunteer has made recent contact" do
    it "returns true" do
    create_case_contact.call(Date.current, true)
    expect(volunteer.made_contact_with_all_cases_in_days?).to eq(true)
    end
    end
    context "when volunteer has not made recent contact" do
    it "returns false" do
    create_case_contact.call(Date.current - 60.days, true)
    expect(volunteer.made_contact_with_all_cases_in_days?).to eq(false)
    end
    end

    View Slide

  73. 53
    You can use a lambda where a code block is expected by preceding it with &.
    Using a Lambda Where a Code Block is Expected
    logger = MyLogger.new
    # The usual case is to pass a code block like this:
    logger.info { 'I am inside a block' }
    # But we can instead adapt a lambda:
    proclaimer = -> { 'I am inside a lambda' }
    MyLogger.new.info(&proclaimer)

    View Slide

  74. 54
    Using a Method Where a Lambda is Expected
    You can use a method where a lambda is expected using this notation:
    lambda(&method(:foo))
    def foo(words)
    puts words.join(', ')
    end
    fn = lambda(&method(:foo))
    fn.(%w(Hello World)) # Hello, World

    View Slide

  75. 55

    View Slide