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, terwijl return 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.



Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow