Статьи

Методы функционального программирования с Ruby: часть I

Ruby — интересный язык, так как он поддерживает использование нескольких парадигм. Одним из них является «функциональная парадигма».

Особенности функциональных языков

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

  • Неизменные значения : после установки «переменной» ее нельзя изменить. В Ruby это означает, что вам нужно обрабатывать переменные как константы.
  • Никаких побочных эффектов : при передаче заданного значения функция всегда должна возвращать один и тот же результат. Это идет рука об руку с наличием неизменных значений; функция никогда не может принимать значение и изменять его, так как это может вызвать побочный эффект, который является косвенным для возврата результата.
  • Функции высшего порядка : это функции, которые допускают функции в качестве аргументов или используют функции в качестве возвращаемого значения. Это, пожалуй, одна из самых важных особенностей любого функционального языка.
  • Curry : включается функциями высшего порядка, каррирование превращает функцию, которая принимает несколько аргументов, в функцию, которая принимает один аргумент. Это идет рука об руку с частичным применением функции, которая преобразует функцию с несколькими аргументами в функцию, которая принимает меньше аргументов, чем изначально.
  • Рекурсия : цикл, вызывая функцию изнутри себя. Когда у вас нет доступа к изменяемым данным, рекурсия используется для построения и создания цепочки данных. Это связано с тем, что зацикливание не является функциональной концепцией, так как требует передачи переменных для хранения состояния цикла в данный момент времени.
  • Ленивая оценка или отложенная оценка : задержка обработки значений до момента, когда это действительно необходимо. Если, например, у вас есть код, который сгенерировал список чисел Фибоначчи с включенной отложенной оценкой, он фактически не будет обрабатываться и вычисляться до тех пор, пока одно из значений в результате не потребуется другой функции, такой как puts

Использование Ruby как функционального языка

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

Я должен заранее указать на предупреждение: использование некоторых методов, которые мы рассмотрим в рабочем коде, не рекомендуется. Я постараюсь, когда смогу, четко указать, какие это особенности и почему они плохие. Дело в том, что Ruby по сути своей является языком OO для мира OO. Код функционального стиля может быть легко написан на Ruby, но в основном он страдает от проблем с производительностью из-за того, что не фокусируется на парадигме.

Подробнее об этом позже; а пока давайте поговорим о теме: неизменность.

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

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

Что такое свободный от побочных эффектов код и почему он полезен для меня?

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

 x = x + 1

Если мы применим правила математики к этому, мы сможем сбалансировать уравнение:

 x - x = 10 = 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