В этом посте представлен язык Coconut, функциональное надмножество языка Python, целью которого является создание элегантного функционального кода, оставаясь при этом в знакомой среде Python и библиотеках, и приведено несколько показательных примеров.
"Здравствуй, Мир!" |> x -> x.replace('Мир', 'Coconut') |> print
Язык Coconut (на момент написания поста его последней версией является v1.5.0) - это функционально-ориентированное строгое надмножество языка Python, и поэтому все, что валидно для Python, также валидно для Coconut, при этом Coconut транспилируется в Python. По сути Coconut представляет собой игровую площадку для освоения парадигмы функционального программирования, тестирования идей в области ФП, отработки приемов решения задач в указанной парадигме и для учебных целей.
На странице веб-сайта языка утверждается, что язык Coconut создан быть вам полезным. Coconut расширяет репертуар программистов на Python, задействуя инструменты современного функционального программирования, упрощая использование этих инструментов и усиливая их мощность. Иными словами, язык Coconut делает с функциональным программированием то, что язык Python сделал с императивным программированием.
Будем надеяться, что этот пост докажет эти утверждения на практике.
На всякий случай, установить Coconut можно посредством менеджера пакетов pip: pip install coconut
Coconut - это строгое надмножество языка Python
Написание кода Python в функциональном стиле нередко выливается в сложную задачу, начиная от незначительных неудобств, таких как многословный синтаксис лямбд, и заканчивая более серьезными проблемами, такими как связывание в цепочку лениво вычисляемых итераторов и сопоставление с шаблонами. Coconut - это функциональное надмножество языка Python, целью которого является создание элегантного и функционально-ориентированного кода в стиле Python.
Поскольку функции являются гражданами первого сорта, Python позволяет строить программы с использованием функций более высокого порядка. Однако делать на Python что-то, что делается в повседневном режиме на типичном функциональном языке, зачастую бывает обременительно. Отсутствие сжатого синтаксиса для лямбд, каррирования и функциональных композиций иногда становится крупной неприятностью. А отсутствие нестереотипного сопоставления с шаблонами может стать решающим фактором, чтобы отказаться от решения на основе ФП.
Разработанный в 2016 году диалект Python с открытым исходным кодом обеспечивает синтаксис для использования функций, которые можно найти в функционально-ориентированных языках, таких как Haskell и Scala. Многие функции Coconut включают в себя более элегантные и читаемые способы выполнения того, что уже делает Python. Например, программирование в стиле конвейера позволяет передавать аргументы функции в функцию с помощью отдельного синтаксиса. Например, print("Здравствуй, мир!")
можно написать как "Здравствуй, мир!" |> print
. Лямбды, или анонимные функции в Python, могут писаться четче, например (x) -> x2
вместо lambda x: x2
.
Вот неполный перечень того, что предлагает Coconut:
Сопоставление с шаблонами
match [head] + tail in [0, 1, 2, 3]:
print(head, tail)
Алгебраические типы данных
data Empty()
data Leaf(n)
data Node(l, r)
def size(Empty()) = 0
addpattern def size(Leaf(n)) = 1
addpattern def size(Node(l, r)) = size(l) + size(r)
Деструктурирующее присваивание
{"list": [0] + rest} = {"list": [0, 1, 2, 3]}
Частичное применение функций
range(10) |> map$(pow$(?, 2)) |> list
Ленивые списки
(| first_elem() |) :: rest_elems()
Функциональная композиция
(f..g..h)(x, y, z)
Более удобные лямбды
x -> x ** 2
Инфиксная нотация
5 `mod` 3 == 2
Конвейерное программирование
"Здравствуй, Мир!" |> x -> x.replace('Мир', 'Coconut') |> print
Операторные функции
product = reduce$(*)
Оптимизация хвостовых вызовов
def factorial(n, acc=1):
case n:
match 0:
return acc
match _ is int if n > 0:
return factorial(n-1, acc*n)
Параллельное программирование
range(100) |> parallel_map$(pow$(2)) |> list
В настоящее время версия coconut-develop
(pip install coconut-develop
) имеет полную поддержку синтаксиса и поведения сопоставления с шаблонами Python 3.10, а также полную обратную совместимость с предыдущими версиями Coconut. Эта поддержка будет выпущена в следующей версии Coconut v1.6.0.
Coconut обрабатывает различия между принятым в Python и Coconut поведением сопоставления с шаблонами следующим образом:
Всякий раз, когда вы будете использовать конструкцию сопоставления с шаблонами с другим поведением в Python, Coconut выдает предупреждение. Кроме того, такие предупреждения предоставляют альтернативный синтаксис, указывая в явной форме поведение, которое вы ищете, и Coconut выбирает вариант поведения, которое он будет использовать по умолчанию, основываясь на том, какой стиль сопоставления с шаблонами использовался: синтаксис в стиле Coconut или же синтаксис в стиле Python, таким образом сохраняя полную совместимость как с Python, так и с Coconut.
Компиляции исходного кода coconut во что-то другое, кроме исходного кода Python, в планах не стоит. Исходник на Python является единственной возможной целью транспиляции для Coconut, которая поддерживает возможность создания универсального кода, работающего одинаково на всех версиях Python — такое поведение невозможно с байт-кодом Python.
Далее, мы определим различные болевые точки при написании функционального кода на Python и продемонстрируем, как Coconut решает эти проблемы. В частности, мы представим решение базовой задачи программирования и покажем несколько других примеров.
Задача о решете Эратосфена
Решето Эратосфена (Sieve of Eratosthenes) - это алгоритм нахождения всех простых чисел до некоторого целого числа n, который приписывают древнегреческому математику Эратосфену Киренскому. Как и во многих случаях, здесь название алгоритма говорит о принципе его работы, то есть решето подразумевает фильтрацию, в данном случае фильтрацию всех чисел за исключением простых. По мере прохождения списка нужные числа остаются, а ненужные (они называются составными) исключаются.
Решение задачи средствами Python
Решение задачи о решете Эратосфена на чистом Python состоит из двух функций: primes
и sieve
. Функция primes
вызывает внутреннюю функцию sieve
.
from itertools import count, takewhile
def primes():
def sieve(numbers):
head = next(numbers)
yield head
yield from sieve(n for n in numbers if n % head)
return sieve(count(2))
list(takewhile(lambda x: x < 60, primes()))
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59]
При вызове функции sieve
мы создаем генератор count
, генерирующий целые числа, начиная с 2 и до бесконечности. В теле функции sieve
мы берем головной элемент списка и выдаем его (yield
) в качестве результата. В следующей строке кода мы выдаем результат (yield from
) рекурсивного вызова функции sieve
, которая в своем аргументе поочередно выбирает число по условию.
Обратите внимание, что numbers
в выражении next(numbers)
отличается от numbers
в выражении n for n in numbers if n % head
. Вся причина в том, что функция next
- это операция с поддержкой состояния: взяв головной элемент списка, у вас останется хвост списка.
В последней инструкции использована функция list
, поскольку takewhile
производит генератор, и без list
не получится заглянуть вовнутрь списка.
Таким образом, мы имеем довольно-таки императивный код: сделать это, сделать то и т.д.
Пошаговая замена кода Python на код Coconut
Всего за 7 шагов и «легким движением руки»(с) мы преобразуем чистый код Python в чистый функциональный код Coconut.
1. Убрать lambda
Замена ключевого слова lambda
оформляется как комбинация символов ->
.
from itertools import count, takewhile
def primes():
def sieve(numbers):
head = next(numbers)
yield head
yield from sieve(n for n in numbers if n % head)
return sieve(count(2))
list(takewhile(x -> x < 60, primes()))
2. Ввести прямой конвейер
Прямой конвейер переставляет обычный порядок приложения функций f(g(h(d)))
на вперед-направленный: d -> h -> g -> f
и оформляется через комбинацию символов |>
.
from itertools import count, takewhile
def primes():
def sieve(numbers):
head = next(numbers)
yield head
yield from sieve(n for n in numbers if n % head)
return sieve(count(2))
primes() |> ns -> takewhile(x -> x < 60, ns) |> list
3. Ввести частичное применение
Частичное применение функции позволяет динамически настраивать функции в соответствии с потребностями того места, где они используются, и создавать новую функцию из старой функции с предварительно заданными несколькими ее аргументами, но не всеми. Частичное применение не следует путать с каррированием, которое состоит в преобразовании функции многих аргументов в функцию, берущую свои аргументы по одному, и в конечном счете в превращении ее в функцию одного аргумента. Частичное применение оформляется через символ $
.
from itertools import count, takewhile
def primes():
def sieve(numbers):
head = next(numbers)
yield head
yield from sieve(n for n in numbers if n % head)
return sieve(count(2))
primes() |> takewhile$(x -> x < 60) |> list
4. Ввести итераторную цепочку
По сути дела, применяя yield
, вы говорите языку создать итератор из некого элемента в инструкции yield
и из всего остального в инструкции yield from
. И такое построение представляет собой итераторную цепочку, которая оформляется через комбинацию символов ::
.
from itertools import count, takewhile
def primes():
def sieve(numbers):
head = next(numbers)
return [head] :: sieve(n for n in numbers if n % head)
return sieve(count(2))
primes() |> takewhile$(x -> x < 60) |> list
5. Ввести сопоставление с шаблоном
Если заранее известно, что вы будете манипулировать списком, то его можно разложить, как тут, на головной элемент и остаток списка. Это оформляется через ту же самую комбинацию символов ::
, используемую для списков. Обратите внимание, что разложение списка происходит при определении аргументов функции.
from itertools import count, takewhile
def primes():
def sieve([head] :: tail):
return [head] :: sieve(n for n in tail if n % head)
return sieve(count(2))
primes() |> takewhile$(x -> x < 60) |> list
6. Преобразовать функции в выражения
Во многих функциональных языках вся работа происходит с выражениями. При таком подходе последнее вычисленное выражение автоматически возвращает значение, и поэтому отпадает необходимость в указании возвращаемого значения. В сущности все сводится к удалению ключевого слова return
и введению символа =
вместо символа :
для определения функции как выражения.
from itertools import count, takewhile
def primes() =
def sieve([x] :: xs) = [x] :: sieve(n for n in xs if n % x)
sieve(count(2))
primes() |> takewhile$(x -> x < 60) |> list
7. Использовать встроенные высокопорядковые функции
Иными словами, убрать инструкции import
. При написании программ в функциональном стиле отпадает необходимость загружать функциональные библиотеки, т.к. функции высокого порядка используются очень часто.
def primes() =
def sieve([x] :: xs) = [x] :: sieve(n for n in xs if n % x)
sieve(count(2))
primes() |> takewhile$(x -> x < 60) |> list
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59]
В итоге мы получили полностью функциональный код. Весь мыслительный процесс основан на определениях: функция primes
определяется как выражение sieve
и его вызов, а определение sieve
состоит из итераторной цепочки. Начав с императивного кода:
from itertools import count, takewhile
def primes():
def sieve(numbers):
head = next(numbers)
yield head
yield from sieve(n for n in numbers if n % head)
return sieve(count(2))
list(takewhile(lambda x: x < 60, primes()))
мы пришли к чистому функциональному коду:
def primes() =
def sieve([x] :: xs) = [x] :: sieve(n for n in xs if n % x)
sieve(count(2))
primes() |> takewhile$(x -> x < 60) |> list
Обратите внимание, насколько версия кода на языке Coconut похожа на версию кода на языке Haskell:
primes :: [Int]
primes = sieve [2..]
where
sieve (x :: xs) = x : sieve (filter (\n -> n `rem` x /= 0) xs
sieve [] = []
?> takewhile (<60) primes
Еще несколько примеров
Сопоставление с шаблонами
def quick_sort([]) = []
@addpattern(quick_sort)
def quick_sort([head] + tail) =
"""Отсортировать последовательность,
используя быструю сортировку."""
(quick_sort([x for x in tail if x < head])
+ [head]
+ quick_sort([x for x in tail if x >= head]))
quick_sort([3,6,9,2,7,0,1,4,7,8,3,5,6,7])
[0, 1, 2, 3, 3, 4, 5, 6, 6, 7, 7, 7, 8, 9]
Оптимизация хвостовых вызовов
def factorial(0, acc=1) = acc
@addpattern(factorial)
def factorial(n is int, acc=1 if n > 0) =
"""Вычислить n!, где n - это целое число >= 0."""
factorial(n-1, acc*n)
def is_even(0) = True
@addpattern(is_even)
def is_even(n is int if n > 0) = is_odd(n-1)
def is_odd(0) = False
@addpattern(is_odd)
def is_odd(n is int if n > 0) = is_even(n-1)
factorial(6) # 720
Рекурсивный итератор
@recursive_iterator
def fib_seq() =
"""Бесконечная последовательность чисел Фибоначчи."""
(1, 1) :: map((+), fib_seq(), fib_seq()$[1:])
fib_seq()$[:10] |> parallel_map$(pow$(?, 2)) |> list
[1, 1, 4, 9, 25, 64, 169, 441, 1156, 3025]
Прочее
def zipwith(f, *args) =
zip(*args) |> map$(items -> f(*items))
list(zipwith(lambda x: x > 4, [1,2,3,4,5,6,7,8,9,0]))
[False, False, False, False, True, True, True, True, True, False]
Выводы
Надеюсь, что наглядность приведенных выше примеров вызовет интерес у читателей и побудит их заняться более глубоким изучением парадигмы ФП. Фактически Coconut предлагает синтаксический сахар, т.е. ряд оптимизаций в написании кода, которые превращают код в функциональный, являясь игровой площадкой для тестирования идей с использованием парадигмы функционального программирования.
Справочные материалы:
Документация в формате PDF
Репозиторий на Github
Пост подготовлен с использованием информации веб-сайта языка и материалов Энтони Квонга.
dolfinus
Идея в целом интересная. Особенно мне нравится то, что реализованные в Coconut функции можно импортировать в Python коде, т.к. это обычный пакет, а значит автоматически становятся доступны функции вроде parallel_map/concurrent_map.
Что касается синтаксиса, во многих случаях сильно страдает читаемость написанного кода:
Это выглядит скорее как смесь Scala и Haskel, чем как Python.
Еще мне интересно, как они будут выкручиваться с введением pattern matching в 3.10 - синтаксис у Coconut и Python здесь заметно различается:
Документации на 1.6.0 не нашел, как и тестов в develop ветке, где можно было бы посмотреть на текущее поведение.
capissimo Автор
Списался с создателем языка. Он сообщил, что в версии develop все уже учтено. Можно установить через pip и попробовать. В win10 могут быть проблемы с установкой develop.
Что касается читабельности, то можно согласиться, а можно и нет. Тут дело хозяйское — используешь только то, что нравится.