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


  1. random1st
    16.02.2017 18:08
    +2

    Вот честно, не придумал сходу ни одного случая, для чего это может понадобиться.


    1. gsedometov
      16.02.2017 18:55
      +2

      Согласен, нет таких проблем, для решения которых было бы необходимо функциональное программирование. Просто некоторые задачи при функциональном подходе решаются более элегантно, что ли. Например, данные при анализе часто приходится прогонять через несколько функций, и генераторы списков не выглядят красивым решением. Да и if/else и словари — не такие гибкие и удобные инструменты, как паттерн-матчинг.


    1. devpony
      16.02.2017 19:16

      Мой код для обработки данных зачастую выглядит как-то так:


      x_train = np.vstack(list(map(lambda x: x.reshape(1, w, h, 3), map(preprocess, map(np.load, map(lambda e: e.filename, protocol))))))

      И по количеству закрывающих скобочек стремится к коду на лиспе. Поэтому я очень часто мечтаю о функциональном языке, компилируемом в питон. Автор, большое спасибо. Хочется побольше статей с примерами применения и, в особенности, использования в нём математических пакетов, таких как numpy например.


      1. gsedometov
        16.02.2017 19:35
        +2

        На самом деле, писать про использование математических пакетов в Coconut нечего, потому что используются они точно так же, как в питоне. То есть, можно написать вот так:


        from operator import attrgetter
        import numpy as np
        
        x_train = protocol |> map$(np.reshape$(?, 1, w, h, 3) ..  preprocess .. np.load ..  attrgetter('filename')) |> list |> np.vstack

        и получится рабочий код, который вы можете запустить из интерпретатора или компилировать в питоновский код, эквивалентный вашему примеру (если я правильно его распарсил, конечно). Единственная проблема, которую я тут заметил — нельзя пайпы переносить по строкам без \, но это мелочи и поправимо.


        1. devpony
          16.02.2017 19:45

          Замечательно, спасбо большое! Вот только attrgetter('filename') не радует, я бы оставил лямбду)


    1. 0xd34df00d
      16.02.2017 20:23

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


  1. Andruwkoo
    16.02.2017 21:35
    +4

    Да-да, совсем как в OCaml, только без строгой типизации (язык-то у нас динамический)

    Если быть до конца честным, то Python 3 — строготипизированный, хоть и динамический)


    1. ZyXI
      16.02.2017 22:42

      Точнее, строго типизированы операции на встроенных типах. Ничто не мешает вам, к примеру, сделать свой числовой тип, у которого __add__(self, string) преобразует в строку и делает конкатенацию. Конечно, кроме того, что числовых литералов, дающих этот ваш тип, вы не увидите.


      1. 0xd34df00d
        17.02.2017 04:35
        +1

        Это не отменяет строгой типизации.


  1. 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К.
    Я, конечно, понимаю. Но все-же :)


    1. gsedometov
      17.02.2017 08:08

      Ну да, Coconut компилируется в предположении, что на машине, где его будут запускать, есть только чистый Python. Поэтому в выходном файле оказывается довольно много функций и классов, которые лежат мёртвым кодом. С другой стороны, рантайм питона 3.6 весит 100+ МБ, так что лишних 25К на проект вряд ли кого-то будет волновать)


  1. 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)
     


  1. Dark_Daiver
    17.02.2017 07:13

    А насколько получаемый код медленней «нативных» питоновских циклов и list comprehension?


    1. gsedometov
      17.02.2017 08:32

      Замерил время на возведение в квадрат 100К целых чисел. В среднем, получились такие результаты: Coconut — 87 мс, циклы — 79 мс, генератор списка — 78 мс. Так что язык, скорее всего, немного медленнее питона.


      1. Dark_Daiver
        17.02.2017 08:35

        Только возведение в квадрат? Т.е. цепочка преобразований (map -> filter -> map -> reduce к примеру) будет давать еще больше оверхеда? Немного грустно, язык выглядит симпатично.


        1. gsedometov
          17.02.2017 10:26

          Прогнал тест с питоновским map'ом — получил те же 87 мс, так что проблема не в Coconut)


          1. Dark_Daiver
            17.02.2017 10:44
            +1

            Ага, т.е. оно компилится в map. Можно написать оптимизирующий компиль который будет компилить Coconut в списочные выражения =)
            Ну и 87 мс для теста это очень мало


  1. delvin-fil
    18.02.2017 10:42

    То, что «доктор прописал».
    Как раз понадобилось немного посчитать и рылся в справочниках.