Статьи

Функциональное программирование в Ruby: объекты-значения

Функциональное программирование

Функциональное программирование существует уже некоторое время. Маккарти представлял LISP в 1950-х годах, но можно даже утверждать, что Lambda Calculus, математическое устройство для рассуждений о вычислениях, предшествовавших LISP на несколько десятилетий, был первым функциональным языком программирования.

Несмотря на свою зрелую старость, FP не планирует покидать нас в ближайшее время. Scala, F # и Clojure — все функциональные языки, которые появились примерно в последнее десятилетие. Все эти «новые» языки все еще завоевывают последователей. Функциональная парадигма кажется более живой, чем когда-либо.

Для этого есть веские причины. Мы живем в мире многоядерных и больших данных, во время массово параллельных распределенных систем. Координация параллельных процессов сложна, но функциональное программирование может сделать это немного проще.

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

Как Рубиисты, что мы делаем из этого? Мы прыгнем с корабля и начнем изучать Haskell или Clojure? Я не думаю, что это необходимо, но это все еще отличная идея. Проведите некоторое время с функциональным языком, пока вещи не начнут «щелкать», и вы обязательно принесете некоторые интересные идеи домой в Rubyland. Вам не нужен функциональный язык для программирования в функциональном стиле, и вам не нужно быть пуристом или педантом, чтобы пожинать плоды.

Ценности и функции

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

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

Значения являются неизменяемыми битами данных. Как только значение существует, все в нем известно и постоянно. Значения могут быть атомарными, как число 7, или составленными, как список с расположением каждого элемента в игре. Мы никогда не сможем изменить значение после его создания, но мы можем извлечь новые значения из тех, которые уже существуют. Скажем, вы хотите переместить элемент игры. Вам не разрешено брать элемент в исходном списке и обновлять его местоположение, но вы можете получить новый список из старого списка, где один элемент имеет новое местоположение.

Значения или переменные

Вы когда-нибудь пытались объяснить «переменную» новичку? Для программистов это так очевидно, но объяснить это в простых терминах может быть сложно. Давайте попробуем

Переменная — это когда вы даете что-то имя.

Это хорошее начало, и на «чисто функциональных» языках без изменчивости, таких как Haskell, мы можем на этом закончить. Но для Руби этого недостаточно. Давай еще раз попробуем.

Переменная — это блок, у блока есть имя. Вы используете имя, чтобы обратиться к вещи в коробке. Коробка может содержать разные вещи в разное время.

Это более точно, но гораздо менее интуитивно понятно. Нам нужно помнить, что существует уровень косвенности: поле и его содержимое. Но если мы соглашаемся, что мы никогда не меняем то, что находится внутри коробки, тогда коробка исчезает, и мы можем рассматривать имя как ссылку на содержимое. Это значительно упрощает ментальную модель работы вашей программы.

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

Объекты значения

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

Вместо того, чтобы делать это вручную, я продемонстрирую некоторые полезные жемчужины в следующих примерах. Первая — это Анима . Думайте об этом как о классе Ruby’s Struct Он создает конструктор на основе хеша, методы атрибута только для чтения и тесты на равенство ( ==eql?equal?

 class Ukulele
  include Anima.new(:color, :tuning)
end

u1 = Ukulele.new(color: 'green', tuning: [:G, :C, :E, :A])
u2 = Ukulele.new(color: 'green', tuning: [:G, :C, :E, :A])

u1 == u2 # => true

Тем не менее, ваш объект значения еще не стоит, потому что используемый им массив все еще изменчив.

 u.tuning << :F
u # => #<Ukulele color="green" tuning=[:G, :C, :E, :A, :F]>

Чтобы убедиться, что создаваемые объекты действительно неизменны, включая любые компоненты, можно использовать Adamantium.

 class Ukulele
  include Anima.new(:color, :tuning)
  include Adamantium
end

u.tuning[0] = :F

# ~> -:6:in `[]=': can't modify frozen Array (RuntimeError)

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

 class Ukulele
  def tune(string, note)
    self.class.new(
      color: color,
      tuning: tuning.take(string) + [note] + tuning.drop(string+1)
    )
  end
end

u = Ukulele.new(tuning: [:G, :C, :E, :A])
u.tune(0, :F) # => #<Ukulele tuning=[:F, :C, :E, :A]>
u             # => #<Ukulele tuning=[:G, :C, :E, :A]>

Необходимость повторять поля, которые не меняются, становится повторяющейся, поэтому Anima :: Update может помочь вам в этом.

 class Ukulele
  include Anima::Update

  def tune(string, note)
    update(
      tuning: tuning.take(string) + [note] + tuning.drop(string+1)
    )
  end
end

Интересный побочный эффект неизменяемых объектов заключается в том, что результаты вызовов методов можно безопасно кэшировать. Это техника, известная как запоминание и встроенная в Adamantium.

 class Ukulele
  def snares
    tuning.count
  end
  memoize :snares
end

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

Функциональные структуры данных

Недостатком этого подхода является то, что для каждого «изменения» необходимо скопировать много данных. Это становится реальной проблемой при работе с большими коллекциями. Представьте себе неизменный массив с миллионами записей. Чтобы добавить или удалить один элемент, сначала нужно продублировать все это. Это привело к идее функциональных структур данных . Они ведут себя как значения, но с помощью метода, называемого структурным разделением, они минимизируют объем данных, которые необходимо скопировать, экономя память и циклы ЦП.

Чтобы понять концепцию, подумайте о связанном списке. Добавление элемента в начало списка не лишает законной силы оригинал, оба списка просто разделяют один и тот же «хвост». Удаление «головы» из списка работает так же.

LINKED_LIST

Та же самая концепция может использоваться с древовидными структурами, многократным использованием больших частей и изменением только пути от измененного узла к корню. Вот как могут быть реализованы сложные типы данных с произвольным доступом, такие как наборы и векторы. Тип древовидной структуры, обладающий особенно привлекательными свойствами, — это «Hash Array Mapped Trie», также известный как «Ideal Hash Tree». Драгоценный камень Хомяка имеет реализации векторов и наборов, основанных на попытках сопоставления массива хэшей, и несколько других функциональных структур данных.

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