Я люблю 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 не получит широкого распространения, хоть мне этого и хотелось бы. Ведь функциональное программирование позволяет решать множество проблем лаконично и изящно. Зачастую даже без потери читабельности кода.
Комментарии (18)
Andruwkoo
16.02.2017 21:35+4Да-да, совсем как в OCaml, только без строгой типизации (язык-то у нас динамический)
Если быть до конца честным, то Python 3 — строготипизированный, хоть и динамический)ZyXI
16.02.2017 22:42Точнее, строго типизированы операции на встроенных типах. Ничто не мешает вам, к примеру, сделать свой числовой тип, у которого
__add__(self, string)
преобразует в строку и делает конкатенацию. Конечно, кроме того, что числовых литералов, дающих этот ваш тип, вы не увидите.
slovak
17.02.2017 02:10Немного смутил размер скомпилированного файла.
-rw------- 1 u0_a209 u0_a209 25 Feb 16 20:07 coco.coco
-rw------- 1 u0_a209 u0_a209 24K Feb 16 20:08 coco.py
25 байт привет мир превратилис в 25К.
Я, конечно, понимаю. Но все-же :)gsedometov
17.02.2017 08:08Ну да, Coconut компилируется в предположении, что на машине, где его будут запускать, есть только чистый Python. Поэтому в выходном файле оказывается довольно много функций и классов, которые лежат мёртвым кодом. С другой стороны, рантайм питона 3.6 весит 100+ МБ, так что лишних 25К на проект вряд ли кого-то будет волновать)
Zenker
17.02.2017 02:28+2Понимаю, что статья немного не об этом, но ленивые вычисления — тоже часть ФП :)
В примере
sum([n**2 for n in lst])
создаётся целый новый ненужный список с квадратами элементов исходного. Это может быть проблемой, если список большой. Избежать этого можно, написав просто
sum(n**2 for n in lst)
Такой вариант допускает, к тому же, ленивые вычисления, например
all(f() for f in fun_lst) # Выполняет функции до тех пор, пока очередная не вернёт False all([f() for f in fun_lst]) # Выполнит все функции из списка
Ну и в общем случае, если нужно поработать с элементами коллекции, а не с коллекцией в целом, часто бывает удобно привязать к ней итератор
it = iter(lst) ... next(it)
Dark_Daiver
17.02.2017 07:13А насколько получаемый код медленней «нативных» питоновских циклов и list comprehension?
gsedometov
17.02.2017 08:32Замерил время на возведение в квадрат 100К целых чисел. В среднем, получились такие результаты: Coconut — 87 мс, циклы — 79 мс, генератор списка — 78 мс. Так что язык, скорее всего, немного медленнее питона.
Dark_Daiver
17.02.2017 08:35Только возведение в квадрат? Т.е. цепочка преобразований (map -> filter -> map -> reduce к примеру) будет давать еще больше оверхеда? Немного грустно, язык выглядит симпатично.
gsedometov
17.02.2017 10:26Прогнал тест с питоновским map'ом — получил те же 87 мс, так что проблема не в Coconut)
Dark_Daiver
17.02.2017 10:44+1Ага, т.е. оно компилится в map. Можно написать оптимизирующий компиль который будет компилить Coconut в списочные выражения =)
Ну и 87 мс для теста это очень мало
delvin-fil
18.02.2017 10:42То, что «доктор прописал».
Как раз понадобилось немного посчитать и рылся в справочниках.
random1st
Вот честно, не придумал сходу ни одного случая, для чего это может понадобиться.
gsedometov
Согласен, нет таких проблем, для решения которых было бы необходимо функциональное программирование. Просто некоторые задачи при функциональном подходе решаются более элегантно, что ли. Например, данные при анализе часто приходится прогонять через несколько функций, и генераторы списков не выглядят красивым решением. Да и if/else и словари — не такие гибкие и удобные инструменты, как паттерн-матчинг.
devpony
Мой код для обработки данных зачастую выглядит как-то так:
И по количеству закрывающих скобочек стремится к коду на лиспе. Поэтому я очень часто мечтаю о функциональном языке, компилируемом в питон. Автор, большое спасибо. Хочется побольше статей с примерами применения и, в особенности, использования в нём математических пакетов, таких как
numpy
например.gsedometov
На самом деле, писать про использование математических пакетов в Coconut нечего, потому что используются они точно так же, как в питоне. То есть, можно написать вот так:
и получится рабочий код, который вы можете запустить из интерпретатора или компилировать в питоновский код, эквивалентный вашему примеру (если я правильно его распарсил, конечно). Единственная проблема, которую я тут заметил — нельзя пайпы переносить по строкам без \, но это мелочи и поправимо.
devpony
Замечательно, спасбо большое! Вот только
attrgetter('filename')
не радует, я бы оставил лямбду)0xd34df00d
Для этого надо иметь некоторый опыт написания на функциональных языках вроде тоготже окамля или хаскеля.