Ruby — интересный язык, так как он поддерживает использование нескольких парадигм. Одним из них является «функциональная парадигма».
Особенности функциональных языков
Использование языка в функциональном стиле подразумевает доступ к нескольким ключевым функциям:
- Неизменные значения : после установки «переменной» ее нельзя изменить. В Ruby это означает, что вам нужно обрабатывать переменные как константы.
- Никаких побочных эффектов : при передаче заданного значения функция всегда должна возвращать один и тот же результат. Это идет рука об руку с наличием неизменных значений; функция никогда не может принимать значение и изменять его, так как это может вызвать побочный эффект, который является косвенным для возврата результата.
- Функции высшего порядка : это функции, которые допускают функции в качестве аргументов или используют функции в качестве возвращаемого значения. Это, пожалуй, одна из самых важных особенностей любого функционального языка.
- Curry : включается функциями высшего порядка, каррирование превращает функцию, которая принимает несколько аргументов, в функцию, которая принимает один аргумент. Это идет рука об руку с частичным применением функции, которая преобразует функцию с несколькими аргументами в функцию, которая принимает меньше аргументов, чем изначально.
- Рекурсия : цикл, вызывая функцию изнутри себя. Когда у вас нет доступа к изменяемым данным, рекурсия используется для построения и создания цепочки данных. Это связано с тем, что зацикливание не является функциональной концепцией, так как требует передачи переменных для хранения состояния цикла в данный момент времени.
- Ленивая оценка или отложенная оценка : задержка обработки значений до момента, когда это действительно необходимо. Если, например, у вас есть код, который сгенерировал список чисел Фибоначчи с включенной отложенной оценкой, он фактически не будет обрабатываться и вычисляться до тех пор, пока одно из значений в результате не потребуется другой функции, такой как
puts
Использование Ruby как функционального языка
В этой серии мы рассмотрим каждую из этих функций по очереди и рассмотрим, как их можно использовать для создания более чистого, более эффективного и более структурированного кода Ruby.
Я должен заранее указать на предупреждение: использование некоторых методов, которые мы рассмотрим в рабочем коде, не рекомендуется. Я постараюсь, когда смогу, четко указать, какие это особенности и почему они плохие. Дело в том, что Ruby по сути своей является языком OO для мира OO. Код функционального стиля может быть легко написан на Ruby, но в основном он страдает от проблем с производительностью из-за того, что не фокусируется на парадигме.
Подробнее об этом позже; а пока давайте поговорим о теме: неизменность.
Неизменность и свободный от побочных эффектов код
Чтобы начать серию, давайте рассмотрим код, не изменяемый и не имеющий побочных эффектов. Чистые функциональные языки не допускают изменяемых данных, и в результате функции не могут ничего делать, кроме как принимать аргументы и выдавать результат, используя эти аргументы.
Что такое свободный от побочных эффектов код и почему он полезен для меня?
Функциональное программирование — это парадигма, основанная главным образом на принципах, согласно которым программирование должно следовать правилам математики. Давайте возьмем следующее правильное выражение парадигмы императивного программирования:
x = x + 1
Если мы применим правила математики к этому, мы сможем сбалансировать уравнение:
x - x = 1
∴ 0 = 1 (!?)
Вау, что там произошло ?! Понятно, что это не работает. Императивный стиль программирования не соответствует математической основе функционального программирования. Математические функции построены на идее неизменности; то есть значения не могут быть переопределены так, как мы это сделали в приведенном выше императивном утверждении. Мы можем бороться с этой проблемой, используя новую константу для представления обновленного значения:
x = 1
y = x + 1
Теперь это совершенно верно в обеих парадигмах:
y = x + 1
∴ x = y - 1
Парадигма функционального программирования, основанная на математике, использует эту основную идею в своей основе. При использовании Ruby для методов функционального программирования, и вы хотите избежать побочных эффектов, вы должны стараться никогда не переопределять переменную; то есть обрабатывать переменные как константы. У этого подхода есть множество преимуществ, некоторые из которых:
- Функции или методы легко проверяются модулем. Это связано с тем, что, если они никогда не изменяют данные, а лишь предоставляют копии или новые объекты в качестве возвращаемых значений, один и тот же вход гарантированно всегда будет давать один и тот же результат. Таким образом, можно сказать, что код «не имеет побочных эффектов».
- Ваш код становится невероятно лаконичным и читабельным. Поскольку вам придется создавать новую переменную для каждой необходимой вам мутации, на первый взгляд будет очевидно, что масштабируемость и недостатки конкретного метода очевидны. Чистые функциональные языки часто демонстрируют простую способность рассуждать о производительности данного блока кода как функции.
- Цепочка становится невероятно легкой, главным образом потому, что каждая функция возвращает новое неизменное значение, которое можно без беспокойства передавать в следующую и отбрасывать.
- Ваши функции будут свободны от домена; Вы можете применять их бесконечным количеством способов, так как внутри них нет скрытой логики.
Написание свободного кода с побочными эффектами
Разбор номера версии
В 2010 году Яхуда Кац разместил простой, но интересный вопрос в Твиттере 1 :
Вопрос: какой самый простой способ получить [«X :: Y :: Z», «X :: Y», «X»] из «X :: Y :: Z» в Ruby?
Конечно, есть десятки решений этой проблемы, но решение ее в функциональном стиле оказалось слишком хорошей приманкой для Ruby Quicktips Tumblr 2 :
def module_split(module_path, separator = "::")
modules = module_path.split(separator)
modules.length.downto(1).map { |n| modules.first(n).join(separator) }
end
module_split("W::X::Y::Z")
Есть много преимуществ написания этого метода в функциональном стиле с нулевыми побочными эффектами:
- Легко тестируется с помощью модульных тестов.
- Использует как можно больше основных методов Ruby, уменьшая вероятность появления ошибок и повторного изобретения колеса.
- Достаточно быстро. Там, где это возможно, используется запоминание, а в других местах оптимизированы основные методы Ruby.
- Чистый, читаемый, легко понимаемый; это практически читается как обычный английский.
Свободные DSL с цепными методами
Одним из основных преимуществ использования функционального стиля является то, что он обычно делает цепочку кусочком пирога. В этом примере мы попытаемся создать DSL для создания блока кода CSS; мы создадим селектор, а также свойства и значения для установки стилей.
Таким образом, нам нужен класс с двумя методами: один для добавления свойств и один для установки селектора.
class CssBlock
# We'll add some methods in here.
end
Сначала мы установим селектор как обязательное свойство в инициализаторе. Мы также настроим атрибут properties как пустой Hash
class CssBlock
attr_reader :selector, :properties
def initialize(selector, properties = {})
@selector = selector.dup.freeze
@properties = properties.dup.freeze
end
end
Обратите внимание, что мы использовали attr_reader
Теперь мы можем гарантировать, что переданные значения не могут быть изменены.
Далее мы создаем код для добавления некоторых свойств в блок:
class CssBlock
# ...
def set(key, value = nil)
new_properties = if key.is_a?(Hash)
key
elsif !value.nil?
{
key => value
}
else
raise "Either provide a Hash of values, or a key and value."
end
self.class.new(self.selector, self.properties.merge(new_properties))
end
end
Ключевые выводы из этого блока кода:
- Мы принимаем
Hash
- Мы возвращаем новый экземпляр класса
CssBlock
- Мы выдаем ошибку, если они не дали нам правильный формат элементов.
- Мы не добавляем, не редактируем и не изменяем любое состояние, хранящееся в копии объекта, с которым мы имеем дело. Как только объект создан, вот и все; это исправлено, как это было.
Теперь давайте добавим метод для сериализации содержимого этого класса, который в основном используется для определения метода #to_s
class CssBlock
# ...
def to_s
serialised_properties = self.properties.inject([]) do |acc, (k, v)|
acc + ["#{k}: #{v}"]
end
"#{self.selector} { #{serialised_properties.join("; ") } }"
end
end
Мы используем #inject
Одна очень важная мелочь здесь заключается в том, что мы не изменяем переменную acc
Как эквивалент Ruby функции fold/foldr
#inject
Теперь, чтобы проверить это:
CssBlock.new("#some_id .class a").set("color", "#FFF").set({ "color" => "#000", "text-decoration" => "underline"}) # => "#some_id .class a { color: #000; text-decoration: underline }"
Huzzah, это работает! Мало того, это очень просто, легко тестируется и должно быть очень легко работать или расширяться по мере необходимости.
Для вашего удобства вот полный исходный код:
class CssBlock
attr_reader :selector, :properties
def initialize(selector, properties = {})
@selector = selector.dup.freeze
@properties = properties.dup.freeze
end
def set(key, value = nil)
new_properties = if key.is_a?(Hash)
key
elsif !value.nil?
{
key => value
}
else
raise "Either provide a Hash of values, or a key and value."
end
self.class.new(self.selector, self.properties.merge(new_properties))
end
def to_s
serialised_properties = self.properties.inject([]) do |acc, (k, v)|
acc + ["#{k}: #{v}"]
end
"#{self.selector} { #{serialised_properties.join("; ") } }"
end
end
Предостережения о побочных эффектах в Ruby
Реальность попытки написать свободный от побочных эффектов код на Ruby состоит в том, что он на самом деле не работает для чего-то другого, кроме экспериментов на данном этапе. Грубая правда в том, что у Ruby есть несколько серьезных проблем с обработкой правильно неизменяемых данных:
- Замораживание объектов не может быть автоматизировано. Вы можете получить часть пути, обезьяна исправляя
#new_method
#new method
Это означает, что вам придется вручную заморозить объекты, если вы хотите, чтобы они были принудительно неизменными; если вы этого не сделаете, вы не можете гарантировать, что на самом деле пишете код без побочных эффектов. - Высокая стоимость создания объектов в Ruby означает, что использование функционального стиля программирования может оказаться дорогостоящим для любых целей, связанных с производительностью. Надеемся, что эта проблема исчезнет, так как реализации Ruby требуют более быстрых интерпретаторов.
- Одно из ключевых преимуществ функционального программирования, более простой параллелизм, трудно достичь в Ruby из-за наличия GIL и наивной реализации потоков. В альтернативных реализациях Ruby, таких как JRuby и Rubinius, это не такая большая проблема. Несмотря на то, что Ruby 1.9 добился больших успехов в улучшении модели потоков с помощью Fibers и тому подобного, он по-прежнему затруднит полное изучение этой области преимуществ по сравнению с целевым функциональным языком, поскольку в CRuby по-прежнему нет возможности иметь надлежащие параллельные потоки. ,
Хотя эти методы не обязательно полезны в производственном коде, их чрезвычайно полезно учитывать при разработке надежных объектных моделей.
В следующий раз…
Мы поговорим о функциях высшего порядка и о том, как они позволяют создавать карри, очень интересный и очень полезный подход к программированию.
1 : http://twitter.com/wycats/status/9042964562
2 : http://rubyquicktips.com/post/1018776470/embracing-functional-programming