В Ruby есть несколько конструкций, используемых для объединения повторяемого кода: методы, блоки, процедуры и лямбда-выражения. Хотя ко всем этим применяется перекрывающееся поведение, каждый тип имеет уникальную оптимизацию и использование.

Однако в JavaScript есть только один тип функции с множеством ролей.

Как они соотносятся друг с другом? Я намеревался расширить свое понимание.

Во-первых, давайте взглянем на….

Функции JavaScript

Функции в JavaScript - это блоки повторяемого кода, инкапсулированные в объект. Они могут определять параметры, которые устанавливаются как имена локальных переменных внутри функции, принимать значения этих параметров в качестве аргументов при вызове и возвращать значения в результате своей процедуры.

Функции могут быть анонимными, одноразовыми и объявляться как часть другого выражения:

[1, 2, 3, 4].map(function(number) {
  return number * number
})
// [ 1, 4, 9, 16 ]

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

function square(number) { return number * number }
[1, 2, 3, 4].map(square)
// [ 1, 4, 9, 16 ]

Функции являются первоклассными гражданами и могут быть присвоены переменным, сохранены в структурах данных и переданы в / возвращены из других функций. Функции также являются замыканиями: они создают свою собственную внутреннюю область видимости, но имеют доступ ко всем переменным в области видимости во время их определения, даже если эта область больше не существует.

function make_divider(divisor) {
  var words = "your answer is: ";
  return function(dividend) {
    return words + (dividend / divisor);
  }
}
var quarter = make_divider(4)
var third = make_divider(3)
quarter(100)
// 'your answer is: 25'
third(60)
// 'your answer is: 20'

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

var john_cena = {
  catch_phrases: ["The champ is here!", "Hustle, Loyalty, Respect", "U Cant See Me"],
  speak: function() {
    var length = this.catch_phrases.length;
    return this.catch_phrases[Math.floor(Math.random() * length)];
  }
}
john_cena.speak();
// 'Hustle, Loyalty, Respect'
john_cena.speak();
// "The champ is here!"
speak();
// ReferenceError: speak is not defined

Выше ключевое слово this относится к текущему контексту, то есть к объекту john_cena. Функция speak случайным образом выбирает из массива, назначенного свойству с именем catch_phrases в текущем контексте.

Теперь - даже функции, определенные вне любого объекта, неявно сохраняются как свойства глобального объекта, поэтому они тоже могут ссылаться на текущий контекст с помощью this.

this
// { DTRACE_NET_SERVER_CONNECTION: [Function],
// DTRACE_NET_STREAM_END: [Function],
// many many lines of global object properties
function what_is_this() {
  console.log(this)
}
what_is_this()
// { DTRACE_NET_SERVER_CONNECTION: [Function],
// DTRACE_NET_STREAM_END: [Function],
// many many lines of global object properties

Контекст JS-функции определяется во время выполнения, а не при определении функции. Наш метод можно вызывать в других контекстах с помощью метода call, передавая объект контекста в качестве аргумента.

var hulk_hogan = {
  catch_phrases: ["Whatcha gonna do brother??", "Let me tell ya something Mean Gene.."]
}
var ric_flair = {
  catch_phrases: ["Woooo", "To be the man, you've got to beat the man"]
}
var catch_phrases = ["Who let the dogs out?", "Keepin' it real"]
john_cena.speak.call(ric_flair)
// "Woooo"
var talk = john_cena.speak
talk.call(hulk_hogan)
// "Whatcha gonna do brother??"
talk()  // no context specified - defaults to global context
// "Who let the dogs out?"
what_is_this.call(hulk_hogan)
// { catch_phrases: 
// [ 'Whatcha gonna do brother??',
//   'Let me tell ya something Mean Gene..' ] }

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

var hulk_up = talk.bind(hulk_hogan)
hulk_up()
// "Let me tell ya something Mean Gene.."
hulk_up()
// "Whatcha gonna do brother??"
hulk_up.call(ric_flair)
// "Whatcha gonna do brother??" ---- bound function cannot have its context changed

Понятия контекста и привязки (или их отсутствия) важны для понимания, как мы увидим, методы Ruby действительно сохраняют привязку к своему родительскому объекту.

Чтобы уточнить и подытожить:

контекст: объект, для которого вызывалась функция. Ключевое слово this относится к текущему контексту выполнения. Контекст определяется тем, как вызывается функция, и по умолчанию используется глобальный объект, если контекст не указан.

Функции в JS берут на себя множество ролей - анонимные функции, методы, обратные вызовы, фабричные функции, конструкторы объектов ... список можно продолжить, и поначалу он может быть довольно запутанным. Давайте переключимся и посмотрим, как Ruby разбивает эти роли на разные типы.

Методы Ruby

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

Воссоздавая наш пример сверху:

class Wrestler
  attr_accessor :catch_phrases
  def initialize(catch_phrases)
    @catch_phrases = catch_phrases
  end
  
  def speak
    @catch_phrases.sample
  end
end
hulk_hogan = Wrestler.new(["Whatcha gonna do brother??", "Let me tell ya something Mean Gene.."])
ric_flair = Wrestler.new(["Woooo", "To be the man, you've got to beat the man"])
ric_flair.speak
// "Woooo"
hulk_hogan.speak
// "Whatcha gonna do brother??"

Методы в Ruby не являются объектами, но могут быть заключены в объект, вызвав метод Object #, передав символ, представляющий имя метода. Результирующий объект метода может быть вызван с помощью метода # call.

call в JS и Ruby не являются синонимами. Вызов метода № - это то, как вызываются объекты метода, а аргументы перечисляются после вызова в круглых скобках.

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

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

hulk_up = hulk_hogan.method(:speak)
hulkup.call
// "Whatcha gonna do brother??"

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

Методы в Ruby всегда вызываются для получающего объекта. Все объекты являются экземплярами класса, а методы определены в классе объекта.

Другими словами, методы - это сообщения, отправляемые объекту, который отвечает, просматривая имя метода в своей поисковой цепочке и выполняя первое найденное совпадение. Self внутри метода - это объект, который получает сообщение, ищет метод и выполняет код. Все это соответствует тому, что Ruby является чистым объектно-ориентированным языком на основе классов.

Мы можем имитировать явный контекст выполнения функции JavaScript с помощью несвязанных методов.

unbound = hulk_up.unbind
//  => #<UnboundMethod: Wrestler#speak> 
unbound.bind(ric_flair).call
// => "To be the man, you've got to beat the man"

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

Блоки

Блок в Ruby - это фрагмент кода, заключенный в фигурные скобки или ключевые слова do..end. Это синтаксис, а не объект. Блоки обнаруживаются только сразу после вызова метода, и только один единственный блок может быть передан в метод. Доступ к этому блоку можно получить с помощью двух ключевых слов внутри метода:

  • block_given? проверяет наличие блока
  • yield дает управление блоку, необязательно передавая ему аргументы.

Два простых примера:

class Example
  def takes_implicit_block
    x = 1
    puts "self in the method is #{self}"
    puts "x in the method is #{x}"
    puts "block has #{block_given? ? 'been' : 'not been'} given" 
    yield "hello from the method" if block_given?
  end
end
x = 1000
my_example = Example.new
my_example.takes_implicit_block
// self in the method is #<Example:0x007f8642a3f230>
// x in the method is 1
// block has not been given
// => nil
my_example.takes_implicit_block do |saying, ignored_argument|
  puts "self in the block is #{self}"
  puts "method has yielded: #{saying}"
  puts "x in the block is #{x}"
end
// self in the method is #<Example:0x007f8642a3f230>
// x in the method is 1
// block has been given
// self in the block is main
// method has yielded: hello from the method
// x in the block is 1000
// => nil

Выше мы видим пару ключевых особенностей блоков.

  • Методы могут быть разработаны так, чтобы требовать блоки, принимать блоки, если они заданы, или полностью игнорировать их в зависимости от использования yield и block_given?
  • Блоки являются замыканиями и несут в себе переменные в области видимости во время их определения.
  • Методы могут передавать аргументы блоку через ключевое слово yield. Соответствующие параметры определяются между вертикальными чертами и разделяются запятыми в начале блока.
  • Отсутствующие аргументы устанавливаются равными нулю, лишние аргументы игнорируются.

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

wrestlers = [john_cena, ric_flair, hulk_hogan]
wrestlers.map { |wrestler| wrestler.speak }
=> ["U Can't See Me", "To be the man, you've got to beat the man", "Let me tell ya something Mean Gene.."]
wrestlers.select { |wrestler| wrestler.catch_phrases.size == 3 }
=> [#<Wrestler:0x007fb3f410dd58 @catch_phrases=["The champ is here!", "U Can't See Me", "Hustle, Loyalty, Respect"]>]

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

Procs и амперсанд

Proc - это объектный тип Ruby, который завершает блок. Он может определять параметры, принимать аргументы, возвращаемые значения, сохраняться в переменной и передаваться / возвращаться из других типов функций. Процедуры вызываются с помощью вызова Proc #.

square = Proc.new { |num| num ** 2 }
square.call(10)
// 100
square.call(-3)
// 9

Procs являются замыканиями и могут обращаться к локальным переменным в области видимости во время их определения, даже после того, как эта область была возвращена из.

make_divider = Proc.new do |divisor|
  words = "your answer is: "
  Proc.new do |dividend|
    words + (dividend / divisor).to_s
  end
end
quarter = make_divider.call(4)
third = make_divider.call(3)
quarter.call(100)
// "your answer is: 25"
third.call(90)
// "your answer is: 30

Ранее мы определили метод, принимающий неявный блок, и продемонстрировали некоторые варианты поведения блока. Этот же метод может принимать объект Proc вместо блока:

x = 1001
my_proc = Proc.new do |saying|
  puts "self in the block is #{self}"
  puts "method has yielded: #{saying}"
  puts "x in the proc is #{x}"
end
my_example.takes_implicit_block(&my_proc)
// self in the method is #<Example:0x007f8642979e90>
// x in the method is 1
// block has been given
// self in the block is main
// method has yielded: hello from the method
// x in the proc is 1001
// => nil

Обратите внимание на то, что нашей процедуре непосредственно предшествует амперсанд и она передается в круглые скобки, как если бы она была аргументом метода. Как и в случае с нашим предыдущим блоком, это необычный аргумент. Методы Ruby строго подчиняются своей арности, и для этого метода не определены параметры. Так почему это работает?

Амперсанд говорит нам: «Используйте это как наш специальный« аргумент блока », доступный через yield и block_given?».

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

class Example
 def explicit_block_with_ampersand(&blok)
    puts "you passed in a #{blok.class} with id #{blok.object_id}"
    puts "block has #{block_given? ? 'been' : 'not been'} given" 
    blok.call('this is the proc being called')
    yield('this is the block being yielded to')
  end
end
P = Proc.new do |saying|
  puts saying
end
my_example.explicit_block_with_ampersand(&P)
// you passed in a Proc with id 70107309288260
// block has been given
// this is the proc being called
// this is the block being yielded to
// => nil

В объявлении нашего метода мы также используем амперсанд, чтобы сказать: «Это не обычный параметр, это имя, используемое для ссылки на специальный аргумент блока, поэтому yield и block_given? это можно увидеть. Если это блок - используйте его. Если это объект с префиксом амперсанда, вызовите для него to_proc и используйте его »

Итак ... мы также можем передать блок в метод с явным параметром block / proc.

my_example.explicit_block_with_ampersand { |saying| puts "#{saying}, Whoah" }
// you passed in a Proc with id 70107309187360
// block has been given
// this is the proc being called, Whoah
// this is the block being yielded to, Whoah

В приведенном выше вызове метода наш переданный блок - это реальный Proc, идентификатор объекта и все остальное.

Это действительно довольно гибко, на самом деле любой объект с определенным методом to_proc может быть передан в метод следующим образом:

class Fixnum
  def to_proc
    Proc.new {|i| self + i }
  end
end
[1,2,3,4,5].map(&20)
// [21, 22, 23, 24, 25]

Если мы хотим передать несколько процедур методу, мы можем отказаться от парадигмы «амперсанд / выход» и передать процедуры в качестве обычных аргументов.

class Example
  def takes_normal_arguments(*args)
    puts "you passed in #{args.size} arguments"
    puts "block has #{block_given? ? 'been' : 'not been'} given" 
    args.map(&:call)
  end
end
a = Proc.new { 'A' }
b = Proc.new { 'B' }
c = Proc.new { 'C' }
my_example.takes_normal_arguments(a, b, c)
// you passed in 3 arguments
// block has not been given
// => ["A", "B", "C"]

Чаще всего проще и элегантнее использовать блок, чем делать дополнительный шаг и определять Proc, особенно если он используется только один раз. Но, возможно, нам нужно динамически сгенерировать блок из фабричного метода на основе пользовательского ввода, или, может быть, у нас есть конкретный блок, который многократно используется в нашем приложении. Вот тут-то и пригодятся Procs.

Лямбды

Лямбды также являются объектами Proc, поэтому они разделяют все вышеперечисленное поведение и, как правило, могут использоваться взаимозаменяемо с Proc, за двумя основными исключениями:

  • Arity: Procs не строго проверяют количество переданных аргументов и даже окажут вам некоторые услуги, упаковывая / распаковывая аргументы в / из массивов для соответствия определенным параметрам. Лямбды строго подчиняются своей строгости и не делают вам никаких одолжений.
  • Операторы return в лямбда-выражениях возвращаются из самих лямбда-выражений, подобно тому, как функция JS или метод Ruby возвращаются сами по себе.
  • Операторы return в Procs возвращаются из области, в которой была определена процедура, - очень похоже на блоки. Эта область может не существовать во время выполнения или может быть объектом main верхнего уровня, из которого нельзя вернуть. Оба эти обстоятельства приводят к ошибке LocalJump.

Чаще всего мы будем полагаться на то, что наши функции неявно возвращают значение последнего выражения, в других случаях нам понадобится условный оператор раннего возврата. Имея как Procs, так и lambdas, у нас есть возможность выбрать, будет ли этот явный возврат завершать весь вызов метода, как блок, или только одну итерацию, как метод.

Готово

Я около года изучаю Ruby и примерно две недели назад легко плавал на территории объекта / метода / блока.

Для Ruby это было нормально - она ​​не возражала, если бы не думала о первоклассных функциях. Я все еще мог решать проблемы на работе, в школе и в личных проектах. Мац хочет, чтобы я чувствовал себя комфортно и счастлив, и я был и тем, и другим!

Функции Ruby подразделяются на четыре разных типа и специализируются на каждом конкретном случае использования. Это более сложный дизайн, но более простой в применении; легче читать, использовать и понимать. В отличие от этого, единичный тип функции JavaScript более мощный и гибкий, играя множество ролей в языке.

Эта статья явилась результатом тщательного исследования из множества (часто противоречивых!) Источников. Если что-то неясно или категорически не так, дайте мне знать в комментариях.

Спасибо за чтение!