Я люблю 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 делает определение анонимной функции именно таким, как мне хотелось бы его видеть:
(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)
Символ $ сразу после названия функции указывает на её частичное применение, а ? используется в качестве местозаполнителя. Почему-то этот пример не работает у меня в Windows, но с этим надо отдельно разобраться.
Пайплайны
Ещё одна простая концепция, которая часто применяется в функциональных языках и даже в широко известном 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 не получит широкого распространения, хоть мне этого и хотелось бы. Ведь функциональное программирование позволяет решать множество проблем лаконично и изящно. Зачастую даже без потери читабельности кода.
Автор: gsedometov