«ФП на Python посредством Coconut!» |> print

Kate

Administrator
Команда форума
В этом посте представлен язык 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:
  • Сопоставление с шаблонами
  • Алгебраические типы данных
  • Деструктурирующее присваивание
  • Частичное применение функций
  • Ленивые списки
  • Функциональная композиция
  • Более удобные лямбды
  • Инфиксная нотация
  • Конвейерное программирование
  • Операторные функции
  • Оптимизация хвостовых вызовов
  • Параллельное программирование
В настоящее время версия 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, который приписывают древнегреческому математику Эратосфену Киренскому. Как и во многих случаях, здесь название алгоритма говорит о принципе его работы, то есть решето подразумевает фильтрацию, в данном случае фильтрацию всех чисел за исключением простых. По мере прохождения списка нужные числа остаются, а ненужные (они называются составными) исключаются.
e5ad0b7e48aed46a99d0080ec12cc400.gif

Решение задачи средствами 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]
  • Алгебраические типы данных
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)
  • Оптимизация хвостовых вызовов
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]
  • Конвейер
"Здравствуй, Мир!" |> 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 предлагает синтаксический сахар, т.е. ряд оптимизаций в написании кода, которые превращают код в функциональный, являясь игровой площадкой для тестирования идей с использованием парадигмы функционального программирования.
Справочные материалы:
Пост подготовлен с использованием информации веб-сайта языка и материалов Энтони Квонга.


Источник статьи: https://habr.com/ru/post/556548/
 
Сверху