Haskell Напишите Monad для выражений
Я пытаюсь разработать встроенный язык, где операции могут поднимать определенные флаги в зависимости от значений. Я предвижу работу со скалярными значениями, а также с векторами (например, map, fold и т. Д.). Моя идея - использовать Writer Monad для отслеживания флагов. Упрощенный пример, где фактическим типом является "Int" и флаг поднимается, если любой из аргументов равен 0.
import Control.Monad.Identity
import Control.Monad.Writer
import Data.Monoid
type WInt = Writer Any Int
bplus :: Int -> Int -> WInt
bplus a b =
do
tell (Any (a == 0 || b == 0)) ;
return (a+b)
wbplus :: WInt -> WInt -> WInt
wbplus wa wb =
do
a <- wa ;
b <- wb ;
tell (Any (a == 0 || b == 0)) ;
return (a+b)
ex0 = runWriter (bplus 1 2)
ex1 = runWriter (bplus 0 2)
ex2 = runWriter (wbplus (return 1) (return 2))
ex3 = runWriter (wbplus (return 0) (return 2))
ex4 = runWriter (wbplus (wbplus (return 1) (return 2)) (return 2))
ex5 = runWriter (wbplus (wbplus (return 0) (return 2)) (return 2))
ex6 = runWriter (wbplus (wbplus (return 1) (return 2)) (return 0))
Я немного не уверен, каков наилучший способ реализовать это. Некоторые вопросы:
Должен ли я определить все операции, как я сделал для
bplus
или как дляwbplus
, Позже, кажется, облегчает композицию. Но использоватьfoldM
бинарный оператор должен иметь типInt -> Int -> WInt
,Какой будет подходящий тип для списков:
Writer Any [Int]
или же[Wint]
?
Любые предложения или мысли приветствуются.
1 ответ
Вы можете получить bplus
от wbplus
и наоборот, используя соответствующие монадические операции:
import Control.Monad
apM2 :: Monad m => (a -> b -> m c) -> m a -> m b -> m c
apM2 f ma mb = do
a <- ma
b <- mb
f a b
pureM2 :: Monad m => (m a -> m b -> m c) -> a -> b -> m c
pureM2 f a b = f (return a) (return b)
Они противоположны друг другу, что видно из типовых подписей их композиций:
ghci> :t pureM2 . apM2
pureM2 . apM2 :: Monad m => (a -> b -> m c) -> a -> b -> m c
ghci> :t apM2 . pureM2
apM2 . pureM2 :: Monad m => (m a -> m b -> m c) -> m a -> m b -> m c
Теперь вы можете определить wbplus = apM2 bplus
или же bplus = pureM2 wbplus
, Нет определенного ответа, какой из них лучше, используйте свой вкус и суждение. TemplateHaskell идет с wbplus
подход и определяет все операции для работы со значениями в Q
монада. См. Language.Haskell.TH.Lib.
относительно [m a]
против m [a]
, вы можете идти только в одном направлении (через sequence :: Monad m => [m a] -> m [a]
). Хотели бы вы когда-нибудь пойти в противоположном направлении? Вы заботитесь о том, чтобы отдельные значения имели свои собственные флаги или вы бы предпочли аннотировать вычисления в целом флагами?
Реальный вопрос в том, какова ваша ментальная модель для этого? Однако давайте подумаем о некоторых последствиях каждого выбора дизайна.
Если вы решите представлять каждое значение как
Writer Any a
и все операции работают с ним, вы можете начать сnewtype
:{-# LANGUAGE GeneralizedNewtypeDeriving #-} import Control.Monad.Writer newtype Value a = Value (Writer Any a) deriving (Functor, Applicative, Monad)
Теперь вы можете определить экземпляры стандартных классов типов для ваших значений:
instance (Num a, Eq a) => Num (Value a) where va + vb = do a <- va b <- vb (Value . tell . Any) (b == 0 || a == 0) return (a + b) (*) = liftM2 (*) abs = fmap abs signum = fmap signum negate = fmap negate fromInteger = return . fromInteger instance Monoid a => Monoid (Value a) where mempty = pure mempty mappend = liftM2 mappend
Для EDSL это дает огромное преимущество: краткость и синтаксическая поддержка от компилятора. Теперь вы можете написать
getValue (42 + 0)
вместоwbplus (pure 42) (pure 0)
,Если вместо этого вы не думаете о флагах как о части своих значений и скорее рассматриваете их как внешний эффект, лучше использовать альтернативный подход. Но вместо того, чтобы написать что-то вроде
Writer Any [Int]
, используйте соответствующие классы изmtl
:MonadWriter Any m => m [Int]
, Таким образом, если позже вы обнаружите, что вам нужно использовать другие эффекты, вы можете легко добавить их к некоторым (но не ко всем) операциям. Например, вы можете поднять ошибку в случае деления на ноль:data DivisionByZero = DivisionByZero divZ :: (MonadError DivisionByZero m, Fractional a, Eq a) => a -> a -> m a divZ a b | b == 0 = throwError DivisionByZero | otherwise = pure (a / b) plusF :: (MonadWriter Any m, Num a, Eq a) => a -> a -> m a plusF a b = do tell (Any (b == 0 || a == 0)) return (a + b)
Теперь вы можете использовать
plusF
а такжеdivZ
вместе в одной монаде, хотя они имеют разные эффекты. Если позже вам понадобится интегрироваться с какой-либо внешней библиотекой, эта гибкость пригодится.
Я не слишком задумывался об этом, но, возможно, вы могли бы объединить эти подходы, используя что-то вроде newtype Value m a = Value { getValue :: m a }
, Удачи в изучении дизайна пространства:)