зачем передавать аргументы блока функции в ruby? - PullRequest
0 голосов
/ 07 мая 2020

Мне непонятно, зачем нужно передавать аргументы блока при вызове функции. почему бы просто не передать в качестве аргументов функции и что происходит с аргументами блока, как они передаются и используются?

m.call(somevalue) {|_k, v| v['abc'] = 'xyz'}

module m 
  def call ( arg1, *arg2, &arg3)

  end
end

1 Ответ

2 голосов
/ 07 мая 2020

Ruby, как и почти все основные языки программирования, является строгим языком, что означает, что аргументы полностью оцениваются перед передачей в метод.

Теперь представьте, что вы хотите реализовать (упрощенная версия) Integer#times. Реализация будет выглядеть примерно так:

class Integer
  def my_times(action_to_be_executed)
    raise ArgumentError, "`self` must be non-negative but is `#{inspect}`" if negative?
    return if zero?

    action_to_be_executed

    pred.my_times(action_to_be_executed)
  end
end

3.my_times(puts "Hello")
# Hello

0.my_times(puts "Hello")
# Hello

-1.my_times(puts "Hello")
# Hello
# ArgumentError (`self` must be non-negative but is `-1`)

Как видите, 3.my_times(puts "Hello") напечатал Hello ровно один раз, а не трижды, как должно быть. Кроме того, 0.my_times(puts "Hello") напечатал Hello ровно один раз, а не совсем, как должен, несмотря на то, что return s во второй строке метода, и, следовательно, action_to_be_executed никогда даже не вычисляется. Даже -1.my_times(puts "Hello") напечатал Hello ровно один раз, несмотря на тот факт, что это raise s исключение ArgumentError как самое первое в методе и, следовательно, все остальное тела метода никогда не оценивается.

Почему? Потому что Ruby строгий! Опять же, strict означает, что аргументы полностью оцениваются перед передачей. Итак, это означает, что перед my_times даже вызывается , вычисляется puts "Hello" (которое выводит Hello в стандартный выходной поток) и результат из эта оценка (которая равна nil, потому что Kernel#puts всегда возвращает nil) передается в метод.

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

Итак, мы берем страницу из плейбука Java и определяем Протокол единого абстрактного метода : аргумент, передаваемый в my_each, должен быть объектом, реализующим метод с определенным c именем. Назовем его call, потому что, ну, мы собираемся называть его.

Это будет выглядеть примерно так:

class Integer
  def my_times(action_to_be_executed)
    raise ArgumentError, "`self` must be non-negative but is `#{inspect}`" if negative?
    return if zero?

    action_to_be_executed.call

    pred.my_times(action_to_be_executed)
  end
end

def (hello = Object.new).call
  puts "Hello"
end

3.my_times(hello)
# Hello
# Hello
# Hello

0.my_times(hello)

-1.my_times(hello)
# ArgumentError (`self` must be non-negative but is `-1`)

Отлично! Оно работает! Переданный аргумент, конечно, все еще строго оценивается перед передачей (мы не можем изменить фундаментальную природу Ruby из самого Ruby), , но эта оценка приводит только к объекту, который связан с локальной переменной hello. Код, который мы хотим запустить, представляет собой еще один уровень косвенного обращения и будет выполняться только в той точке, где мы его вызываем.

У него также есть еще одно преимущество: Integer#times фактически делает индекс текущей итерации доступно для действия в качестве аргумента. Это было невозможно реализовать с нашим первым решением, но здесь мы можем это сделать, потому что мы используем метод, и методы могут принимать аргументы:

class Integer
  def my_times(action_to_be_executed)
    raise ArgumentError, "`self` must be non-negative but is `#{inspect}`" if negative?

    __my_times_helper(action_to_be_executed)
  end

  protected

  def __my_times_helper(action_to_be_executed, index = 0)
    return if zero?

    action_to_be_executed.call(index)

    pred.__my_times_helper(action_to_be_executed, index + 1)
  end
end

def (hello = Object.new).call(i)
  puts "Hello from iteration #{i}"
end

3.my_times(hello)
# Hello from iteration 0
# Hello from iteration 1
# Hello from iteration 2

0.my_times(hello)

-1.my_times(hello)
# ArgumentError (`self` must be non-negative but is `-1`)

Однако на самом деле это не очень удобно для чтения. Если бы вы не хотели давать имя этому действию, которое мы пытаемся передать, а вместо этого просто записали его в списке аргументов, это выглядело бы примерно так:

3.my_times(Object.new.tap do |obj|
  def obj.call(i)
    puts "Hello from iteration #{i}"
  end
end)
# Hello from iteration 0
# Hello from iteration 1
# Hello from iteration 2

или на одном строка:

3.my_times(Object.new.tap do |obj| def obj.call; puts "Hello from iteration #{i}" end end)
# Hello from iteration 0
# Hello from iteration 1
# Hello from iteration 2

# or:

3.my_times(Object.new.tap {|obj| def obj.call; puts "Hello from iteration #{i}" end })
# Hello from iteration 0
# Hello from iteration 1
# Hello from iteration 2

Я не знаю как вы, но я считаю это довольно уродливым.

В Ruby 1.9, Ruby добавлено Proc литералы aka stabby lambda literals для языка. Лямбда-литералы - это краткий буквальный синтаксис для записи объектов с помощью метода call, в частности Proc объектов с Proc#call.

Использование лямбда-литералов и без каких-либо изменений в нашем существующем коде это выглядит примерно так:

3.my_times(-> i { puts "Hello from iteration #{i}" })
# Hello from iteration 0
# Hello from iteration 1
# Hello from iteration 2

Это неплохо!

Когда Юкихиро «мац» разработал Мацумото Ruby почти тридцать лет go в начале 1993 года он провел обзор основных библиотек и стандартных библиотек таких языков, как Smalltalk, Scheme и Common Lisp, чтобы выяснить, как такие методы, которые принимают фрагмент кода в качестве аргумента, на самом деле и он обнаружил, что подавляющее большинство таких методов принимают ровно один аргумент кода , и все, что они делают с этим аргументом, - это вызывают его.

Итак, он решил чтобы добавить поддержку специального языка для с одним аргументом , который содержит код и может вызываться только . Этот аргумент является как синтаксически, так и семантически легким, в частности, он синтаксически выглядит точно так же, как любая другая структура управления, и семантически не является объектом. блоки .

Каждый метод в Ruby имеет необязательный параметр блока. Я могу всегда передать блок методу. Что-либо делать с блоком зависит от метода. Здесь, например, блок бесполезен, потому что Kernel#puts ничего не делает с блоком:

puts("Hello") { puts "from the block" }
# Hello

Поскольку блоки не являются объектами, вы не можете вызывать для них методы. Кроме того, поскольку может быть только один аргумент блока, нет необходимости давать ему имя: если вы ссылаетесь на блок, всегда ясно , какой блок , потому что может быть только один. Но если у блока нет методов и имени, как мы можем его назвать?

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

С блоками наше решение будет выглядеть так:

class Integer
  def my_times(&action_to_be_executed)
    raise ArgumentError, "`self` must be non-negative but is `#{inspect}`" if negative?
    return enum_for(__callee__) unless block_given?

    __my_times_helper(&action_to_be_executed)
  end

  protected

  def __my_times_helper(&action_to_be_executed, index = 0)
    return if zero?

    yield index

    pred.__my_times_helper(&action_to_be_executed, index + 1)
  end
end

3.my_times do 
  puts "Hello from iteration #{i}"
end
# Hello from iteration 0
# Hello from iteration 1
# Hello from iteration 2

0.my_times do 
  puts "Hello from iteration #{i}"
end

-1.my_times do 
  puts "Hello from iteration #{i}"
end
# ArgumentError (`self` must be non-negative but is `-1`)

Хорошо, вы могли заметить, что я немного упростил, когда написал выше, что единственное, что вы можете делать с блоком, - это вызывать его. С ним можно делать еще две вещи:

  1. Вы можете проверить, был ли передан аргумент блока, используя Kernel#block_given?. Поскольку блоки всегда являются необязательными, а у блоков нет имен, должен быть способ проверить, был ли блок передан или нет.

  2. Вы можете «свернуть» блок (что является не объект и не имеет имени) в объект Proc (который является объектом) и привяжите его к параметру (который дает ему имя) с помощью унарного префикса амперсанда & sigil в списке параметров метода. Теперь, когда у нас есть объект и способ ссылаться на него, мы можем сохранить его в переменной, вернуть его из метода или (как мы делаем здесь) передать его в качестве аргумента другому методу, который в противном случае было бы невозможно.

    Существует также обратная операция: с помощью оператора унарного префикса амперсанда & вы можете «развернуть» объект Proc в блок в списке аргументов; благодаря этому метод ведет себя так, как если бы вы передали методу код, который хранится внутри Proc, как буквальный аргумент блока.

И вот он! Для этого и нужны блоки: семантически и синтаксически облегченная форма передачи кода методу.

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

Например, Smalltalk фактически не имеет условного выражения if / then / else, на самом деле Smalltalk не имеет управляющих структур вообще . Все делается методами. Таким образом, условное выражение работает так: каждый из двух логических классов TrueClass и FalseClass имеет метод с именем ifTrue:ifFalse:, который принимает два аргумента блока, и две реализации будут просто оценивать первый или второй блок. . Например, реализация в TrueClass может выглядеть примерно так (обратите внимание, что Smalltalk не имеет синтаксиса для классов или методов, вместо этого классы и методы создаются в среде IDE путем создания объектов классов и объектов методов через GUI) :

True>>ifTrue: trueBlock ifFalse: falseBlock
   "Answer with the value of `trueBlock`."

   ↑trueBlock value

Соответствующая реализация в FalseClass тогда будет выглядеть так:

FalseClass>>ifTrue: trueBlock ifFalse: falseBlock
   "Answer with the value of `falseBlock`."

   ↑falseBlock value

И вы бы назвали это так:

2 < 3 ifTrue: [ Transcript show: 'yes' ] ifFalse: [ Transcript show: 'no' ].
"yes"

4 < 3 ifTrue: [ Transcript show: 'yes' ] ifFalse: [ Transcript show: 'no' ].
"no"

В ECMAScript вы можете просто использовать определения функций как выражения, а также есть облегченный синтаксис для функций.

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

Scala имеет параметры вызова по имени , которые оцениваются только тогда, когда вы используете их имя, и они оцениваются каждый раз, когда вы используете их имя. Это будет выглядеть примерно так:

implicit class IntegerTimes(val i: Int) extends AnyVal {
  @scala.annotation.tailrec
  def times(actionToBeExecuted: => Unit): Unit = {
    if (i < 0) throw new Error()
    if (i == 0) () else { actionToBeExecuted; (i - 1).times(actionToBeExecuted) }
  }
}

3.times { println("Hello") }
// Hello
// Hello
// Hello
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...