Как стать автором
Обновить

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

Время на прочтение9 мин
Количество просмотров5.7K

В этом посте представлен язык 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 

Для сравнения ниже приведен вариант имплементации в чистом Python-овском стиле:

def primes(n):
    def sieve(l):
        if l==[]: return []
        else: 
            h, *t = l
            return [h] + sieve([x for x in l if x%h > 0])
    return sieve(range(2,n))

primes(60)
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59]

Еще несколько примеров

  • Сопоставление с шаблонами

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 предлагает синтаксический сахар, т.е. ряд оптимизаций в написании кода, которые превращают код в функциональный, являясь игровой площадкой для тестирования идей с использованием парадигмы функционального программирования.

Справочные материалы:

Пост подготовлен с использованием информации веб-сайта языка и материалов Энтони Квонга.

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 6: ↑6 и ↓0+6
Комментарии6

Публикации

Истории

Работа

Python разработчик
138 вакансий
Data Scientist
61 вакансия

Ближайшие события