Змея и кокос

в 14:23, , рубрики: Coconut, python, Программирование, ФП, функциональное программирование, метки:

Я люблю 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js