Функциональное программирование на Ruby. Часть 1

Язык Ruby поддерживает несколько парадигм программирования: процедурную, объектно-ориентированную и функциональную . Этот цикл статей посвящен “функциональной парадигме”.

Концепции функционального программирования

Неизменяемые значения — значение переменной после объявления не может быть изменено. В Ruby так ведут себя константы.

Чистые функции — функции без побочных эффектов ввода-вывода и памяти. Для одного и того же набора параметров чистая функция всегда возвращает одинаковый результат.

Функции высшего порядка — функции, которые принимают другие функции как аргументы или возвращают функцию как результат.

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

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

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

Ruby как функциональный язык

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

Неизменяемость и отсутствие побочных эффектов

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

Применяя функциональный подход на Ruby, старайтесь не переопределять переменные, рассматривайте их как константы. Это поможет избежать побочных эффектов.

Основные преимущества такого подхода:

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

Простой DSL с цепочкой методов

Преимущества функционального подхода в простоте использования цепочек методов. Рассмотрим построение DSL для генерации CSS кода, который будет возвращать селектор со свойствами и значениями для его стилей.

Сначала создадим класс CssBlock. Добавим в конструктор селектор как обязательный атрибут, а свойства сделаем опциональными, с пустым хэшем по умолчанию.

class CssBlock
attr_reader :selector, :properties
  def initialize selector, properties={}
@selector = selector.dup.freeze
@properties = properties.dup.freeze
end
end

Мы использовали attr_reader для обоих свойств, методы dup и freeze, чтобы скопировать и заморозить значения. Так мы гарантируем, что передаваемые значения неизменяемы.

Напишем метод, добавляющий свойства блоку.

class CssBlock
  # ...
  def set(key, value=nil)
new_properties = if key.is_a?(Hash)
key
elsif !value.nil?
{ key: value }
else
raise "you need to provide a Hash of values, or a key and value."
end
    self.class.new(self.selector, self.properties.merge(new_properties))
end
end

Метод set

  • принимает на вход хэш или пару ключ/значение и добавляет их к уже имеющимся свойствам;
  • возвращает новый экземпляр класса CssBlock, добавляя к уже существующим свойствам новые;
  • если переданы неправильные параметры, выводит ошибку выполнения;
  • не добавляет, не редактирует и не изменяет состояние экземпляра объекта.

Добавим метод, сериализующий содержимое объекта. Для этого переопределим метод to_s.

class CssBlock
  # ...
  def to_s
serialized_properties = self.properties.inject([]) do |acc, (k, v)|
acc + ["#{k}: #{v}"]
end
    "#{self.selector} {#{serialized_properties.join("; ")}}"
end
end

Мы использовали inject для создания массива строковых свойств. Важно, что мы не меняем переменную acc в блоке. inject является основным методом создания объектов в функциональном стиле на Ruby. Он соответствует функциям fold/foldr в функциональных языках.

Теперь протестируем генератор:

CssBlock.new(".header").set("color", "#EF8D24").set({"text-transform" => "uppercase", "text-decoration" => "underline"})
=> ".header {key: #EF8D24; text-transform: uppercase; text-decoration: underline}"

Итоговый код класса:

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 "you need to 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
serialized_properties = self.properties.inject([]) do |acc, (k, v)|
acc + ["#{k}: #{v}"]
end
    "#{self.selector} {#{serialized_properties.join("; ")}}"
end
end

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

Замечание о функциональном стиле в Ruby

В Ruby есть несколько проблем при работе с неизменяемыми данными:

  • Отсутствие автоматической заморозки объектов. Для собственных классов можно определить заморозку объектов и значений в конструкторе, чтобы возвращать неизменяемое значение экземпляра. Но это не сработает при создании базовых объектов языка — Хэшей, Массивов, Строк и т.д. Их придется замораживать вручную, чтобы они оставались неизменными, иначе мы не сможем гарантировать отсутствие побочных эффектов в коде.
  • Создание объекта — ресурсоемкая операция, поэтому использование функционального стиля может сильно сказаться на производительности.
One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.