В первой части этой серии мы рассмотрели основы функционального программирования и подробно остановились на неизменяемости и отсутствии побочных эффектов кода.
Сегодня мы рассмотрим функции высшего порядка и карри, две удивительно полезные функциональные возможности стилей, которые Ruby поддерживает с возвышенным классом. Для начала давайте рассмотрим различные типы функций в Ruby и их особенности.
Методы, блоки, процедуры и лямбды
Как вы, наверное, знаете, Ruby поддерживает множество различных типов функций. Блоки, Procs, Methods и Lambdas — это всего лишь небольшие отклонения этих типов в Ruby. Нюансы, которые разделяют каждый из них, — это то, что заставляет большинство новичков в этой «перегрузке» функций в Ruby поднимать руки в отчаянии. Не сдавайся, это проще, чем ты думаешь!
Начнем с методов, которые мы все знаем и любим. Вы используете их постоянно, их легко и быстро объявить, и они помогают нам использовать ранние принципы программирования на основе подпрограмм:
def some_method # ... end
Что часто приукрашивается методами в Ruby, так это то, что они не являются строго «функциями первого класса» (функциями, которые можно передавать как объекты). Этот код не работает:
def some_method | |
def return_function | |
end | |
end | |
some_method # => nil | |
x = some_method | |
x() # => NoMethodError: undefined method `x’ for main:Object |
Хотя мы ожидаем получить в качестве возвращаемого значения метод return_function
который мы определили с помощью вызова some_method
, вы можете видеть, что это не так. Увы, похоже, что методы в Ruby не являются объектами . И, таким образом, мы обнаруживаем, почему блоки, пробы и лямбды так полезны.
Блоки — это, безусловно, самый распространенный способ передачи ссылок на функции в Ruby. Они открывают двери для некоторых классных DSL, делая их очень простыми и удобочитаемыми для передачи функций в качестве аргументов:
def some_method | |
yield if block_given? | |
end | |
some_method do | |
puts 1 + 1 | |
end # => 2 |
На первый взгляд может показаться, что блоки являются такими же ограниченными, как и методы, в том смысле, что они могут использоваться только для ограниченного применения, передаваемого в функцию. Прорыв происходит, когда вы понимаете, что за кулисами Блок на самом деле просто замаскированный Proc. Фактически, блоки — это просто особый синтаксический сахар в Ruby для создания Procs. Вы можете видеть это на практике, когда вы используете унарный оператор амперсанда для получения доступа к блоку в качестве аргумента в методе:
def some_method(&block) | |
block.class | |
end | |
some_method do | |
end # => Proc |
Вы также можете увидеть это в действии, когда заметите, что можете передавать экземпляры Proc вместо блока в метод приема блоков (вы должны использовать унарный оператор амперсанда перед Proc, чтобы Ruby знал, что вы передаете его как Блок а не как обычный аргумент):
def some_method | |
yield | |
end | |
some_method &proc { puts «hello world!» } |
Возвращаясь на мгновение к методам, они фактически похожи на блоки в одном главном отношении: они являются синтаксическим сахаром для некоторой вариации базового процесса. С этой целью Ruby действительно дает нам способ получить метод, который мы определили как экземпляр класса Method, поэтому мы можем получить ссылку на него, которую мы можем обойти:
def some_method | |
end | |
method :some_method # => #<Method: Object#some_method> |
Этот Объект Метода снова действует точно так же, как Proc, так что, шокируйте, мы можем передать это в методы приема блоков!
def hello | |
yield | |
end | |
def hi | |
«hi there» | |
end | |
hello &method(:hi) |
Последний оставшийся тип функции, который будет обсуждаться — это лямбда-выражения. Что может добавить Lambdas к этой безумной мешанине различных типов функций в Ruby? Ну, опять же, лямбды — это всего лишь проки, но с двумя небольшими, но чрезвычайно важными отличиями
- Лямбды проверяют полученные аргументы, как методы. Проц не делает. Это означает, что если вы передадите только один аргумент в Lambda, который принимает два аргумента, вы получите
ArgumentError
. Если вы сделаете то же самое с Proc, он просто слепо примет те, что вы дали, и установит остальные аргументыnil
. - Любые операторы
return
используемые в Proc, также будут возвращаться из метода, вызвавшего этот Proc. Лямбда, с другой стороны, не будет. Это означает, что вы можете вызывать лямбда-выражение, получать его возвращаемое значение и обрабатывать его в одном методе.
Итак, краткий обзор того, на что мы смотрели до сих пор:
- Все функции в Ruby действуют или могут быть настроены, как какой-то вариант Proc.
- Блоки действительно просто синтаксический сахар для Procs.
- Лямбды похожи на Procs, но с более строгой передачей аргументов и локализованным
returns
. - Определенные методы могут быть получены как объекты метода с помощью
Kernel#method
методаKernel#method
. - Используйте унарный оператор
&
чтобы указать, когда вы передаете Procs / Lambdas / Methods как блок, и не включайте оператор, когда вы передаете их как обычный аргумент.
Теперь, чтобы использовать эти функции!
Функции высшего порядка
Концепция функций высшего порядка на самом деле удивительно проста для понимания. Они определяются как функции, которые выполняют одно или несколько из следующих действий:
- Принять функцию в качестве аргумента.
- Вернуть функцию в качестве возвращаемого значения.
Теперь вы, вероятно, думаете: «Я могу представить тысячу методов в Ruby, которые делают один из них», и вы были бы правы! Функции высшего порядка в Ruby есть практически везде; они являются одной из самых распространенных функциональных возможностей Ruby.
Давайте запустим один из наиболее известных методов в Ruby, который принимает функцию в качестве аргумента:
words = [«foo», «bar», «baz», «pibb»] | |
words.map do |word| | |
«mr « + word | |
end # => [«mr foo», «mr bar», «mr baz», «mr pibb»] |
Поразительное количество основных методов в Ruby, которые поставляются из коробки, являются функциями высшего порядка, так как многие из них используют блоки. Такие методы, как map
, inject
и each
являются яркими примерами.
Что касается функций, которые возвращают функции, они не так часто встречаются в базовой библиотеке Ruby, но это не значит, что у вас нет всех инструментов, чтобы сделать это самостоятельно!
def adder(a, b) | |
lambda { a + b } | |
end | |
adder_fn = adder(1, 2) | |
adder_fn.call # => 3 |
Поскольку нам нужно вернуть эту анонимную функцию, мы используем здесь лямбду, но нет причины, по которой вы не могли бы также использовать Proc. Но что в действительности все это использует, и как вы можете достойно использовать это в своих программах?
Преимущества принятия функций в качестве аргументов для других функций наглядно демонстрируется огромным количеством примеров в базовой библиотеке Ruby, поэтому мы пока остановимся на этом стиле функций высшего порядка. Однако оказывается, что второе определение функций более высокого порядка (возможность возвращать функции из других функций) открывает двери для двух удивительно полезных методов: «частичное применение функции», и это более известный кузен «карри».
Применение функций высшего порядка: частичное применение и каррирование
В Ruby 1.9 класс Proc получил один новый и чрезвычайно полезный метод: #curry
. Это приносит частичное применение функции и карри в Ruby. Возможно, вы сталкивались с каррированием раньше, вероятно, при использовании таких языков, как JavaScript, где первоклассная поддержка функций распространена, а карри — относительно недооцененная и практичная техника.
Давайте сначала разберемся, каковы эти два разных применения функций. Частичное применение функции и карри определяются следующим образом:
- Частичная аппликация функции вызывает функцию с некоторым количеством аргументов, чтобы вернуть функцию, которая будет принимать на столько меньше аргументов.
- Карринг принимает функцию, которая принимает
n
аргументов, и разбивает ее наn
функций, которые принимают один аргумент.
Чтобы дать вам более четкое представление о том, что каждая из этих двух функций будет выполнять, давайте рассмотрим пример Proc:
proc { |x, y, z| x + y + z }
Частичное применение этой функции вернет, если мы передадим первые два аргумента, следующие вложенные Procs:
proc { |x, y| proc { |z| x + y + z} }
С другой стороны, каррирование этой функции вернет следующие вложенные Procs:
proc { |x| proc { |y| proc { |z| x + y + z} } }
Обратите внимание, что вы можете передавать только один аргумент за раз в результат функции с карри, но передавайте столько, сколько хотите, за один раз при использовании частичного приложения. Это основной принцип, который определяет эти два приложения. Метод Proc#curry
в Ruby позволяет выполнять оба этих приложения.
Некоторые примеры для того, чтобы правильно объяснить это. Допустим, у нас есть следующие методы, определенные в нашем приложении:
def add(a, b) | |
a + b | |
end | |
def subtract(a, b) | |
a — b | |
end | |
def multiply(a, b) | |
a * b | |
end | |
def divide(a, b) | |
a / b | |
end |
Если посмотреть на это с точки зрения попытки высушить этот код, то здесь что-то серьезно не так. Все эти функции принимают абсолютно одинаковые аргументы, единственное их отличие — это функция, которую они используют в аргументе a
. Может быть, тогда мы сможем высушить это?
def apply_math(fn, a, b) | |
a.send(fn, b) | |
end | |
apply_math(:+, 1, 2) # => 3 |
Но теперь мы должны каждый раз записывать это имя функции ?! Это не кажется намного лучше.
Proc#curry
на помощь!
Что если бы мы могли создать некоторые вспомогательные функции, которые исправят первый аргумент #apply_math
и позволят нам вызывать наши функции, используя имена методов, которые мы использовали ранее?
apply_math = lambda do |fn, a, b| | |
a.send(fn, b) | |
end | |
add = apply_math.curry.(:+) | |
subtract = apply_math.curry.(:-) | |
multiply = apply_math.curry.(:*) | |
divide = apply_math.curry.(:/) | |
add.(1, 2) # => 3 |
Это не только позволило нам высушить наш код, но и открыло двери для интересных вещей в будущем. Что если мы хотим создать приращение на одну функцию? Легко, как пирог, всего с одним дополнительным вызовом Proc#curry
:
increment = add.curry.(1) | |
# Calculate the answer to life, the universe, and everything. | |
increment.(41) # => 42 |
Функциональные языки, такие как Haskell, имеют каррирование и частичное применение функций, встроенные прямо в язык. Эта функция чрезвычайно полезна и может помочь вам высушить код и сохранить функции на минимальном уровне. Побочным эффектом такой работы с функциями является то, что ваш код также становится намного проще для тестирования. Поскольку вы разбили функции на более мелкие повторяющиеся сегменты, чтобы изобразить их, вы можете проверить каждую функцию только на то, что они добавляют поверх существующих функций, которые они каррируют; это поможет вам писать более простой, понятный, более лаконичный код и тесты, а также гораздо более многократно используемый код.
В следующий раз…
Поговорим о рекурсии и лени-оценке! Это основные элементы функционального программирования, и они снова доступны для использования во многих скрытых формах в Ruby.