Несмотря на то, что я в основном разрабатывают программы на языке Python, некоторые понятия
из функционального программирования всё равно на слуху - одно из них это монады.
Монады фундаментальны для функциональных ЯП, особенно для языков вроде Haskell
, для которых
не допускают “не чистых” функций, поэтому монада - это единственный способ выражения и работы с состоянием.
Однако недавно я наткнулся на ещё один термин - “свободные монады”.
Такие монады называются “свободными”, потому что их можно собрать из любого функтора. Но также словом “free” часто обозначается минимально приемлимое выражение какой-то концепции - т.е. “Free Monad” это самое простое выражение концепции “монады”.
Монада “Free” получается на основе какого-то фунтора, т.е. класса (типа)
который поддерживает операцию map
. Определим такой функтор:
Попробуем реализовать хоть как-то эту концепцию на Python.
from typing import TypeVar, Callable
A = TypeVar('A')
B = TypeVar('B')
class Functor[A]:
def map(self, f: Callable[[A], B]) -> 'Functor[B]':
raise NotImplementedError
Монада Free
- это рекурсивная структура данных, которая имеет 2 случая:
Pure
: выражает “чистое” просто значениеFree
: выражение, которое производит новую Free-монаду
Также мы должны расширить список операций flat_map
- который является
обязательным для реализации монад.
from typing import Union
class Free[A]:
def __init__(self, value: Union[A, Functor['Free[A]']]):
self.value = value
def map(self, f: Callable[[A], B]) -> 'Free[B]':
if isinstance(self.value, Functor):
return Free(self.value.map(lambda x: x.map(f)))
else:
return Free(f(self.value))
def flat_map(self, f: Callable[[A], 'Free[B]']) -> 'Free[B]':
if isinstance(self.value, Functor):
return Free(self.value.map(lambda x: x.flat_map(f)))
else:
return f(self.value)
@staticmethod
def pure(a: A) -> 'Free[A]':
return Free(a)
В целом, получилось похоже на выражение из Haskell (но естественно куда многословней):
data Free f a = Pure a | Free (f (Free f a))
Реализуем тестовый функтор для вывода в консоль - IO
:
class IO(Functor[A]):
def __init__(self, action: Callable[[], A]):
self.action = action
def map(self, f: Callable[[A], B]) -> 'IO[B]':
return IO(lambda: f(self.action()))
Также нам нужна функция, которая запустит наше вычисление, назовём её interpret
:
def interpret(io: Free[A]) -> A:
if isinstance(io.value, IO):
return io.value.action()
elif isinstance(io.value, Functor):
return interpret(io.value.map(lambda x: interpret(x)))
else:
return io.value
Теперь мы можем собрать всё вместе в виде последовательных вызовов функций:
# Определим несколько IO-действий
read_line: Free[str] = Free(IO(lambda: input("Enter something: ")))
write_line: Free[None] = Free(IO(lambda: print("You entered: ...")))
# "Соберём" их вместе, используя Free-монаду
program: Free[None] = read_line.flat_map(
lambda user_input: write_line.map(lambda _: print(f"You entered: {user_input}"))
)
# Запуск программы
interpret(program)
Реализация вроде бы удовлетворяет требованиям монад, но тем не менее я ещё не до конца прочувствовал смысл это концепции :).
Одно из описаний различия Free от обычных монад - это то, что тип монады обычно позволяет выразить некоторое “вычисление” (это как раз то, как в ФП происходит управление состоянием - работа с логами, исключениями, IO и т.д.). Это вычисление происходит в обычных монадах при “коллапсе” контекста, т.е. контекст дополняется новой информацией. Однако с Free этого не происходит, поэтому её назначение - это просто передача с сохранением некоторого дополнительного контекста для функтора.
Возможно, лучшее объяснение вы найдете здесь: https://stackoverflow.com/questions/13352205/what-are-free-monads