Що таке “Вільний монад + перекладач”?


95

Я бачив людей, що розмовляють про Вільну Монаду з Інтерпретатором , особливо в контексті доступу до даних. Яка ця закономірність? Коли я можу захотіти ним скористатися? Як це працює, і як би я його реалізував?

Я розумію (з повідомлень , таких як це ) , що мова йде про відокремлюючи моделі від даних доступу. Чим вона відрізняється від відомої структури репозиторію? Вони, схоже, мають однакову мотивацію.

Відповіді:


138

Фактична картина насправді є значно загальнішою, ніж просто доступ до даних. Це легкий спосіб створення мови, що залежить від домену, що дає вам AST, а потім мати одного або декількох перекладачів, щоб "виконати" AST, як вам завгодно.

Безкоштовна частина монади - це просто зручний спосіб отримати AST, який ви можете зібрати за допомогою стандартних засобів монади Haskell (наприклад, нотація) без необхідності писати багато спеціального коду. Це також гарантує, що ваш 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

Насправді це єдиний дійсний спосіб зробити його Функтором, тому ми можемо використовувати derivingдля створення екземпляра автоматично, включивши DeriveFunctorрозширення.

Наступний крок - сам Freeтип. Саме це ми використовуємо для представлення нашої структури AST , побудованої на основі DSLтипу. Ви можете думати про це як список на рівні типу , де "мінуси" просто вкладають функтор, наприклад 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для кожного наступного поля і нехай нотація зробить усе сантехнічне:

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 (). У той час як стара підпис приймає 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, використовуючи вільну структуру монади, щоб зробити всю сантехніку. Ми можемо використовувати до-нотацію та стандартні функції монади з нашим DSL. Тоді, щоб насправді його використовувати, ми мусимо якось його інтерпретувати; оскільки дерево в кінцевому підсумку є лише структурою даних, ми можемо інтерпретувати його, як нам подобається, для різних цілей.

Коли ми використовуємо це для управління доступом до зовнішнього сховища даних, він дійсно схожий на шаблон сховища. Він проміжний між нашим сховищем даних та нашим кодом, розділяючи два. Дещо, однак, є більш конкретним: "сховище" - це завжди DSL з явним AST, який ми можемо потім використовувати, як би ми хотіли.

Однак сама закономірність є більш загальною. Він може використовуватися для багатьох речей, які не обов'язково включають зовнішні бази даних або сховища. Це має сенс, де ви хочете тонкий контроль ефектів або декількох цілей для DSL.


6
Чому його називають "вільною" монадою?
Бенджамін Ходжсон

14
"Безкоштовне" ім'я походить від теорії категорій: ncatlab.org/nlab/show/free+object, але це свого роду означає, що це "мінімальна" монада - що лише дійсні операції на ній є операціями монади, як це має " забули "все це інша структура.
Бойд Стівен Сміт-молодший

3
@BenjaminHodgson: Бойд абсолютно прав. Я б не переживав про це занадто сильно, якщо ти просто не цікавий. Ден Піпоні розмовляв чудово про те, що означає "безкоштовно" у BayHac, що варто подивитися. Спробуйте слідувати разом із його слайдами, тому що візуальне відео у відео зовсім марне.
Тихон Єлвіс

3
Нітпік: "Вільна частина монади - це лише [мій акцент] зручний спосіб отримати AST, який можна зібрати за допомогою стандартних засобів монади Haskell (наприклад, донотація) без необхідності писати багато спеціального коду." Це більше, ніж "просто" це (як я впевнений, ви знаєте). Вільні монади - це також нормалізоване представлення програми, що унеможливлює перекладача розрізняти програми, чия doпримітка відрізняється, але насправді "означає те саме".
sacundim

5
@sacundim: Ви могли б детальніше розказати свій коментар? Особливо речення "Вільні монади - це також нормалізоване програмне представлення, що унеможливлює перекладача розрізняти програми, чиї нотації відрізняються, але насправді" означають те саме ".
Джорджіо

15

Вільна монада - це в основному монада, яка будує структуру даних у тій же «формі», що й обчислення, а не робить щось складніше. ( Є приклади можна знайти в Інтернеті. ) Ця структура даних потім передаються шматок коду , який споживає його і здійснюють операції. * Я не зовсім знайомий з шаблоном сховища, але і від того, що я прочитав це здається бути архітектурою вищого рівня, і для її реалізації може бути використаний безкоштовний монад + перекладач. З іншого боку, безкоштовний монад + перекладач також може бути використаний для реалізації зовсім інших речей, таких як парсери.

* Варто зазначити, що ця модель не є винятковою для монад, і насправді може створювати більш ефективний код з безкоштовними додатками або вільними стрілками . ( Парсери - ще один приклад цього. )


Вибачте, я повинен був бути зрозумілішим щодо сховища. (Я забув, що не всі мають бізнес-системи / фон OO / DDD!) Репозиторій в основному інкапсулює доступ до даних і регідрує об'єкти домену для вас. Його часто використовують поряд із інверсією залежності - ви можете «підключити» різні реалізації Repo (корисні для тестування або якщо вам потрібно переключити базу даних або ORM). Доменний код просто дзвонить, repository.Get()не знаючи, звідки береться об’єкт домену.
Бенджамін Ходжсон
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.