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

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
  2. Lambda Topics 4 What is a lambda? What does it

    look like? What is it good for? How is it used?
  3. 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.
  4. 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
  5. “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.
  6. “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!
  7. 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
  8. 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
  9. 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
  10. 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
  11. 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.()
  12. 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
  13. 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
  14. 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.)
  15. 22 Comparing Lambdas and Procs Arity Checking Lambdas have strict

    arity checking; blocks and non-lambda procs do not.
  16. 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
  17. 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
  18. Why Do We Use Local Variables? 27 • To limit

    their scope. Does it improve: • reliability? • simplicity? • readability?
  19. 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
  20. 29 If only we could de fi ne method-like things

    that were nested inside a method, and local to it...
  21. 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]
  22. 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:
  23. 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)
  24. 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)
  25. 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
  26. 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
  27. 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
  28. 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
  29. 38 Lambdas are Closures n = 15 # Create the

    lambda and call it immediately: -> { puts n }.() # 15
  30. 39 n = 15 -> { n = "I just

    overwrote n." }.() puts n # I just overwrote n. Lambdas are Closures
  31. 40 Lambda Locals n = 15 ->(;n) { n =

    'I did NOT overwrite n.' }.() puts n # still 15
  32. 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'
  33. 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.
  34. 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.
  35. 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)
  36. 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]
  37. 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]
  38. 47 Predicates A function that returns either true or false.

    -> { true } -> { false } -> (n) { n.even? }
  39. 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
  40. 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
  41. 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
  42. 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)
  43. 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 # => #<IWasCreatedByALambda:0x000000010efabd88>
  44. 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
  45. 54 Eliminating Duplication doubler = -> (n) { 2 *

    n } tripler = -> (n) { 3 * n } quadrupler = -> (n) { 4 * n }
  46. 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
  47. 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
  48. 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
  49. 58