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

2022-rubyconf-ruby-lambdas.pdf

 2022-rubyconf-ruby-lambdas.pdf

keithrbennett

December 01, 2022
Tweet

More Decks by keithrbennett

Other Decks in Programming

Transcript

  1. Functional Programming
    in Ruby with Lambdas
    RubyConf

    Houston, Texas

    December 1, 2022

    Keith R. Bennett

    (@)keithrbennett on:

    GMail, LinkedIn, Speakerdeck, Twitter, Github
    1

    View Slide

  2. Ruby not Rails
    2
    I am a
    developer.

    View Slide

  3. Disclaimer
    3
    I am not a functional programming expert.

    View Slide

  4. Lambda Topics
    4
    What is a lambda?
    What does it look like?
    What is it good for?
    How is it used?

    View Slide

  5. What is a lambda?
    5
    A lambda is a free
    fl
    oating function.

    View Slide

  6. 6
    Remember when
    you encountered code blocks for the
    fi
    rst time?
    Remember how they were confusing at
    fi
    rst?
    But we persevered and mastered them.
    Was it worth the e
    ff
    ort?
    Of course it was.

    View Slide

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

    View Slide

  8. Bene
    fi
    ts of Lambdas
    8
    • Simplicity
    • Extensibility
    • Productivity
    • Fun

    View Slide

  9. Ways to Call a Lambda
    9
    # First, let’s define a lambda:


    is_even = ->(n) { n % 2 == 0 }
    is_even.call(2)
    is_even.(2)
    is_even[2] # Don't do this!
    is_even === 2 # Don't do this either!
    case 2


    when is_even # (case equality, ===)


    puts 'is even'


    end

    View Slide

  10. “Callables” — Objects Responding to `call`
    10
    • All Proc instances (lambda and non lambda)
    • Modules or Classes with a `call` class method
    • Instances of classes having a `call` instance method
    • Any other object having a `call` method added to it directly.

    View Slide

  11. “Callables” — Objects Responding to `call`
    11
    def callable?(object)


    object.respond_to?(:call)


    end


    class C; def self.call; end end


    class I; def call; end end


    p callable?(-> {}) # -> true


    p callable?(Proc.new {}) # -> true


    p callable?(C) # -> true


    p callable?(I.new) # -> true


    The .() abbreviation can be used to call any and all of these!

    View Slide

  12. 12
    You can use a lambda where a code block is expected by preceding it with &.
    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)
    Using a Lambda Where a Code Block is Expected

    View Slide

  13. 13
    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


    puts 'Hello, world.'


    end


    fn = lambda(&method(:foo))


    fn.() # Hello, World

    View Slide

  14. 14
    Lambda Syntax
    In Ruby versions <= 1.8:
    In Ruby versions >= 1.9, the "stabby lambda" syntax is added:
    lambda {}


    lambda do


    end
    -> {}


    -> do


    end

    View Slide

  15. 15
    In Ruby versions <= 1.8,
    parameters are speci
    fi
    ed the same way as in a code block:
    lambda { |param1, param2| }


    lambda do |param1, param2|


    end
    Lambda Syntax
    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

  16. 16
    Self Invoking Anonymous Functions
    -> 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

  17. 17
    .() Abbreviated Notation:

    Without Parameters, Parentheses are Mandatory

    View Slide

  18. 18
    Lambda and non-Lambda Procs

    View Slide

  19. 19
    Comparing Lambda and Nonlambda

    Return Behavior
    A lambda's return returns only 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

  20. 20
    In contrast, a non-lambda Proc (proc) return returns from its enclosing scope:
    def foo


    proc { return }.()


    puts "still in foo"


    end


    # > foo


    # > (no output)


    Comparing Lambda and Nonlambda

    Return Behavior

    View Slide

  21. 21
    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.)

    View Slide

  22. 22
    Comparing Lambdas and Procs Arity Checking
    Lambdas have strict arity checking; blocks and non-lambda procs do not.

    View Slide

  23. 23
    Lambdas and Procs are Selfless
    # Executed in irb, in which the top level object's to_s returns 'main'.


    #


    # Lambdas:


    -> { puts self }.()


    # main


    # Procs:


    (proc { puts self }).()


    # main

    View Slide

  24. 24
    Lambdas are Safer
    Unless the looser behavior of code blocks and non-lambda procs is needed,
    I believe that lambdas are safer and therefore preferable.
    Arity Checking Return Behavior

    View Slide

  25. 25
    Unnecessary complexity is our enemy.

    View Slide

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

    View Slide

  27. Why Do We Use Local Variables?
    27
    • To limit their scope. Does it improve:
    • reliability?
    • simplicity?
    • readability?

    View Slide

  28. 28
    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

  29. 29
    If only we could de
    fi
    ne method-like things that
    were nested inside a method,
    and local to it...

    View Slide

  30. 30
    We Can De
    fi
    ne Methods Within Other Methods
    class C


    def outer


    puts 'defining inner'


    def inner


    puts 'inner'


    end


    end


    end


    c = C.new


    c.outer #
    =>
    defining inner
    c.outer.inner #
    =>
    undefined method `inner' for :inner:Symbol (NoMethodError)
    c.inner #
    =>
    inner
    C.instance_methods.grep /er/ #
    =>
    [:outer, :inner]

    View Slide

  31. Lambdas as Inner Methods!
    31
    Encapsulation ‘Lite’
    Would you notice anything interesting about the structure?
    def compute_something(input)


    compute_part = ->(part) do


    # ...


    end


    [compute_part.(input.left), compute_part.(input.right)]


    end


    left_part, right_part = compute_something(input)
    Imagine there were several lambdas there, and not only one.
    We can use lambdas as local nested functions:

    View Slide

  32. 32
    A Method with Nested Lambdas

    Can Easily Be Converted to a Class
    class ComputerOfSomething


    def compute_part(part)


    #
    ...

    end


    # other lambdas would also be converted to methods




    def compute(input)


    [compute_part(input.left), compute_part(input.right)]


    end


    end


    left_part, right_part = ComputerOfSomething.new.compute(input)

    View Slide

  33. 33
    …Or a Module
    module ComputerOfSomething


    class
    <<
    self


    def compute_part(part)


    #
    ...

    end


    # other lambdas would also be converted to methods


    def compute(input)


    [compute_part(input.left), compute_part(input.right)]


    end


    end


    end
    left_part, right_part = ComputerOfSomething.compute(input)

    View Slide

  34. 34
    Lambdas as Nested Functions:

    Formatters
    def report_times(times)


    format_line = ->(caption, value) { format("%-16.16s: %s", caption, value) }


    duration_s = ((times[:finish] - times[:start]).round(2)).to_s


    <<~RETURN_VALUE


    #{format_line.("Start Time", times[:start])}


    #{format_line.("End Time", times[:finish])}


    #{format_line.("Duration (secs)", duration_s)}


    RETURN_VALUE


    end
    puts report_times({ start: Time.now, finish: Time.now + 10 })
    • DRY (Don't Repeat Yourself)
    • Separates high from low level code
    # Returns a multiline string like this:


    #


    # Start Time : 2022-11-04 18:44:23 -0400


    # End Time : 2022-11-04 18:44:33 -0400


    # Duration (secs) : 10.0

    View Slide

  35. 35
    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

  36. 36
    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
    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

  37. Lambdas and Threads
    37
    fetch_type_1_data = -> { `uptime` } # complex and long running task #1


    fetch_type_2_data = -> { `whoami` } # complex and long running task #2


    type_1_data, type_2_data = [


    Thread.new(&fetch_type_1_data),


    Thread.new(&fetch_type_2_data),


    ].map(&:value)


    # now do something useful with type_1_data and type_2_data.


    puts "\nType 1 data:"; ap type_1_data


    puts "\nType 2 data:"; ap type_2_data


    View Slide

  38. 38
    Lambdas are Closures
    n = 15


    # Create the lambda and call it immediately:


    -> { puts n }.()


    # 15

    View Slide

  39. 39
    n = 15


    -> { n = "I just overwrote n." }.()


    puts n # I just overwrote n.
    Lambdas are Closures

    View Slide

  40. 40
    Lambda Locals
    n = 15


    ->(;n) { n = 'I did NOT overwrite n.' }.()


    puts n # still 15

    View Slide

  41. 41
    This illustrates the closure behavior using the `binding`.
    Lambdas and Bindings
    name = 'Joe'


    f = -> { puts "Name is #{name}" }


    f.() # 'Name is Joe'


    f.binding.eval("name = 'Anil'")


    f.() # 'Name is Anil'


    name # 'Name is Anil'

    View Slide

  42. 42
    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

  43. 43
    Lambdas Local to a Method Really Are Private
    class ClassWithLocalLambda


    def foo


    my_local_lambda = -> do


    puts "You can't find me."


    end


    # ...


    end


    end
    Can’t access
    this lambda
    from outside
    this method.
    So you can't get to it with a unit test either.

    View Slide

  44. 44
    Lambdas Are Great Lightweight 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 (this notation is very similar to


    # block notation):


    something.add_event_handler(
    ->
    (event) do


    puts "This event occurred:
    #{
    event}"


    end)

    View Slide

  45. 45
    Customizable Behavior in Ruby Using Classes
    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

  46. 46
    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

  47. 47
    Predicates
    A function that returns either true or false.
    ->
    { true }


    ->
    { false }


    ->
    (n) { n.even? }

    View Slide

  48. 48
    Lambdas as Filter Predicates
    This method takes a message
    fi
    lter as a parameter, defaulting to

    a
    fi
    lter 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

  49. 49
    Code Blocks as Filter Predicates
    Unlike the lambda approach, the code block approach is cryptic and not
    intention-revealing. The parameter list does not reveal that a
    fi
    lter 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

  50. 50
    Multiple Variable Behaviors
    When multiple variable behaviors are needed, code blocks don’t work, but callables do.
    class BufferedEnumerable


    # ...


    def self.create_with_callables(chunk_size, fetcher, fetch_notifier = nil)


    # ...


    end


    # ...


    end


    View Slide

  51. 51
    Separating Unrelated Concerns
    The Bu
    ff
    eredEnumerable class can be seen at https://github.com/keithrbennett/trick_bag/blob/master/lib/trick_bag/enumerables/bu
    ff
    ered_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

  52. 52
    Classes Can Be De
    fi
    ned in a Lambda
    module M


    CREATE_CLASS = -> do


    class ::IWasCreatedByALambda


    # ...


    end


    end


    def self.create_class


    CREATE_CLASS.()


    end


    end


    M.create_class if some_condition # some real condition would go here


    puts IWasCreatedByALambda.new # => #

    View Slide

  53. 53
    Transform Chains — ETL “Lite”
    We can use Ruby's Enumerable functionality to simplify passing an object

    through multiple transformations.
    tripler =
    ->
    (n) { 3 * n }


    squarer =
    ->
    (n) { n * n }


    transformers = [tripler, squarer]


    #144


    start_val = 4


    transformers.inject(start_val) do |value, transform|


    transform.(value)


    end


    # 144

    View Slide

  54. 54
    Eliminating Duplication
    doubler =
    ->
    (n) { 2 * n }


    tripler =
    ->
    (n) { 3 * n }


    quadrupler =
    ->
    (n) { 4 * n }

    View Slide

  55. 55
    Partial Application
    “Partial application (or partial function application)
    refers to the process of
    fi
    xing a number of arguments to a function,
    producing another function of smaller arity.” - Wikipedia
    fn_multiply_by =
    ->
    (factor) do


    ->
    (n) { factor * n }


    end


    tripler = fn_multiply_by.(3)


    tripler.(123) #
    =>
    369

    View Slide

  56. 56
    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
    Partial Application

    View Slide

  57. 57
    The ‘curry’ method returns a lambda that, when called with arguments,
    returns another lambda in which those arguments are pre
    fi
    lled (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

  58. 58

    View Slide