Haskell 为表达式编写 Monad
Haskell Write Monad for expressions
我正在尝试设计嵌入式语言,其中操作可以根据值引发某些标志。我预见到对标量值和向量(例如映射、折叠等)的操作。我的想法是使用 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
那样定义所有操作? Laters 似乎让组合更容易。但是要使用 foldM
二元运算符应该具有类型 Int -> Int -> WInt
.
列表的适当类型是什么:Writer Any [Int]
或 [Wint]
?
如有任何建议或想法,我们将不胜感激。
您可以使用适当的一元运算从 wbplus
导出 bplus
,反之亦然:
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
monad 中的值。参见 Language.Haskell.TH.Lib。
关于[m a]
vs 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)
现在您可以在一个 monad 中同时使用 plusF
和 divZ
,尽管它们有不同的效果。如果您以后发现自己需要与一些外部库集成,这种灵活性会派上用场。
现在,我没有考虑太多,但也许您可以使用 newtype Value m a = Value { getValue :: m a }
之类的方法组合这些方法。祝你探索设计好运 space :)
我正在尝试设计嵌入式语言,其中操作可以根据值引发某些标志。我预见到对标量值和向量(例如映射、折叠等)的操作。我的想法是使用 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
那样定义所有操作? Laters 似乎让组合更容易。但是要使用foldM
二元运算符应该具有类型Int -> Int -> WInt
.列表的适当类型是什么:
Writer Any [Int]
或[Wint]
?
如有任何建议或想法,我们将不胜感激。
您可以使用适当的一元运算从 wbplus
导出 bplus
,反之亦然:
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
monad 中的值。参见 Language.Haskell.TH.Lib。
关于[m a]
vs 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)
现在您可以在一个 monad 中同时使用
plusF
和divZ
,尽管它们有不同的效果。如果您以后发现自己需要与一些外部库集成,这种灵活性会派上用场。
现在,我没有考虑太多,但也许您可以使用 newtype Value m a = Value { getValue :: m a }
之类的方法组合这些方法。祝你探索设计好运 space :)