Ruby Language
Blokken en Procs en Lambdas
Zoeken…
Syntaxis
- Proc. Nieuw ( blok )
- lambda {| args | code}
- -> (arg1, arg2) {code}
- object.to_proc
- {| single_arg | code}
- do | arg, (sleutel, waarde) | code einde
Opmerkingen
Wees voorzichtig met de prioriteit van de operator wanneer u een lijn hebt met meerdere geketende methoden, zoals:
str = "abcdefg"
puts str.gsub(/./) do |match|
rand(2).zero? ? match.upcase : match.downcase
end
In plaats van iets als abCDeFg
, zoals je zou verwachten, drukt het iets af als #<Enumerator:0x00000000af42b28>
- dit komt omdat do ... end
een lagere prioriteit heeft dan methoden, wat betekent dat gsub
alleen het /./
argument ziet en niet het blokargument. Het geeft een teller terug. De blokeinden doorgegeven wordt aan puts
, die negeert en gewoon toont het resultaat van gsub(/./)
.
Om dit op te lossen, gsub
u de gsub
aanroep tussen haakjes of gebruikt u { ... }
.
Proc
def call_the_block(&calling); calling.call; end
its_a = proc do |*args|
puts "It's a..." unless args.empty?
"beautiful day"
end
puts its_a #=> "beautiful day"
puts its_a.call #=> "beautiful day"
puts its_a[1, 2] #=> "It's a..." "beautiful day"
We hebben de methode call_the_block
uit het laatste voorbeeld gekopieerd. Hier kun je zien dat een proc wordt gemaakt door de proc
methode met een blok aan te roepen. Je kunt ook zien dat blokken, net als methoden, impliciete rendementen hebben, wat betekent dat procs (en lambdas) dat ook doen. In de definitie van its_a
kun je zien dat blokken zowel splat-argumenten als normale kunnen aannemen; ze zijn ook in staat om standaardargumenten aan te nemen, maar ik kon geen manier bedenken om dat in te werken. Ten slotte kun je zien dat het mogelijk is om meerdere syntaxis te gebruiken om een methode aan te roepen - de call
of de []
operator.
lambda
# lambda using the arrow syntax
hello_world = -> { 'Hello World!' }
hello_world[]
# 'Hello World!'
# lambda using the arrow syntax accepting 1 argument
hello_world = ->(name) { "Hello #{name}!" }
hello_world['Sven']
# "Hello Sven!"
the_thing = lambda do |magic, ohai, dere|
puts "magic! #{magic}"
puts "ohai #{dere}"
puts "#{ohai} means hello"
end
the_thing.call(1, 2, 3)
# magic! 1
# ohai 3
# 2 means hello
the_thing.call(1, 2)
# ArgumentError: wrong number of arguments (2 for 3)
the_thing[1, 2, 3, 4]
# ArgumentError: wrong number of arguments (4 for 3)
Je kunt ook ->
gebruiken om te creëren en .()
Om lambda aan te roepen
the_thing = ->(magic, ohai, dere) {
puts "magic! #{magic}"
puts "ohai #{dere}"
puts "#{ohai} means hello"
}
the_thing.(1, 2, 3)
# => magic! 1
# => ohai 3
# => 2 means hello
Hier kun je zien dat een lambda bijna hetzelfde is als een proc. Er zijn echter verschillende voorbehouden:
De arity van de argumenten van een lambda worden afgedwongen; het doorgeven van het verkeerde aantal argumenten aan een lambda, zal een
ArgumentError
. Ze kunnen nog steeds standaardparameters, splatparameters, enz. Hebben.return
vanuit een lambda keert terug van de lambda, terwijlreturn
uit een proc uit de omsluitende scope terugkeert:def try_proc x = Proc.new { return # Return from try_proc } x.call puts "After x.call" # this line is never reached end def try_lambda y = -> { return # return from y } y.call puts "After y.call" # this line is not skipped end try_proc # No output try_lambda # Outputs "After y.call"
Objecten als blokargumenten voor methoden
Als u een &
(ampersand) voor een argument plaatst, wordt dit doorgegeven als het blok van de methode. Objecten worden geconverteerd naar een Proc
met behulp van de to_proc
methode.
class Greeter
def to_proc
Proc.new do |item|
puts "Hello, #{item}"
end
end
end
greet = Greeter.new
%w(world life).each(&greet)
Dit is een gebruikelijk patroon in Ruby en veel standaardklassen bieden dit.
Symbol
implementeert bijvoorbeeld to_proc
door zichzelf naar het argument te sturen:
# Example implementation
class Symbol
def to_proc
Proc.new do |receiver|
receiver.send self
end
end
end
Dit maakt het handige &:symbol
idioom mogelijk, vaak gebruikt met Enumerable
objecten:
letter_counts = %w(just some words).map(&:length) # [4, 4, 5]
Blocks
Blokken zijn stukjes code ingesloten tussen accolades {}
(meestal voor blokken met één regel) of do..end
(gebruikt voor blokken met meerdere regels).
5.times { puts "Hello world" } # recommended style for single line blocks
5.times do
print "Hello "
puts "world"
end # recommended style for multi-line blocks
5.times {
print "hello "
puts "world" } # does not throw an error but is not recommended
Opmerking: accolades hebben een hogere prioriteit dan do..end
opbrengst
Blokken kunnen binnen methoden en functies worden gebruikt met behulp van het woord yield
:
def block_caller
puts "some code"
yield
puts "other code"
end
block_caller { puts "My own block" } # the block is passed as an argument to the method.
#some code
#My own block
#other code
Wees echter voorzichtig als yield
zonder een blok wordt aangeroepen, dit een LocalJumpError
zal LocalJumpError
. Voor dit doel biedt ruby een andere methode genaamd block_given?
hiermee kunt u controleren of een blok is gepasseerd voordat yield wordt aangeroepen
def block_caller
puts "some code"
if block_given?
yield
else
puts "default"
end
puts "other code"
end
block_caller
# some code
# default
# other code
block_caller { puts "not defaulted"}
# some code
# not defaulted
# other code
yield
kan ook argumenten aan het blok bieden
def yield_n(n)
p = yield n if block_given?
p || n
end
yield_n(12) {|n| n + 7 }
#=> 19
yield_n(4)
#=> 4
Hoewel dit een eenvoudig voorbeeld is, kan yield
erg handig zijn om directe toegang tot variabelen van variabelen of evaluaties binnen de context van een ander object mogelijk te maken. Bijvoorbeeld:
class Application
def configuration
@configuration ||= Configuration.new
block_given? ? yield(@configuration) : @configuration
end
end
class Configuration; end
app = Application.new
app.configuration do |config|
puts config.class.name
end
# Configuration
#=> nil
app.configuration
#=> #<Configuration:0x2bf1d30>
Zoals je kunt zien, maakt de yield
op deze manier de code leesbaarder dan het continu oproepen van app.configuration.#method_name
. In plaats daarvan kunt u alle configuraties binnen het blok uitvoeren en de code erin houden.
Variabelen
Variabelen voor blokken zijn lokaal voor het blok (vergelijkbaar met de variabelen van functies), ze sterven wanneer het blok wordt uitgevoerd.
my_variable = 8
3.times do |x|
my_variable = x
puts my_variable
end
puts my_variable
#=> 0
# 1
# 2
# 8
Blokken kunnen niet worden opgeslagen, ze sterven als ze worden uitgevoerd. Om de blokken op te slaan moet je gebruiken procs
en lambdas
.
Converteren naar Proc
Objecten die reageren op to_proc
kunnen worden geconverteerd naar procs met de operator &
(waardoor ze ook als blokken kunnen worden doorgegeven).
De klasse Symbol definieert #to_proc
dus het probeert de overeenkomstige methode aan te roepen voor het object dat het als parameter ontvangt.
p [ 'rabbit', 'grass' ].map( &:upcase ) # => ["RABBIT", "GRASS"]
#to_proc
definiëren ook #to_proc
.
output = method( :p )
[ 'rabbit', 'grass' ].map( &output ) # => "rabbit\ngrass"
Gedeeltelijke toepassing en curry
Technisch gezien heeft Ruby geen functies, maar methoden. Een Ruby-methode gedraagt zich echter vrijwel identiek aan functies in een andere taal:
def double(n)
n * 2
end
Deze normale methode / functie neemt een parameter n
, verdubbelt deze en retourneert de waarde. Laten we nu een hogere orde functie (of methode) definiëren:
def triple(n)
lambda {3 * n}
end
In plaats van een getal terug te geven, geeft triple
een methode terug. Je kunt het testen met de Interactive Ruby Shell :
$ irb --simple-prompt
>> def double(n)
>> n * 2
>> end
=> :double
>> def triple(n)
>> lambda {3 * n}
>> end
=> :triple
>> double(2)
=> 4
>> triple(2)
=> #<Proc:0x007fd07f07bdc0@(irb):7 (lambda)>
Als je het drievoudige nummer wilt krijgen, moet je de lambda bellen (of "verkleinen"):
triple_two = triple(2)
triple_two.call # => 6
Of korter:
triple(2).call
Curry- en gedeeltelijke toepassingen
Dit is niet handig in termen van het definiëren van zeer eenvoudige functionaliteit, maar het is handig als u methoden / functies wilt die niet onmiddellijk worden aangeroepen of verlaagd. Laten we bijvoorbeeld zeggen dat u methoden wilt definiëren die een nummer toevoegen met een specifiek nummer (bijvoorbeeld add_one(2) = 3
). Als je een heleboel hiervan zou moeten definiëren, zou je kunnen doen:
def add_one(n)
n + 1
end
def add_two(n)
n + 2
end
U kunt dit echter ook doen:
add = -> (a, b) { a + b }
add_one = add.curry.(1)
add_two = add.curry.(2)
Met behulp van lambda-calculus kunnen we zeggen dat add
is (λa.(λb.(a+b)))
. Currying is een manier om add
gedeeltelijk toe te passen . Dus add.curry.(1)
, is (λa.(λb.(a+b)))(1)
die kan worden gereduceerd tot (λb.(1+b))
. Gedeeltelijke toepassing betekent dat we het ene argument hebben toegevoegd om add
te add
maar het andere argument hebben achtergelaten om later te worden verstrekt. De output is een gespecialiseerde methode.
Handiger voorbeelden van curry
Laten we zeggen dat we een hele grote algemene formule hebben, dat als we er bepaalde argumenten voor opgeven, we er specifieke formules uit kunnen halen. Overweeg deze formule:
f(x, y, z) = sin(x\*y)*sin(y\*z)*sin(z\*x)
Deze formule is gemaakt om in drie dimensies te werken, maar laten we zeggen dat we deze formule alleen met betrekking tot y en z willen. Laten we ook zeggen dat om x te negeren, we de waarde ervan op pi / 2 willen instellen. Laten we eerst de algemene formule maken:
f = ->(x, y, z) {Math.sin(x*y) * Math.sin(y*z) * Math.sin(z*x)}
Laten we nu currying gebruiken om onze yz
formule te krijgen:
f_yz = f.curry.(Math::PI/2)
Om vervolgens de in f_yz
opgeslagen lambda aan te f_yz
:
f_xy.call(some_value_x, some_value_y)
Dit is vrij eenvoudig, maar laten we zeggen dat we de formule voor xz
willen krijgen. Hoe kunnen we y
op Math::PI/2
als dit niet het laatste argument is? Nou, het is een beetje ingewikkelder:
f_xz = -> (x,z) {f.curry.(x, Math::PI/2, z)}
In dit geval moeten we tijdelijke aanduidingen opgeven voor de parameter die we niet vooraf invullen. Voor de consistentie kunnen we f_xy
als volgt schrijven:
f_xy = -> (x,y) {f.curry.(x, y, Math::PI/2)}
Zo werkt de lambda-calculus voor f_yz
:
f = (λx.(λy.(λz.(sin(x*y) * sin(y*z) * sin(z*x))))
f_yz = (λx.(λy.(λz.(sin(x*y) * sin(y*z) * sin(z*x)))) (π/2) # Reduce =>
f_yz = (λy.(λz.(sin((π/2)*y) * sin(y*z) * sin(z*(π/2))))
Laten we nu eens kijken naar f_xz
f = (λx.(λy.(λz.(sin(x*y) * sin(y*z) * sin(z*x))))
f_xz = (λx.(λy.(λz.(sin(x*y) * sin(y*z) * sin(z*x)))) (λt.t) (π/2) # Reduce =>
f_xz = (λt.(λz.(sin(t*(π/2)) * sin((π/2)*z) * sin(z*t))))
Probeer dit voor meer informatie over lambda-calculus.