Last active
March 5, 2020 21:03
-
-
Save keithrbennett/927c989cc301c63d35a8ceec9f3f9404 to your computer and use it in GitHub Desktop.
Text and code for Functional Programming in Ruby presentation.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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