Я люблю Python. Нет, правда, это отличный язык, подходящий для широкого круга задач: тут вам и работа с операционной системой, и веб-фреймворки на любой вкус, и библиотеки для научных вычислений и анализа данных. Но, помимо Python, мне нравится функциональное программирование. И питон в этом плане неплох: есть замыкания, анонимные функции и вообще, функции здесь — объекты первого класса. Казалось бы, чего ещё можно желать? И тут я случайно наткнулся на Coconut — функциональный язык, компилируемый в Python. Всех любителей Python и ФП прошу под кат.
Что? Функциональный язык, который компилируется в Python? Но зачем, ведь функциональных фич и так полно, а если хочется дополнительных извращений, то есть модуль toolz.functoolz? Но давайте рассмотрим простую задачу: нам необходимо сложить квадраты чисел из некоторого списка.
l = [1, 2, 3, 4, 5]
Возможные решения
Императивное решение "в лоб":
def sum_imp(lst): s = 0 for n in lst: s += n**2 return s
С использованием map и reduce (выглядит жутко):
from functools import reduce from operator import add def sum_map_reduce(lst): return reduce(add, map(lambda n: n**2, lst))
С использованием генераторов списков (pythonic-way):
def sum_list_comp(lst): return sum([n**2 for n in lst])
Последний вариант не так уж плох. Но в таких случаях хочется написать что-нибудь в духе
sum_sqr(lst) = lst |> map(n -> n**2) |> sum
Да-да, совсем как в OCaml, только без строгой типизации (язык-то у нас динамический). А что, если я вам скажу, что с Coconut мы действительно можем так сделать? С его помощью можно написать
sum_sqr(lst) = lst |> map$(n -> n**2) |> sum
и получить полноценное решение поставленной задачи без вызовов функций(от_функций(от_функций))).
Фичи
Авторы языка пишут, что он добавляет в Python следующие возможности:
- Сопоставление с образцом
- Алгебраические типы данных
- Деструктурирующее присваивание
- Частичное применение (я знаю про partial, но подробнее чуть ниже)
- Ленивые списки (те самые head::tail из окамла)
- Композиция функций
- Улучшенный синтаксис лямбда-выражений
- Инфиксная запись для функций
- Пайплайны
- Оптимизация хвостовой рекурсии (мнение Гвидо по этому поводу известно, но иногда ведь хочется)
- Параллельное исполнение
Также стоит отметить, что язык может работать в режиме интерпретатора, компилироваться в исходники Python и использоваться в качестве ядра для Jupyter Notebook (сам пока не проверял, но разработчики пишут, что можно).
А теперь остановимся на некоторых возможностях поподробнее. Все примеры были проверены на Coconut 1.2.1.
Синтаксис лямбда-выражений
Я уверен, что не мне одному доставляет боль запись лямбда-выражений в питоне. Я даже думаю, что её специально создали такой, чтобы ей пользовались как можно реже. Coconut делает определение анонимной функции именно таким, как мне хотелось бы его видеть:
(x -> x*2)(a) # То же, что (lambda x: x*2)(a)
Композиция функций
Композиция функций выглядит здесь почти как в хаскеле:
(f .. g .. h)(x) # То же, что и f(g(h(x)))
Частичное применение
В модуле functools есть функция partial, которая позволяет создавать функции с фиксированными аргументами. У неё есть существенный недостаток: позиционные аргументы нужно подставлять строго по порядку. Например, нам нужна функция, которая возводит числа в пятую степень. По логике, мы должны использовать partial (мы ведь просто хотим взять функцию и зафиксировать один из аргументов!), но никакого выигрыша это не даст (pow в обоих случаях используется, чтобы отвлечься от того, что это встроенная операция):
from functools import partial from operator import pow def partial5(lst): return map(lambda x: partial(pow(x, 5)), lst) # Какой кошмар! def lambda5(lst): return map(lambda x: pow(x, 5), lst) # Так немного лучше
Что может предложить Coconut? А вот что:
def coco5(lst) = map$(pow$(?, 5), lst)
Символ $ сразу после названия функции указывает на её частичное применение, а ? используется в качестве местозаполнителя.
Пайплайны
Ещё одна простая концепция, которая часто применяется в функциональных языках и даже в широко известном bash. Всего здесь имеется 4 типа пайплайнов:
| Пайплайн | Название | Пример использования | Пояснение |
| |> | простой прямой | x |> f | f(x) |
| <| | простой обратный | f <| x | f(x) |
| |*> | мультиаргументный прямой | x |*> f | f(*x) |
| <*| | мультиаргументный обратный | f <*| x | f(*x) |
Сопоставление с образцом и алгебраические типы
В самом простом случае паттерн-матчинг выглядит так:
match 'шаблон' in 'значение' if 'охранное выражение': 'код' else: 'код'
Охрана и блок else могут отсутствовать. В таком виде паттерн-матчинг не очень интересен, поэтому рассмотрим пример из документации:
data Empty() data Leaf(n) data Node(l, r) Tree = (Empty, Leaf, Node) def depth(Tree()) = 0 @addpattern(depth) def depth(Tree(n)) = 1 @addpattern(depth) def depth(Tree(l, r)) = 1 + max([depth(l), depth(r)])
Как вы могли догадаться, Tree — это тип-сумма, который включает в себя разные типы узлов бинарного дерева, а функция depth предназначена для рекурсивного вычисления глубины дерева. Декоратор addpattern позволяет выполнять диспетчеризацию при помощи шаблона.
Для случаев, к��гда результат должен вычисляться в зависимости от первого подходящего шаблона, введено ключевое слово case. Вот пример его использования:
def classify_sequence(value): '''Классификатор последовательностей''' out = "" case value: match (): out += "пусто" match (_,): out += "одиночка" match (x,x): out += "повтор "+str(x) match (_,_): out += "пара" match _ is (tuple, list): out += "последовательность" else: raise TypeError() return out
Параллельное выполнение
parallel_map и concurrent_map из Coconut — это просто обёртки над ProcessPoolExecutor и ThreadPoolExecutor из concurrent.futures. Несмотря на их простоту, они обеспечивают упрощенный интерфейс для многопроцессного/многопоточного выполнения:
parallel_map(pow$(2), range(100)) |> list |> print concurrent_map(get_data_for_user, all_users) |> list |> print
Заключение
Мне всегда было завидно, что в .Net есть F#, под JVM — Scala, Clojure, про количество функциональных языков, компилируемых в JS я вообще молчу. Наконец-то я нашёл нечто похожее для Python. Я почти уверен, что Coconut не получит широкого распространения, хоть мне этого и хотелось бы. Ведь функциональное программирование позволяет решать множество проблем лаконично и изящно. Зачастую даже без потери читабельности кода.
