AlterEGO Labs official blog

About ruby, rails and another amazing stuffs…

Ruby с функциональным блеском

Вот и наступил 2015 год. Подводя итоги прошлого года можно отметить две мэйнстримовые тенденции:

  1. Статическая типизация
  2. Функциональное программирование

Сегодня пойдет речь о втором пункте.

Год перемен

2014 год однозначно был функциональным. Вторую жизнь получили многие забытые функциональные языки, много новых появилось. Их начинали пихать куда можно и куда нельзя, где уместно их использовать, а где нет. Меня тоже немного задела эта волна. Совершенно случайно необходимо было написать слой приложения на любом языке кроме ruby. Задача выбора языка сейчас очень сложна из-за их огромного выбора. Но не на этот раз! Все больше и больше информации появлялось о новом молодом языке elixir. Он привлекал еще больше внимания, т.к. его создатель - Rails Core Team Member (Jose Valim)!. И как раз вышла стабильная версия 1.0.0! И он как раз еще и функциональный язык! Бинго! Как раз есть возможность окунуться в мир ФП на реальном примере! Работка обещала быть интересной :-)

Другой мир

На первый взгляд все казалось намного проще. Но в самом начале был тупик… ООПшный тупик… Нет классов и, соответственно, нет наследования и всех плюшек ООП. Есть только функции и их можно объединять в модули и все. Совсем другой мир! Но не менее интересный) Но я не буду полностью все описывать, а остановлюсь на нескольких, как мне кажется, особо интересных особенностях.

Трансформация данных

Для тех, кто хочет познакомится с elixir и функциональным программированием, обязательно стоит посмотреть доклад Дейва Томаса (Dave Thomas) Elixir: Power of Erlang, Joy of Ruby. Основная идея, которую я подчерпнул из данного доклада - трансформация данных. Т.е., рассматривая какую-либо часть архитектуры как черный ящик, то на вход подается данные_1, а на выходе мы ожидаем данные_2. Я думаю, что это применимо и к ОПП подходу, но в ФП это более выражено и есть даже специальный оператор!

Pipe-оператор

В elixir есть специальный оператор |>, который называется pipe-оператор, который выполняет операции над данными слева. Например:

Листинг 1 - Пример использования pipe-оператора
1
2
str = "   sergey     "
str |> String.strip # => "sergey"

Причем количество операций преобразования может быть несколько

Листинг 2 - Пример комбинирования операций преобразования через pipe-оператор
1
2
str = "   sergey     "
str |> String.strip |> String.capitalize # => "Sergey"

Это очень напоминает оператор |, используемый в bash.

А что в ruby?

Ruby полностью объектно-ориентированный язык, поэтому в основе лежат объекты и связь между объектами с помощью сообщений. Но сколько же раз вы видели подобный код:

Листинг 3
1
2
3
def fetch_name(html)
  normalize_name(strip_html(html))
end

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

Давайте попробуем реализовать подобное в ruby. Мы будем использовать здесь monckeypatching (начиная с ruby 2.0 необходимо использовать refinements)!

Листинг 4 - Monckeypatching
1
2
3
4
5
6
7
8
9
10
11
12
class String
  def strip_html
    # some code
  end
  def normalize_name
    # some code
  end
end

def fetch_name(html)
  html.strip_html.normalize_name
end

Вот чем вам не код, как в листинге 2? В отличии от листинга 3, здесь код намного понятен с первого взгляда, т.к. сразу можно понять: слева объект, над которым производятся трансформации, а далее идут операции. Стоит отметить, что в данном примере все операции производятся над типом String. Если в цепочке обработки используются несколько типов данных, то необходимо патчить все соответствующие классы.

А как же коллекции?

Pipe-оператор также отлично справляется и с коллекциями. Вот, например, код, который увеличивает каждый элемент в массиве на 1:

Листинг 5
1
2
3
4
5
6
7
8
defmodule Transform do
  def increment(el) do
    el + 1
  end
end

arr = [1, 2, 3]
arr |> Enum.map(&Transform.increment(&1)) #=> [2, 3, 4]

Или даже лучше так:

Листинг 6
1
2
3
4
5
6
7
8
9
10
11
12
defmodule Transform do
  def increment(el) do
    el + 1
  end

  def process_element(el) do
    el |> increment
  end
end

arr = [1, 2, 3]
arr |> Enum.map(&Transform.process_element(&1)) #=> [2, 3, 4]

В ruby это выглядело бы следующим образом:

Листинг 7
1
2
3
4
5
6
def process_element(el)
  el + 1
end

arr = [1, 2, 3]
arr.map { |e| process_element(e) } # => [2, 3, 4]

Или в стиле elixir’a (для этого будем использовать магический метод Kernel#method):

Листинг 8
1
2
3
4
5
6
def process_element(el)
  el + 1
end

arr = [1, 2, 3]
arr.map &method(:process_element) # => [2, 3, 4]

Что в итоге?

Я очень люблю ruby! Каждый день для себя открываю все больше и больше возможностей и не перестаю восхищатся его мощью и выразительностью. Но всегда не покидает чувство, что чего-то не хватает и ты находишь это в другом языке и пытаешься реализовать в ruby. Кто знает, возможно лет через N это запилят и в ruby :-)

Спасибо за внимание!

Comments