В этом посте представлен язык Coconut, функциональное надмножество языка Python, целью которого является создание элегантного функционального кода, оставаясь при этом в знакомой среде Python и библиотеках, и приведено несколько показательных примеров.
"Здравствуй, 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:
-
Сопоставление с шаблонами
-
Алгебраические типы данных
-
Деструктурирующее присваивание
-
Частичное применение функций
-
Ленивые списки
-
Функциональная композиция
-
Более удобные лямбды
-
Инфиксная нотация
-
Конвейерное программирование
-
Операторные функции
-
Оптимизация хвостовых вызовов
-
Параллельное программирование
В настоящее время версия 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 решает эти проблемы. В частности, мы начнем с базовой задачи программирования и перейдем к разработке конвейера ML с помощью функционального подхода.
Задача о решете Эратосфена
Решето Эратосфена (Sieve of Eratosthenes) - это алгоритм нахождения всех простых чисел до некоторого целого числа n, который приписывают древнегреческому математику Эратосфену Киренскому. Как и во многих случаях, здесь название алгоритма говорит о принципе его работы, то есть решето подразумевает фильтрацию, в данном случае фильтрацию всех чисел за исключением простых. По мере прохождения списка нужные числа остаются, а ненужные (они называются составными) исключаются.
Решение задачи средствами Python
Решение задачи о решете Эратосфена на чистом Python состоит из
двух функций: primes
и sieve
. Функция
primes
вызывает внутреннюю функцию
sieve
.
from itertools import count, takewhiledef 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, takewhiledef 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, takewhiledef 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, takewhiledef 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, takewhiledef 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, takewhiledef 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, takewhiledef 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, takewhiledef 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]
-
Алгебраические типы данных
data vector2(x, y): """Immutable two-element vector.""" def __abs__(self): return (self.x**2 + self.y**2)**.5data Empty()data Leaf(n)data Node(l, r)def size(Empty()) = 0@addpattern(size)def size(Leaf(n)) = 1@addpattern(size)def size(Node(l, r)) = size(l) + size(r)
-
Оптимизация хвостовых вызовов
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_iteratordef 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]
-
Конвейер
"Здравствуй, Мир!" |> x -> x.replace('Мир', 'Coconut') |> print
Здравствуй, Coconut!
-
Прочее
product = reduce$(*)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 предлагает синтаксический сахар, т.е. ряд оптимизаций в написании кода, которые превращают код в функциональный, являясь игровой площадкой для тестирования идей с использованием парадигмы функционального программирования.
Справочные материалы:
Пост подготовлен с использованием информации веб-сайта языка и материалов Энтони Квонга.