C # против Ruby
Эти два по сути одно и то же? Они очень похожи на меня.
Они очень разные.
Во-первых, лямбды в C # выполняют две очень разные вещи, только одна из которых имеет эквивалент в Ruby. (И этот эквивалент, сюрприз, лямбды, а не блоки.)
В C # литералы лямбда-выражения перегружены. (Интересно, что, насколько я знаю, это только перегруженные литералы *1013*). И они перегружены своим типом результата . (Опять же, они являются only вещью в C #, которая может быть перегружена в ее типе результата, методы могут быть перегружены только в их типах аргумента.)
C # литералы лямбда-выражения могут или быть анонимным фрагментом исполняемого кода или абстрактным представлением анонимного фрагмента исполняемого кода, в зависимости от того, является ли их тип результата: Func
/ Action
или Expression
.
Ruby не имеет эквивалента для последней функциональности (ну, есть специфичные для интерпретатора непереносимые нестандартизированные расширения). И эквивалент для прежней функциональности - лямбда, а не блок.
Синтаксис Ruby для лямбды очень похож на C #:
->(x, y) { x + y } # Ruby
(x, y) => { return x + y; } // C#
В C # вы можете отбросить return
, точку с запятой и фигурные скобки, если в качестве тела используется только одно выражение:
->(x, y) { x + y } # Ruby
(x, y) => x + y // C#
Вы можете оставить скобки, если у вас есть только один параметр:
-> x { x } # Ruby
x => x // C#
В Ruby вы можете пропустить список параметров, если он пуст:
-> { 42 } # Ruby
() => 42 // C#
Альтернативой использованию литерального лямбда-синтаксиса в Ruby является передача аргумента блока методу Kernel#lambda
:
->(x, y) { x + y }
lambda {|x, y| x + y } # same thing
Основное различие между этими двумя заключается в том, что вы не знаете, что делает lambda
, поскольку он может быть переопределен, перезаписан, упакован или изменен иным образом, тогда как поведение литералов не может быть изменено в Ruby.
В Ruby 1.8 вы также можете использовать Kernel#proc
, хотя вам, вероятно, следует избегать этого, так как этот метод делает что-то другое в 1.9.
Еще одно различие между Ruby и C # заключается в синтаксисе , вызывающем лямбду:
l.() # Ruby
l() // C#
т.е. в C # вы используете тот же синтаксис для вызова лямбды, который вы использовали бы для вызова чего-либо еще, тогда как в Ruby синтаксис для вызова метода отличается от синтаксиса для вызова любого другого типа вызываемого объекта.
Другое отличие состоит в том, что в C # * * * * ()
встроено в язык и доступно только для определенных встроенных типов, таких как методы, делегаты, Action
s и Func
s, тогда как в Ruby .()
просто синтаксический сахар для .call()
и, таким образом, можно заставить работать с любым объектом, просто реализовав метод call
.
проц против лямбды
Итак, что же являются лямбдами? Ну, это экземпляры класса Proc
. За исключением небольшого осложнения: на самом деле есть два разных типа экземпляров класса Proc
, которые немного различаются. (ИМХО, класс Proc
должен быть разбит на два класса для двух разных типов объектов.)
В частности, не все Proc
являются лямбдами. Вы можете проверить, является ли Proc
лямбда-выражением, вызвав метод Proc#lambda?
. (Обычно принято называть лямбду Proc
s "лямбдами", а не лямбда Proc
s - просто "процами".)
Не лямбда-проки создаются путем передачи блока в Proc.new
или Kernel#proc
. Однако обратите внимание, что до Ruby 1.9 Kernel#proc
создает lambda , а не proc.
Какая разница? По сути, лямбды ведут себя больше как методы, а проки - как блоки.
Если вы следили за некоторыми обсуждениями в списках рассылки Project Lambda для Java 8, возможно, вы столкнулись с проблемой того, что не совсем понятно, как нелокальный поток управления должен вести себя с лямбдами.В частности, существует три возможных разумных поведения для return
(ну, три возможных , но только два действительно разумные ) в лямбде:
- возврат из лямбды
- возврат из метода, из которого лямбда была вызвана
- возврат из метода, в котором лямбда была создана
Последний из них немного сомнительный, поскольку в общем случае метод вернет уже , но два других имеют смысл, и ни один не является более правильным или более очевидным, чем другой.Текущее состояние Project Lambda для Java 8 состоит в том, что они используют два разных ключевых слова (return
и yield
).Ruby использует два различных вида Proc
s:
- возвращение процедур из вызывающего метода (точно так же, как блоки)
- лямбда-возвращение из лямбды (так же, как методы)
Они также отличаются тем, как они обрабатывают привязку аргумента.Опять же, лямбды ведут себя больше как методы, а проки ведут себя больше как блоки:
- вы можете передать в прок больше аргументов, чем есть параметров, и в этом случае лишние аргументы будут игнорироваться
- вы можете передать меньше аргументов в процесс, чем есть параметры, и в этом случае избыточные параметры будут связаны с
nil
- , если вы передадите single аргумент, который является
Array
(или реагирует на to_ary
), и процедура имеет несколько параметров, массив будет распакован, а элементы привязаны к параметрам (точно так же, как и в случае назначения деструктурирования)
Блоки: lightweight procs
Блок - это по сути легкий процесс. Каждый метод в Ruby имеет ровно один параметр блока , который фактически не отображается в списке параметров (подробнее об этом позже), то есть неявно.Это означает, что в каждый метод вызов вы можете передать аргумент блока, независимо от того, ожидает метод метод или нет.
Поскольку блок не отображается в параметреВ списке нет имени, которое вы можете использовать для ссылки на него.Итак, как вы используете это?Ну, единственные две вещи, которые вы можете сделать (не совсем, но об этом позже), это вызов неявно через ключевое слово yield
и проверка, был ли блок передан через block_given?
.(Поскольку имени нет, вы не можете использовать методы call
или nil?
. Как бы вы их называли?)
Большинство реализаций Ruby реализуют блоки очень легковесным способом.В частности, они на самом деле не реализуют их как объекты.Однако, поскольку у них нет имени, вы не можете ссылаться на них, поэтому на самом деле невозможно определить, являются ли они объектами или нет.Вы можете просто думать о них как о процах, что облегчает эту задачу, поскольку есть одна менее важная концепция, о которой следует помнить.Просто учтите тот факт, что они на самом деле не реализованы как блоки, как оптимизацию компилятора.
to_proc
и &
Там - это на самом деле способ ссылкик блоку: &
сигил / модификатор / унарный префиксный оператор.Он может появляться только в списках параметров и списках аргументов.
В списке параметров это означает " обернуть неявный блок в процесс и связать его с этимназвание".В списке аргументов это означает « развернуть this Proc
в блок».
def foo(&bar)
end
Внутри метода bar
теперь связан собъект proc, представляющий блок.Это означает, например, что вы можете сохранить его в переменной экземпляра для последующего использования.
baz(&quux)
В этом случае baz
на самом деле является методом, который принимает нулевые аргументы.Но, конечно, он принимает неявный аргумент блока, который принимают все методы Ruby.Мы передаем содержимое переменной quux
, но сначала развернем его в блок.
Эта «развертка» на самом деле работает не только для Proc
с. &
сначала вызывает to_proc
для объекта, чтобы преобразовать его в процесс. Таким образом, любой объект может быть преобразован в блок.
Наиболее широко используемый пример - Symbol#to_proc
, который впервые появился где-то в конце 90-х, я полагаю. Он стал популярным, когда его добавили в ActiveSupport, откуда он распространился на Facets и другие библиотеки расширений. Наконец, он был добавлен в базовую библиотеку Ruby 1.9 и перенесен в 1.8.7. Все довольно просто:
class Symbol
def to_proc
->(recv, *args) { recv.send self, *args }
end
end
%w[Hello StackOverflow].map(&:length) # => [5, 13]
Или, если вы интерпретируете классы как функции для создания объектов, вы можете сделать что-то вроде этого:
class Class
def to_proc
-> *args { new *args }
end
end
[1, 2, 3].map(&Array) # => [[nil], [nil, nil], [nil, nil, nil]]
Method
с и UnboundMethod
с
Другим классом, представляющим часть исполняемого кода, является класс Method
. Method
объекты являются реципиентами для методов. Вы можете создать объект Method
, вызвав Object#method
для любого объекта и передав имя метода, который вы хотите изменить:
m = 'Hello'.method(:length)
m.() #=> 5
или с помощью оператора ссылки метода .:
:
m = 'Hello'.:length
m.() #=> 5
Method
s отвечают на to_proc
, поэтому вы можете передавать их везде, где можете пройти блок:
[1, 2, 3].each(&method(:puts))
# 1
# 2
# 3
UnboundMethod
- это прокси для метода, который еще не был привязан к получателю, то есть для метода, для которого self
еще не определен. Вы не можете вызвать UnboundMethod
, но вы можете bind
его объекту (который должен быть экземпляром модуля, из которого вы получили метод), который преобразует его в Method
.
UnboundMethod
объекты создаются путем вызова одного из методов семейства Module#instance_method
, передавая имя метода в качестве аргумента.
u = String.instance_method(:length)
u.()
# NoMethodError: undefined method `call' for #<UnboundMethod: String#length>
u.bind(42)
# TypeError: bind argument must be an instance of String
u.bind('Hello').() # => 5
Обобщенные вызываемые объекты
Как я уже намекнул выше: ничего особенного в Proc
с и Method
с нет. Любой объект , который отвечает на call
, может быть вызван, а любой объект , который отвечает на to_proc
, может быть преобразован в Proc
и, таким образом, развернут в блок и передан в метод, который ожидает блок.
История
Лямбда-выражение позаимствовало свою идею у Руби?
Вероятно, нет. Большинство современных языков программирования имеют некоторую форму анонимного буквального блока кода: Lisp (1958), Scheme, Smalltalk (1974), Perl, Python, ECMAScript, Ruby, Scala, Haskell, C ++, D, Objective-C, даже PHP (! ). И, конечно, вся идея восходит к «лямбда-калькуле» Алонзо Черча (1935 и даже раньше).