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

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
  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.
  3. 3 A lambda is a free floating function. It does

    not belong to, or is even associated with, an object or class.
  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
  5. 5 "...by enabling functions as return values, you create the

    opportunity to build highly dynamic, adaptable systems.” – Neal Ford
  6. 6 Remember when you encountered code blocks for the first

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

    time? Remember how they were confusing at first? But you persevered and mastered them.
  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. Was it worth the effort?
  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? I thought you’d say that.
  10. 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!
  11. Why Do We Use Local Variables? 12 • To limit

    their scope. What is the value of that?
  12. 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?
  13. 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?
  14. 13 If locality is so important and useful for data,

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

    why don’t we use it more for code? We do have classes…
  16. 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…
  17. 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.
  18. 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
  19. 15 If only we could define method-like things that were

    nested inside a method, and local to it...
  20. 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
  21. 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
  22. 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
  23. 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
  24. 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
  25. 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
  26. 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
  27. 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
  28. 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
  29. 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
  30. 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:
  31. 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
  32. 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.
  33. 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
  34. 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'
  35. 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
  36. 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.
  37. 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
  38. 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.()
  39. 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.()
  40. 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)
  41. 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]
  42. 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]
  43. 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 }
  44. 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
  45. 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
  46. 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
  47. 40 Predicates “An operator or function that returns either true

    or false.” - Wiktionary -> { true } -> { false } -> (n) { n.even? }
  48. 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
  49. 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
  50. 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)
  51. 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' } => #<Proc:0x000001022b04f0@(irb):1 (lambda)> 2.1.2 :002 > lam => #<Proc:0x000001022b04f0@(irb):1 (lambda)> 2.1.2 :003 > lam.() hello
  52. 45 The Proc Class: Lambda and non-Lambda Procs Ruby's Proc

    class was confusing to me at first.
  53. 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.
  54. 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".
  55. 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.
  56. 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.
  57. 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 {} #<Proc:0x00007f97f4944128 (pry):1> pry(main)> a_lambda = -> {} #<Proc:0x00007f97f1280150 (pry):2 (lambda)> pry(main)> [a_proc.class, a_lambda.class].inspect "[Proc, Proc]" pry(main)> [a_proc.lambda?, a_lambda.lambda?].inspect "[false, true]"
  58. 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
  59. 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)
  60. 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
  61. 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
  62. 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
  63. 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
  64. 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
  65. 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)
  66. 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
  67. 55