Избегайте упаковки монад в типизированный DSL в окончательном стиле - PullRequest
2 голосов
/ 14 октября 2019

Вот небольшая игрушка DSL в набранном без тега окончательном стиле (см. Типизированные окончательные переводчики без тега О. Киселева).

class Monad m => RPCToy m where
    mkdir :: FilePath -> m ()
    ls    :: FilePath -> m [FilePath]

Различные установкиэтот маленький DSL будет, например, реализацией mkdir и ls на разных платформах, как локальных, так и удаленных. Тип m является монадой во всех реализациях, это может быть IO, или предоставленная некоторой сетевой библиотекой, или какой-то другой доморощенной монадой.

Вот реализация в IO:

import System.Directory (listDirectory)
import Control.Monad (void)

instance RPCToy IO where
    mkdir = void . putStrLn . ("better not create "++)
    ls    = listDirectory

и небольшое приложение

import Control.Monad (unless)

demo :: RPCToy m => m ()
demo = do
    files <- ls "."
    unless ("test" `elem` files) $
        mkdir "test"

, которое можно запустить в IO монаде

main :: IO ()
main = do
    demo

Пока все хорошо.

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

{-# LANGUAGE GeneralizedNewtypeDeriving #-}

newtype Local a = Local {runLocal :: IO a} deriving (Functor, Applicative, Monad)

, а затем внедрив RPCToy Local,

instance RPCToy Local where
    mkdir = Local . putStrLn . ("BETTER NOT CREATE "++)
    ls    = Local . listDirectory

, которые можно прекрасно запустить

main :: IO ()
main = do
    runLocal demo

Что меня беспокоит, так это: Разработчики должны поместить в код много Local с, или, довольно многократно, обернуть библиотечные функции следующим образом:

localListDirectory = Local . listDirectory
...

Одна идея состоит в том, чтобысоздайте 'индексированную монаду' im i a, im i являющуюся монадой, которая содержит индексный тип i с единственной целью позволить компилятору различать различные реализации. Расширение RebindableSyntax делает это возможным без отказа от синтаксиса do. Но каждую монаду нужно «поднять» в эту индексированную монаду. Улучшение заключается в следующем: каждая монада m и ее функции должны быть сняты только один раз. Иначе это все еще довольно запутанно.

Мне интересно, есть ли лучший способ избавиться от обертывания монады.

1 Ответ

1 голос
/ 16 октября 2019

Вот один из подходов: представьте монадный преобразователь, который просто наматывает другую монаду с добавлением фантомного типа i,

import Control.Monad.Trans.Class (MonadTrans, lift)

newtype IndexedWrapT i m a = IndexedWrapT {runIndexedWrapT :: m a} 
            deriving (Functor, Applicative, Monad)

instance MonadTrans (IndexedWrapT i) where
    lift = IndexedWrapT

Тип фантома i имеет единственную цель:реализации имеют различный тип.

Затем оберните (lift) соответствующие функции один раз, например:

putStrLn' :: MonadTrans t => String -> t IO ()
putStrLn' = lift . putStrLn

На стороне реализации

data MyImpl'

type MyImpl = IndexedWrapT MyImpl' IO

runMyImpl :: MyImpl a -> IO a
runMyImpl = runIndexedWrapT

instance RPCToy MyImpl where
    mkdir = putStrLn' . ("BETTER NOT CREATE "++)
    ....

СформулировавОперация обёртывания как монадного преобразователя становится ясной, что здесь могут использоваться другие подходы к составлению эффектов, как указано в комментариях, например, freer-simple или polysemy .

...