Skip to content

Instantly share code, notes, and snippets.

@keithrbennett
Last active March 5, 2020 21:03
Show Gist options
  • Save keithrbennett/927c989cc301c63d35a8ceec9f3f9404 to your computer and use it in GitHub Desktop.
Save keithrbennett/927c989cc301c63d35a8ceec9f3f9404 to your computer and use it in GitHub Desktop.
Text and code for Functional Programming in Ruby presentation.
Source Code for Ruby Lambdas Presentation
# This is the source code included in the Ruby Lambdas presentation
# given at Ruby Tuesday, Penang, Malaysia, 9 April 2019.
# This file is available at:
# https://gist.github.com/keithrbennett/927c989cc301c63d35a8ceec9f3f9404
# and the presentation slides PDF is at:
# https://speakerdeck.com/keithrbennett/ruby-lambdas
# -- Keith Bennett (@keithrbennett)
# ============================================================================
# …Yet We Do Exactly That With Methods
class C
def aaaa
# ...
end
def bbbb
# ...
end
def cccc
# ...
end
def dddd
# ...
end
def eeee
# ...
end
def ffff
# ...
end
def gggg
# ...
end
def hhhh
# ...
end
end
# ============================================================================
# We Can!
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
# ============================================================================
# # A Method with Nested Lambdas Can Easily Be Converted to a Class
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
# then, to use it: IntegratedResult.new.run
# ============================================================================
# Lambdas and Threads
# ...fetcher lambdas have been defined above
type_1_data = nil; type_2_data = nil
[
Thread.new { type_1_data = fetch_type_1_data.() },
Thread.new { type_2_data = fetch_type_2_data.() },
].each { |thread| thread.join }
integrate_data.(type_1_data, type_2_data)
# ============================================================================
# Lambda Syntax
The simplest lambda takes no parameters and returns nothing, and is shown below.
In Ruby versions <= 1.8:
lambda {}
lambda do
end
In Ruby versions >= 1.9, the "stabby lambda" syntax is added:
-> {}
-> do
end
In Ruby versions <= 1.8, parameters are specified the same way as in a code block:
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
# ============================================================================
# Lambdas are Assignable
You can assign a lambda to a variable...
greeter = ->(name) { "Hello, #{name}!" }
...and then call it:
# all Ruby Versions:
greeter.call('Juan')
greeter['Juan']
# Ruby versions >= 1.9
greeter.('Juan')
# All produce: "Hello, Juan!"
# ============================================================================
# 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
# ============================================================================
# # Lambdas Are Closures
However, this can be _bad_ if there is a variable of the same name that you did not intend to use:
n = 15
-> { n = "I just overwrote n." }.()
puts n # I just overwrote n.
# Lambda Bindings
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.
name = 'Joe'
f = -> { puts "Name is #{name}" }
f.() # 'Name is Joe'
f.binding.eval("name = 'Anil'")
f.() # 'Name is Anil'
# 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
f = ->(;n) { n = 'I did NOT overwrite n.' }
f.()
puts n # still 15
# ============================================================================
# A Collection Accessor Lambda
Using the lambda's [] method (which is an alias for the 'call' method),
we can implement a multilevel collection accessor that looks like the array and hash [] method:
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
# ============================================================================
# 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.
# ============================================================================
# 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
# ============================================================================
# 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.()
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.
# ============================================================================
# Lambdas as 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)
# ============================================================================
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]
# ============================================================================
# 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]
# ============================================================================
# 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.
There are 2 main ways to do this, Partial Application and Currying.
double = ->(n) { 2 * n }
triple = ->(n) { 3 * n }
quadruple = ->(n) { 4 * n }
# ============================================================================
# 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
# ============================================================================
# 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
==============================================
# ============================================================================
"The technique of transforming a function that takes multiple arguments into a function that takes a single argument (the first of the arguments to the original function) and returns a new function that takes the remainder of the arguments and returns the result."
- Wiktionary
multiply_2_numbers = ->(x, y) { x * y }
tripler = multiply_2_numbers.curry.(3)
tripler.(7) # => 21
# ============================================================================
# Predicates
-> { true }
-> { false }
-> (n) { n.even? }
# ============================================================================
# Predicates as Filters
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
# ============================================================================
# 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
# ============================================================================
# Separating Logic and Strategy
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.
# 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)
# ============================================================================
# Lambdas as Nested Functions
In the following example, format refers to a lambda that is called multip
to provide consistent formatting defined in only 1 place.
def report_times
times_str = ''
format = ->(caption, value) do
"%-12.12s: %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
# ============================================================================
# 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
# ============================================================================
# Methods Are Not Always an Option…
Let's say we want to dry up our RSpec tests
by extracting common functionality into a method
and then calling it several times.
It is not possible to do this with a method in the describe block:
describe 'Something' do
def test_gt_zero(n)
specify "#{n} > 0" do
expect(n > 0).to eq(true)
end
end
test_gt_zero(1)
test_gt_zero(2)
end
# yields: undefined method 'test_gt_zero' for
# RSpec::ExampleGroups::Something:Class (NoMethodError)
# ============================================================================
# …But Lambdas Can Be Created and Used Pretty Much Anywhere
We can do what we need to with a lambda:
describe 'Something' do
test_gt_zero = ->(n) do
specify "#{n} > 0" do
expect(n > 0).to eq(true)
end
end
test_gt_zero.(1)
test_gt_zero.(2)
end
# ..
#
# Finished in 0.03158 seconds (files took 0.0898 seconds to load)
# 2 examples, 0 failures
# ============================================================================
# Methods Work in RSpec Outside the Describe, But
A method will work in RSpec if defined outside the describe block,
but in that case we cannot make it local to that describe block.
def test_gt_zero(n)
specify "#{n} > 0" do
expect(n > 0).to eq(true)
end
end
describe 'Something' do
test_gt_zero 1
test_gt_zero 2
end
# ..
#
# Finished in 0.00084 seconds (files took 0.08872 seconds to load)
# 2 examples, 0 failures
# ============================================================================
# Using a Lambda Where a Code Block is Expected
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 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
The Proc Class: Lambda and non-Lambda Procs
Ruby's Proc class wa 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.
# ============================================================================
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
# ============================================================================
# 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)
# ============================================================================
# Comparing Lambdas, Procs, and Blocks: Argument 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
# ============================================================================
# Lambdas and Procs are Selfless
# Lambdas:
-> { puts self }.()
# main
# Procs:
(proc { puts self }).()
# main
# ============================================================================
# Classes Can Be Defined in a Lambda
def create_class
class C; end
end
# SyntaxError: (irb):103: class definition in method body
# class C; end
# ^
-> { class C; end }.()
# => nil
> C.new
# => #<C:0x007ffe728ccf08>
# ============================================================================
# Transform Chains
tripler = ->(n) { 3 * n }
squarer = ->(n) { n * n }
transfomers = [tripler, squarer]
start_val = 4
transfomers.inject(start_val) do |value, transform|
transform.(value)
end # 144
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment