Блоки и доходности в Ruby

Я пытаюсь понять блоки и yield и то, как они работают в Ruby.

Как yield используется? Многие из рассмотренных мною приложений Rails используют yield странным образом.

Может ли кто-нибудь объяснить мне или показать, куда обратиться, чтобы их понять?


person Matt Elhotiby    schedule 18.06.2010    source источник
comment
Возможно, вас заинтересует ответ на функцию доходности Ruby в отношение к информатике. Хотя это несколько другой вопрос, чем ваш, он может пролить некоторый свет на этот вопрос.   -  person Ken Bloom    schedule 18.06.2010


Ответы (10)


Да, поначалу это немного озадачивает.

В Ruby методы могут получать блок кода для выполнения произвольных сегментов кода.

Когда метод ожидает блок, он вызывает его, вызывая функцию yield.

Это очень удобно, например, для перебора списка или для предоставления специального алгоритма.

Возьмем следующий пример:

Я собираюсь определить класс Person, инициализированный именем, и предоставить метод do_with_name, который при вызове просто передает атрибут name полученному блоку.

class Person 
    def initialize( name ) 
         @name = name
    end

    def do_with_name 
        yield( @name ) 
    end
end

Это позволит нам вызвать этот метод и передать произвольный блок кода.

Например, чтобы напечатать имя, мы сделаем:

person = Person.new("Oscar")

#invoking the method passing a block
person.do_with_name do |name|
    puts "Hey, his name is #{name}"
end

Напечатал бы:

Hey, his name is Oscar

Обратите внимание, блок получает в качестве параметра переменную с именем name (примечание: вы можете называть эту переменную как угодно, но имеет смысл называть ее name). Когда код вызывает yield, он заполняет этот параметр значением @name.

yield( @name )

Мы могли бы предоставить еще один блок для выполнения другого действия. Например, переверните имя:

#variable to hold the name reversed
reversed_name = ""

#invoke the method passing a different block
person.do_with_name do |name| 
    reversed_name = name.reverse
end

puts reversed_name

=> "racsO"

Мы использовали точно такой же метод (do_with_name) - просто другой блок.

Этот пример тривиален. Более интересные способы использования - фильтровать все элементы в массиве:

 days = ["monday", "tuesday", "wednesday", "thursday", "friday"]  

 # select those which start with 't' 
 days.select do | item |
     item.match /^t/
 end

=> ["tuesday", "thursday"]

Или мы также можем предоставить собственный алгоритм сортировки, например, на основе размера строки:

 days.sort do |x,y|
    x.size <=> y.size
 end

=> ["monday", "friday", "tuesday", "thursday", "wednesday"]

Надеюсь, это поможет вам лучше понять это.

Кстати, если блок является необязательным, вы должны называть его так:

yield(value) if block_given?

Если не является необязательным, просто вызовите его.

ИЗМЕНИТЬ

@hmak создал repl.it для этих примеров: https://repl.it/@makstaks/blocksandyieldsrubyexample

person OscarRyz    schedule 18.06.2010
comment
как он печатает racsO если the_name = "" - person Paritosh Piplewar; 21.11.2012
comment
Извините, имя - это переменная экземпляра, инициализированная с помощью "Oscar" (не очень понятно в ответе) - person OscarRyz; 22.11.2012
comment
А как насчет такого кода? person.do_with_name {|string| yield string, something_else } - person f.ardelian; 25.11.2012
comment
Итак, в терминах Javascripty, это стандартизированный способ передачи обратного вызова заданному методу и его вызова. Спасибо за объяснение! - person yitznewton; 23.04.2013
comment
В более общем смысле - блок - это усиленный рубином синтаксический сахар для паттерна Стратегия. потому что типичное использование - предоставить код для выполнения каких-либо действий в контексте другой операции. Но улучшения рубина открывают путь к таким интересным вещам, как написание DSL с использованием блока для передачи контекста. - person Roman Bulgakov; 25.05.2017
comment
Спасибо @OscarRyz, я добавил repl.it, чтобы помочь проиллюстрировать ваши замечательные примеры: repl.it/@makstaks/blocksandyieldsrubyexample - person makstaks; 03.03.2020
comment
@hmak офигенно, добавила ссылку на ответ - person OscarRyz; 03.03.2020
comment
Подправьте ссылку, пожалуйста, она 404 - person Nick M; 21.07.2021

В Ruby методы могут проверять, были ли они вызваны таким образом, чтобы в дополнение к обычным аргументам был предоставлен блок. Обычно это делается с помощью метода block_given?, но вы также можете ссылаться на блок как на явный Proc, добавив амперсанд (&) перед именем последнего аргумента.

Если метод вызывается с блоком, тогда метод может yield управлять блоком (вызывать блок) с некоторыми аргументами, если это необходимо. Рассмотрим этот пример метода, который демонстрирует:

def foo(x)
  puts "OK: called as foo(#{x.inspect})"
  yield("A gift from foo!") if block_given?
end

foo(10)
# OK: called as foo(10)
foo(123) {|y| puts "BLOCK: #{y} How nice =)"}
# OK: called as foo(123)
# BLOCK: A gift from foo! How nice =)

Или, используя специальный синтаксис аргумента блока:

def bar(x, &block)
  puts "OK: called as bar(#{x.inspect})"
  block.call("A gift from bar!") if block
end

bar(10)
# OK: called as bar(10)
bar(123) {|y| puts "BLOCK: #{y} How nice =)"}
# OK: called as bar(123)
# BLOCK: A gift from bar! How nice =)
person maerics    schedule 18.06.2010
comment
Полезно знать разные способы срабатывания блокировки. - person LPing; 25.06.2015

Вполне возможно, что здесь кто-то предоставит действительно подробный ответ, но я всегда находил этот пост Роберта Сосински, чтобы быть отличным объяснением тонкостей между блоками, процедурами и лямбдами.

Я должен добавить, что я считаю, что сообщение, на которое я ссылаюсь, относится к Ruby 1.8. Некоторые вещи изменились в Ruby 1.9, например, переменные блока являются локальными для блока. В 1.8 вы получите что-то вроде следующего:

>> a = "Hello"
=> "Hello"
>> 1.times { |a| a = "Goodbye" }
=> 1
>> a
=> "Goodbye"

В то время как 1.9 даст вам:

>> a = "Hello"
=> "Hello"
>> 1.times { |a| a = "Goodbye" }
=> 1
>> a
=> "Hello"

У меня нет 1.9 на этой машине, поэтому в приведенном выше может быть ошибка.

person theIV    schedule 18.06.2010
comment
Отличное описание в той статье, мне потребовались месяцы, чтобы разобраться в этом самостоятельно =) - person maerics; 18.06.2010
comment
Я согласен. Не думаю, что я знал половину объясненного материала, пока не прочитал его. - person theIV; 18.06.2010
comment
Обновленная ссылка теперь тоже 404. Вот Ссылка на Wayback Machine. - person klenwell; 07.07.2016
comment
@klenwell спасибо за внимание, я снова обновил ссылку. - person theIV; 07.07.2016

Я нашел эту статью очень полезной. В частности, следующий пример:

#!/usr/bin/ruby

def test
  yield 5
  puts "You are in the method test"
  yield 100
end

test {|i| puts "You are in the block #{i}"}

test do |i|
    puts "You are in the block #{i}"
end

который должен дать следующий результат:

You are in the block 5
You are in the method test
You are in the block 100
You are in the block 5
You are in the method test
You are in the block 100

Таким образом, каждый раз при вызове yield ruby ​​запускает код в блоке do или внутри {}. Если параметр предоставлен для yield, то он будет предоставлен как параметр для блока do.

Для меня это был первый раз, когда я действительно понял, что делают блоки do. По сути, это способ для функции предоставить доступ к внутренним структурам данных, будь то для итерации или для настройки функции.

Итак, когда в рельсах вы пишете следующее:

respond_to do |format|
  format.html { render template: "my/view", layout: 'my_layout' }
end

Это запустит функцию respond_to, которая выдаст блок do с (внутренним) параметром format. Затем вы вызываете функцию .html для этой внутренней переменной, которая, в свою очередь, дает блок кода для запуска команды render. Обратите внимание, что .html будет уступать только в том случае, если это запрошенный формат файла. (техническая сторона вопроса: эти функции фактически используют block.call, а не yield, как вы можете видеть из источника, но функциональность в основном та же, см. этот вопрос для обсуждения.) предоставляет возможность функции выполнить некоторую инициализацию, затем принять входные данные из вызывающего кода и, при необходимости, продолжить обработку.

Или, другими словами, это похоже на функцию, которая принимает анонимную функцию в качестве аргумента и затем вызывает ее в javascript.

person zelanix    schedule 14.05.2014

К и без того отличным ответам я хотел добавить, почему вы так поступаете.

Понятия не имею, с какого языка вы пришли, но если предположить, что это статический язык, такие вещи будут вам знакомы. Вот как вы читаете файл в java

public class FileInput {

  public static void main(String[] args) {

    File file = new File("C:\\MyFile.txt");
    FileInputStream fis = null;
    BufferedInputStream bis = null;
    DataInputStream dis = null;

    try {
      fis = new FileInputStream(file);

      // Here BufferedInputStream is added for fast reading.
      bis = new BufferedInputStream(fis);
      dis = new DataInputStream(bis);

      // dis.available() returns 0 if the file does not have more lines.
      while (dis.available() != 0) {

      // this statement reads the line from the file and print it to
        // the console.
        System.out.println(dis.readLine());
      }

      // dispose all the resources after using them.
      fis.close();
      bis.close();
      dis.close();

    } catch (FileNotFoundException e) {
      e.printStackTrace();
    } catch (IOException e) {
      e.printStackTrace();
    }
  }
}

Игнорируя всю цепочку потоков, идея состоит в следующем.

  1. Инициализировать ресурс, который нужно очистить
  2. использовать ресурс
  3. не забудьте очистить это

Вот как вы это делаете в рубине

File.open("readfile.rb", "r") do |infile|
    while (line = infile.gets)
        puts "#{counter}: #{line}"
        counter = counter + 1
    end
end

Совершенно иначе. Нарушение этого

  1. сообщить классу File, как инициализировать ресурс
  2. скажите классу файла, что с ним делать
  3. смейтесь над java-парнями, которые все еще печатают ;-)

Здесь вместо обработки первого и второго шагов вы в основном делегируете это другому классу. Как видите, это резко снижает объем кода, который вам нужно написать, что упрощает чтение и снижает вероятность того, что такие вещи, как утечки памяти или блокировки файлов, не будут сняты.

Это не значит, что вы не можете сделать что-то подобное в java, на самом деле люди делали это уже несколько десятилетий. Это называется шаблоном Стратегия. Разница в том, что без блоков, для чего-то простого, как пример файла, стратегия становится излишней из-за количества классов и методов, которые вам нужно написать. С блоками это настолько простой и элегантный способ сделать это, что нет никакого смысла НЕ структурировать свой код таким образом.

Это не единственный способ использования блоков, но другие (например, шаблон Builder, который вы можете увидеть в form_for api в рельсах) достаточно похожи, чтобы было очевидно, что происходит, когда вы обернетесь вокруг этого. Когда вы видите блоки, обычно можно с уверенностью предположить, что вызов метода - это то, что вы хотите сделать, и что блок описывает, как вы хотите это сделать.

person Matt Briggs    schedule 18.06.2010
comment
Давайте немного упростим это: File.readlines("readfile.rb").each_with_index do |line, index| puts "#{index + 1}: #{line}" end и посмеемся над Java-парнями еще сильнее. - person Michael Hampton; 29.12.2014
comment
@MichaelHampton, смейтесь, прочитав файл длиной в пару гигабайт. - person akostadinov; 17.02.2015
comment
@akostadinov Нет ... от этого мне хочется плакать! - person Michael Hampton; 17.02.2015
comment
@MichaelHampton Или, еще лучше: IO.foreach('readfile.rb').each_with_index { |line, index| puts "#{index}: #{line}" } (плюс нет проблем с памятью) - person Fund Monica's Lawsuit; 11.07.2015

В Ruby блок - это, по сути, кусок кода, который может быть передан и выполнен любым методом. Блоки всегда используются с методами, которые обычно передают им данные (в качестве аргументов).

Блоки широко используются в драгоценных камнях Ruby (включая Rails) и в хорошо написанном коде Ruby. Они не являются объектами, поэтому не могут быть присвоены переменным.

Базовый синтаксис

Блок - это фрагмент кода, заключенный в {} или do..end. По соглашению синтаксис фигурных скобок следует использовать для однострочных блоков, а синтаксис do..end - для многострочных блоков.

{ # This is a single line block }

do
  # This is a multi-line block
end 

Любой метод может получить блок как неявный аргумент. Блок выполняется оператором yield внутри метода. Базовый синтаксис:

def meditate
  print "Today we will practice zazen"
  yield # This indicates the method is expecting a block
end 

# We are passing a block as an argument to the meditate method
meditate { print " for 40 minutes." }

Output:
Today we will practice zazen for 40 minutes.

Когда достигается оператор yield, метод meditate передает управление блоку, код внутри блока выполняется, и управление возвращается методу, который возобновляет выполнение сразу же после оператора yield.

Когда метод содержит оператор yield, он ожидает получения блока во время вызова. Если блок не предоставлен, при достижении оператора yield будет выброшено исключение. Мы можем сделать блок необязательным и избежать возникновения исключения:

def meditate
  puts "Today we will practice zazen."
  yield if block_given? 
end meditate

Output:
Today we will practice zazen. 

Невозможно передать в метод несколько блоков. Каждый метод может получить только один блок.

См. Дополнительную информацию по адресу: http://www.zenruby.info/2016/04/introduction-to-blocks-in-ruby.html

person Community    schedule 01.05.2016
comment
Это (единственный) ответ, который действительно заставляет меня понять, что такое block и yield, и как их использовать. - person user218867; 22.05.2020

Я иногда использую "yield" вот так:

def add_to_http
   "http://#{yield}"
end

puts add_to_http { "www.example.com" }
puts add_to_http { "www.victim.com"}
person Samet Sazak    schedule 04.02.2015
comment
Хорошо, но почему ? Существует множество причин, например, одна Logger не должна выполнять какую-либо задачу, если пользователю это не нужно. Но ты должен объяснить свое ... - person Ulysse BN; 14.06.2018

Проще говоря, yield позволяет создаваемому вами методу принимать и вызывать блоки. Ключевое слово yield - это именно то место, где будет выполняться «материал» в блоке.

person ntarpey    schedule 02.12.2013

В отношении доходности я хотел бы остановиться на двух моментах. Во-первых, хотя многие ответы здесь говорят о различных способах передачи блока методу, который использует yield, давайте также поговорим о потоке управления. Это особенно актуально, так как вы можете передать блоку НЕСКОЛЬКО раз. Давайте посмотрим на пример:

class Fruit
  attr_accessor :kinds

  def initialize 
    @kinds = %w(orange apple pear banana)
  end

  def each 
    puts 'inside each'
    3.times { yield (@kinds.tap {|kinds| puts "selecting from #{kinds}"} ).sample }
  end  
end

f = Fruit.new
f.each do |kind|
  puts 'inside block'
end    

=> inside each
=> selecting from ["orange", "apple", "pear", "banana"]
=> inside block
=> selecting from ["orange", "apple", "pear", "banana"]
=> inside block
=> selecting from ["orange", "apple", "pear", "banana"]
=> inside block

Когда вызывается каждый метод, он выполняется построчно. Теперь, когда мы перейдем к блоку 3 раза, этот блок будет вызван 3 раза. Каждый раз он вызывает yield. Этот доход связан с блоком, связанным с методом, который вызвал каждый метод. Важно отметить, что каждый раз, когда вызывается yield, он возвращает управление блоку каждого метода в клиентском коде. Как только блок завершает выполнение, он возвращается к блоку 3 раза. И это происходит 3 раза. Таким образом, этот блок в клиентском коде вызывается в 3 отдельных случаях, поскольку yield явно вызывается 3 раза.

Второй момент касается enum_for и yield. enum_for создает экземпляр класса Enumerator, и этот объект Enumerator также отвечает на yield.

class Fruit
  def initialize
    @kinds = %w(orange apple)
  end

  def kinds
    yield @kinds.shift
    yield @kinds.shift
  end
end

f = Fruit.new
enum = f.to_enum(:kinds)
enum.next
 => "orange" 
enum.next
 => "apple" 

Обратите внимание: каждый раз, когда мы вызываем типы с помощью внешнего итератора, он вызывает yield только один раз. В следующий раз, когда мы его вызовем, он вызовет следующий yield и так далее.

В отношении enum_for есть интересный лакомый кусочек. В онлайн-документации указано следующее:

enum_for(method = :each, *args) → enum
Creates a new Enumerator which will enumerate by calling method on obj, passing args if any.

str = "xyz"
enum = str.enum_for(:each_byte)
enum.each { |b| puts b }    
# => 120
# => 121
# => 122

Если вы не укажете символ в качестве аргумента enum_for, ruby ​​подключит перечислитель к каждому методу получателя. В некоторых классах нет метода each, например в классе String.

str = "I like fruit"
enum = str.to_enum
enum.next
=> NoMethodError: undefined method `each' for "I like fruit":String

Таким образом, в случае некоторых объектов, вызываемых с помощью enum_for, вы должны четко указать, каким будет ваш метод перечисления.

person Donato    schedule 29.10.2018

Yield можно использовать как безымянный блок для возврата значения в методе. Рассмотрим следующий код:

Def Up(anarg)
  yield(anarg)
end

Вы можете создать метод «Вверх», которому назначен один аргумент. Теперь вы можете присвоить этот аргумент yield, который будет вызывать и выполнять связанный блок. Вы можете назначить блок после списка параметров.

Up("Here is a string"){|x| x.reverse!; puts(x)}

Когда метод Up вызывает yield с аргументом, он передается переменной блока для обработки запроса.

person gkstr1    schedule 21.09.2016