Slide 1

Slide 1 text

Functional Programming in Ruby with Lambdas RubyConf Houston, Texas December 1, 2022 Keith R. Bennett (@)keithrbennett on: GMail, LinkedIn, Speakerdeck, Twitter, Github 1

Slide 2

Slide 2 text

Ruby not Rails 2 I am a developer.

Slide 3

Slide 3 text

Disclaimer 3 I am not a functional programming expert.

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

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

Slide 6

Slide 6 text

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.

Slide 7

Slide 7 text

7 Lambdas are the next step in that progression.

Slide 8

Slide 8 text

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

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

“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!

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

17 .() Abbreviated Notation: Without Parameters, Parentheses are Mandatory

Slide 18

Slide 18 text

18 Lambda and non-Lambda Procs

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

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

Slide 24

Slide 24 text

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

Slide 25

Slide 25 text

25 Unnecessary complexity is our enemy.

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

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]

Slide 31

Slide 31 text

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:

Slide 32

Slide 32 text

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)

Slide 33

Slide 33 text

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)

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

38 Lambdas are Closures n = 15 # Create the lambda and call it immediately: -> { puts n }.() # 15

Slide 39

Slide 39 text

39 n = 15 -> { n = "I just overwrote n." }.() puts n # I just overwrote n. Lambdas are Closures

Slide 40

Slide 40 text

40 Lambda Locals n = 15 ->(;n) { n = 'I did NOT overwrite n.' }.() puts n # still 15

Slide 41

Slide 41 text

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'

Slide 42

Slide 42 text

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.

Slide 43

Slide 43 text

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.

Slide 44

Slide 44 text

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)

Slide 45

Slide 45 text

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]

Slide 46

Slide 46 text

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]

Slide 47

Slide 47 text

47 Predicates A function that returns either true or false. -> { true } -> { false } -> (n) { n.even? }

Slide 48

Slide 48 text

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

Slide 49

Slide 49 text

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

Slide 50

Slide 50 text

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

Slide 51

Slide 51 text

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)

Slide 52

Slide 52 text

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 # => #

Slide 53

Slide 53 text

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

Slide 54

Slide 54 text

54 Eliminating Duplication doubler = -> (n) { 2 * n } tripler = -> (n) { 3 * n } quadrupler = -> (n) { 4 * n }

Slide 55

Slide 55 text

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

Slide 56

Slide 56 text

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

Slide 57

Slide 57 text

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

Slide 58

Slide 58 text

58