Что такое шаблон «Свободная монада + переводчик»?

95

Я видел людей, говорящих о Free Monad с Interpreter , особенно в контексте доступа к данным. Что это за образец? Когда я мог бы хотеть использовать это? Как это работает, и как бы я это реализовал?

Я понимаю (из сообщений , таких как это ) , что речь идет о отделяя модели от данных доступа. Чем он отличается от известного шаблона репозитория? У них, кажется, есть та же самая мотивация.

Бенджамин Ходжсон
источник

Ответы:

138

Фактический шаблон на самом деле значительно более общий, чем просто доступ к данным. Это упрощенный способ создания предметно-ориентированного языка, который дает вам AST, а затем наличие одного или нескольких интерпретаторов для «выполнения» AST, как вам нравится.

Свободная часть монады - это просто удобный способ получить AST, который можно собрать, используя стандартные средства монады Haskell (например, do-notation), без необходимости написания большого количества пользовательского кода. Это также обеспечивает компоновку вашего DSL : вы можете определить его по частям, а затем структурировать части, что позволит вам воспользоваться преимуществами обычных абстракций Haskell, таких как функции.

Использование бесплатной монады дает вам структуру компонуемого DSL; все, что вам нужно сделать, это указать кусочки. Вы просто пишете тип данных, который охватывает все действия в вашем DSL. Эти действия могут делать что угодно, не только доступ к данным. Однако, если вы указали все свои обращения к данным как действия, вы получите AST, который определяет все запросы и команды к хранилищу данных. Затем вы можете интерпретировать это так, как вам нравится: запустить его для действующей базы данных, запустить для макета, просто записать команды для отладки или даже попытаться оптимизировать запросы.

Давайте рассмотрим очень простой пример, скажем, хранилища значений ключей. Сейчас мы будем рассматривать ключи и значения как строки, но вы можете добавить типы, приложив немного усилий.

data DSL next = Get String (String -> next)
              | Set String String next
              | End

nextПараметр позволяет нам комбинировать действия. Мы можем использовать это, чтобы написать программу, которая получает «foo» и устанавливает «bar» с этим значением:

p1 = Get "foo" $ \ foo -> Set "bar" foo End

К сожалению, этого недостаточно для значимого DSL. Так как мы использовали nextдля композиции, тип p1такой же длины, как наша программа (т.е. 3 команды):

p1 :: DSL (DSL (DSL next))

В этом конкретном примере nextтакое использование кажется немного странным, но это важно, если мы хотим, чтобы наши действия имели переменные другого типа. Мы могли бы хотеть печатать getи set, например.

Обратите внимание, как nextполе отличается для каждого действия. Это намекает на то, что мы можем использовать его для создания DSLфунктора:

instance Functor DSL where
  fmap f (Get name k)          = Get name (f . k)
  fmap f (Set name value next) = Set name value (f next)
  fmap f End                   = End

Фактически, это единственный действительный способ сделать его Functor, поэтому мы можем использовать его derivingдля автоматического создания экземпляра, включив DeriveFunctorрасширение.

Следующим шагом является сам Freeтип. Это то, что мы используем для представления нашей структуры AST , построенной поверх DSLтипа. Вы можете думать об этом как о списке на уровне типов , где «cons» - это просто вложенный функтор, например DSL:

-- compare the two types:
data Free f a = Free (f (Free f a)) | Return a
data List a   = Cons a (List a)     | Nil

Таким образом, мы можем использовать Free DSL nextпрограммы разных размеров одинакового типа:

p2 = Free (Get "foo" $ \ foo -> Free (Set "bar" foo (Free End)))

Который имеет гораздо более приятный тип:

p2 :: Free DSL a

Однако фактическое выражение со всеми его конструкторами все еще очень неудобно в использовании! Вот где появляется часть монады. Как следует из названия «свободная монада», Freeона является монадой, если f(в данном случае DSL) является функтором:

instance Functor f => Monad (Free f) where
  return         = Return
  Free a >>= f   = Free (fmap (>>= f) a)
  Return a >>= f = f a

Теперь мы кое-что получаем: мы можем использовать doнотацию, чтобы сделать наши выражения DSL более привлекательными. Вопрос только в том, что поставить next? Итак, идея состоит в том, чтобы использовать Freeструктуру для композиции, поэтому мы просто поместим Returnдля каждого следующего поля и позволим do-notation выполнить всю сантехнику:

p3 = do foo <- Free (Get "foo" Return)
        Free (Set "bar" foo (Return ()))
        Free End

Это лучше, но все равно немного неловко. У нас Freeи Returnповсюду. К счастью, есть образец, который мы можем использовать: способ, которым мы «поднимаем» действие DSL Free, всегда один и тот же - мы оборачиваем его Freeи подаем заявку Returnна next:

liftFree :: Functor f => f a -> Free f a
liftFree action = Free (fmap Return action)

Теперь, используя это, мы можем написать хорошие версии каждой из наших команд и иметь полный DSL:

get key       = liftFree (Get key id)
set key value = liftFree (Set key value ())
end           = liftFree End

Используя это, вот как мы можем написать нашу программу:

p4 :: Free DSL a
p4 = do foo <- get "foo"
        set "bar" foo
        end

Уловка в том, что, хотя p4выглядит как маленькая императивная программа, на самом деле это выражение имеет значение

Free (Get "foo" $ \ foo -> Free (Set "bar" foo (Free End)))

Таким образом, свободная монадная часть шаблона дала нам DSL, который создает синтаксические деревья с хорошим синтаксисом. Мы также можем написать составные поддеревья, не используя End; например, у нас может быть followключ, который получает ключ, получает его значение и затем использует его в качестве самого ключа:

follow :: String -> Free DSL String
follow key = do key' <- get key
                get key'

Теперь followможно использовать в наших программах так же, как getили set:

p5 = do foo <- follow "foo"
        set "bar" foo
        end

Таким образом, мы получили хорошую композицию и абстракцию для нашего DSL.

Теперь, когда у нас есть дерево, мы попадаем во вторую половину шаблона: интерпретатор. Мы можем интерпретировать дерево так, как нам нравится, просто сопоставляя его с шаблоном. Это позволило бы нам написать код для реального хранилища данных IO, а также для других вещей. Вот пример против гипотетического хранилища данных:

runIO :: Free DSL a -> IO ()
runIO (Free (Get key k)) =
  do res <- getKey key
     runIO $ k res
runIO (Free (Set key value next)) =
  do setKey key value
     runIO next
runIO (Free End) = close
runIO (Return _) = return ()

Это с радостью оценит любой DSLфрагмент, даже тот, который не заканчивается end. К счастью, мы можем создать «безопасную» версию функции, которая принимает только закрытые программы end, установив для сигнатуры типа ввода значение (forall a. Free DSL a) -> IO (). В то время как старая подпись принимает a Free DSL aдля любого a (например Free DSL String, Free DSL Intи т. Д.), Эта версия принимает только тот, Free DSL aкоторый работает для всех возможных - aчто мы можем создать только с помощью end. Это гарантирует, что мы не забудем закрыть соединение, когда закончим.

safeRunIO :: (forall a. Free DSL a) -> IO ()
safeRunIO = runIO

(Мы не можем просто начать с предоставления runIOэтого типа, потому что он не будет работать должным образом для нашего рекурсивного вызова. Однако мы можем переместить определение runIOв whereблок safeRunIOи получить тот же эффект, не раскрывая обе версии функции.)

Запуск нашего кода IO- не единственное, что мы можем сделать. Для тестирования мы можем захотеть запустить его State Mapвместо чистого . Написание этого кода - хорошее упражнение.

Так что это бесплатный образец монады + интерпретатора. Мы делаем DSL, используя все преимущества свободной структуры монады. Мы можем использовать do-нотацию и стандартные функции монады с нашим DSL. Затем, чтобы фактически использовать это, мы должны как-то интерпретировать это; поскольку дерево в конечном итоге представляет собой просто структуру данных, мы можем интерпретировать ее так, как нам нравится для разных целей.

Когда мы используем это для управления доступом к внешнему хранилищу данных, это действительно похоже на шаблон Repository. Он является посредником между нашим хранилищем данных и нашим кодом, разделяя их. В некотором смысле, однако, это более конкретно: «хранилище» - это всегда DSL с явным AST, который мы затем можем использовать так, как нам нравится.

Однако сам шаблон более общий, чем этот. Он может быть использован для многих вещей, которые не обязательно связаны с внешними базами данных или хранилищем. Это имеет смысл везде, где вы хотите точный контроль эффектов или нескольких целей для DSL.

Тихон Джелвис
источник
6
Почему это называется «свободной» монадой?
Бенджамин Ходжсон
14
«Свободное» имя происходит из теории категорий: ncatlab.org/nlab/show/free+object, но оно в некотором роде означает, что это «минимальная» монада - только допустимые операции над ней являются операциями монады, как и раньше » забыли "все это другая структура".
Бойд Стивен Смит младший
3
@BenjaminHodgson: Бойд совершенно прав. Я бы не стал сильно беспокоиться об этом, если тебе просто не интересно. Дэн Пипони рассказал о том, что означает «бесплатный» в BayHac, и на который стоит обратить внимание. Попробуйте следовать за его слайдами, потому что видео в видео совершенно бесполезно.
Тихон Джелвис
3
Ниппик: «Свободная часть монады - это просто [мой акцент] удобный способ получить AST, который вы можете собрать, используя стандартные средства монады Haskell (например, do-notation), без необходимости писать много собственного кода». Это больше, чем «просто» (как я уверен, вы знаете). Свободные монады также являются нормализованным представлением программы, что делает невозможным для интерпретатора различать программы, чьи- doнотации отличаются, но на самом деле «означают одно и то же».
sacundim
5
@sacundim: Не могли бы вы уточнить ваш комментарий? В особенности предложение «Свободные монады - это также нормализованное представление программы, которое делает невозможным для интерпретатора различать программы, чьи нотации различны, но на самом деле« означают одно и то же ».
Джорджио
15

Свободная монада - это, по сути, монада, которая строит структуру данных в той же «форме», что и вычисления, а не делает что-то более сложное. ( Есть примеры, которые можно найти в Интернете. ) Затем эта структура данных передается коду, который использует его и выполняет операции. * Я не совсем знаком с шаблоном хранилища, но из того, что я прочитал, видно, что быть архитектурой более высокого уровня, и для ее реализации можно использовать бесплатный интерпретатор monad +. С другой стороны, бесплатный интерпретатор monad + может также использоваться для реализации совершенно разных вещей, таких как парсеры.

* Стоит отметить, что этот шаблон не является эксклюзивным для монад, и фактически может создавать более эффективный код со свободными аппликативами или свободными стрелками . ( Парсеры - другой пример этого. )

Дэн
источник
Извинения, я должен был быть яснее о хранилище. (Я забыл, что не у всех есть бизнес-системы / фоны OO / DDD!) Репозиторий в основном инкапсулирует доступ к данным и регидратирует объекты домена для вас. Он часто используется вместе с Deverdency Inversion - вы можете «подключить» различные реализации Repo (полезно для тестирования или если вам нужно переключить базу данных или ORM). Код домена просто вызывает, repository.Get()не зная, откуда он получает объект домена.
Бенджамин Ходжсон