Свободная монада для функтора или изоляция сайд-эффектов в программе
На основе англоязычной статьи Gabriel Gonzalez.
Предположим, что мы написали простейшую компьютерную программу, делающую некий ввод-вывод (т.е. имеющую сайд-эффекты):
module Main where
import Data.Char (toUpper)
import System.Exit (exitSuccess)
main = do x <- getLine
putStrLn (map toUpper x)
putStrLn "Finished"
exitSuccess
Всё бы ничего, но данный код смешивает “чистую” логику (не имеющую сторонних эффектов чистую трансформацию строки в верхний регистр: строка
-> СТРОКА
) и “грязный” ввод-вывод с терминала (имеющий сторонние эффекты вроде ожидания клавиатурного ввода, разрыв терминальной линии и т.п.). Можем ли мы изолировать и сконцентрировать весь нечистый код в одном месте, оставив всю остальную логику программы чистой, не имеющей сайд-эффектов, а значит легко осмысляемой?
Один из интересных способов сделать это состоит в использовании свободной монады (free monad) функтора – монады, которая однозначно (уникально) определяется данным функтором (с точностью до изоморфизма). В некотором смысле, свободная монада – это монада с наименьшими ограничениями (на монадическую структуру), которую мы можем получить для данного функтора. Для понимания можно также использовать интуицию дерева, вершины которого помечены конструкторами функториального типа.
Свободные монады позволяют нам легко декомпозить нашу программу в её чистую репрезентацию и нечистый интерпретатор. Функториальный тип, таким образом, описывает F-алгебру, а интерпретатор знает как её вычислять в нечистом Мире сайд-эффектов (например, в IO
-монаде). Логика программы пишется этой функториальной алгеброй, а затем исследуется, тестируется, хранится, передаётся по сети, доказывается и, наконец, выполняется интерпретатором.
Перепишем нашу программку.
Функториальная алгебра⌗
Представим все нужные нам операции ввода-вывода типом TeletypeF
с тривиальной реализацией тайпкласса Functor
:
module Main where
import Control.Monad.Free
import Data.Char (toUpper)
import System.Exit (exitSuccess)
data TeletypeF x
= PutStrLn String x
| GetLine (String -> x)
| ExitSuccess
instance Functor TeletypeF where
fmap f (PutStrLn str x) = PutStrLn str (f x)
fmap f (GetLine k) = GetLine (f . k)
fmap f ExitSuccess = ExitSuccess
type Teletype = Free TeletypeF
putStrLn' :: String -> Teletype ()
putStrLn' str = liftF $ PutStrLn str ()
getLine' :: Teletype String
getLine' = liftF (GetLine id)
exitSuccess' :: Teletype ()
exitSuccess' = liftF ExitSuccess
Мы искусственно “поддерживаем” в алгебре тип последнего вычисления (параметр x
). Для унифицированного интерфейса к свободным монадам используем пакет free
и тип Free f a
(“The Free Monad for a Functor f”):
data Free f a = Pure a | Free (f (Free f a))
В монадических синонимах операций ввода-вывода используем функцию liftF
для лифтинга, т.е. “повышения” значений нашего функториального типа TeletypeF
до значений нового монадического типа Teletype
. Чисто механический код.
Интерпретатор⌗
А вот наш “нечистый” интерпретатор, рекурсивно вычисляющий (разрушающий) значения нашей свободной монады Teletype
в значение в контексте IO
:
run :: Teletype r -> IO r
run (Pure r) = return r
run (Free (PutStrLn str x)) = putStrLn str >> run x
run (Free (GetLine k)) = getLine >>= run . k
run (Free ExitSuccess) = exitSuccess
Чистая логика программы⌗
Наконец, теперь мы можем выразить логику нашей программы в “чистом” виде. Все сторонние эффекты (I/O) полностью изолированы в “грязном” коде интерпретатора run
!
echo :: Teletype ()
echo = do
str <- getLine'
putStrLn' (map toUpper str)
putStrLn' "Finished"
exitSuccess'
Запустить программу - значит произвести вычисления в функториальной алгебре:
main :: IO ()
main =
run echo