考虑以下示例函数,它们都向纯输入添加随机值:
addRand1 :: (MonadRandom m) => m (Int -> Int)
addRand2 :: (MonadRandom m) => Int -> m Int -- *can* be written as m (Int -> Int)
很容易将addRand1
转换为与addRand2
,but not vice versa具有相同签名的函数。
对我来说,这提供了强有力的证据,证明我应该addRand1
写addRand2
。在此示例中,addRand1
具有更真实/通用的类型,通常捕获Haskell中的重要抽象。
虽然拥有“正确”签名似乎是函数式编程的一个重要方面,但我也有很多实际原因可以解释为什么addRand2
可能是更好的签名,即使它可以是用addRand1
签名写的。
使用接口:
class FakeMonadRandom m where
getRandom :: (Random a, Num a) => m a
getRandomR1 :: (Random a, Num a) => (a,a) -> m a
getRandomR2 :: (Random a, Num a) => m ((a,a) -> a)
突然getRandomR1
似乎“更一般”,因为与getRandom
相比,它允许更多实例(重复调用getRandomR2
直到结果在范围内),这似乎需要某种减少技术。
addRand2
更易于编写/阅读:
addRand1 :: (MonadRandom m) => m (Int -> Int)
addRand1 = do
x <- getRandom
return (+x) -- in general requires `return $ \a -> ...`
addRand2 :: (MonadRandom m) => Int -> m Int
addRand2 a = (a+) <$> getRandom
addRand2
更易于使用:
foo :: (MonadRandom m) => m Int
foo = do
x <- addRand1 <*> (pure 3) -- ugly syntax
f <- addRand1 -- or a two step process: sequence the function, then apply it
x' <- addRand2 3 -- easy!
return $ (f 3) + x + x'
addRand2
更难以误用:考虑getRandomR :: (MonadRandom m, Random a) => (a,a) -> m a
。对于给定的范围,我们可以重复采样并获得不同的结果,这可能是我们想要的。但是,如果我们改为getRandomR :: (MonadRandom m, Random a) => m ((a,a) -> a)
,我们可能会想写
do
f <- getRandomR
return $ replicate 20 $ f (-10,10)
但结果会非常惊讶!
我对如何编写monadic代码感到非常矛盾。在许多情况下,“版本2”似乎更好,但我最近遇到了需要“版本1”签名的示例。*
什么样的因素会影响我的设计决策w.r.t. monadic签名?有没有办法调和“一般签名”和“自然,干净,易于使用,难以滥用的语法”这些明显冲突的目标?
*:我写了一个函数foo :: a -> m b
,它在(字面上)很多年都很好用。当我尝试将其合并到一个新的应用程序(带有HOAS的DSL)时,我发现我无法,直到我意识到foo
可以被重写为具有签名m (a -> b)
。突然间,我的新申请成为可能。
答案 0 :(得分:5)
这取决于多种因素:
理解Int -> m Int
和m (Int -> Int)
之间差异的关键是,在前一种情况下,效果(m ...
)可能取决于输入参数。例如,如果m
为IO
,则可以使用启动n
导弹的函数,其中n
是函数参数。另一方面,m (Int -> Int)
中的效果并不取决于任何事物 - 效果不会看到&#34;&#34;它返回的函数的参数。
回过头来看你的情况:你接受一个纯粹的输入,生成一个随机数并将其添加到输入中。我们可以看到效果(产生随机数)并不取决于输入。这就是我们可以签名m (Int -> Int)
的原因。例如,如果任务是生成n
个随机数,则签名Int -> m [Int]
会起作用,但m (Int -> [Int])
不会。
关于可用性,Int -> m Int
在monadic上下文中更常见,因为大多数monadic组合器都期望a -> b -> ... -> m r
形式的签名。例如,您通常会写
getRandom >>= addRand2
或
addRand2 =<< getRandom
将随机数添加到另一个随机数。
像m (Int -> Int)
这样的签名不太常见于monad,但通常与applicative functors一起使用(每个monad也是一个应用函子),其中效果不依赖于参数。特别是,运算符<*>
在这里可以很好地工作:
addRand1 <*> getRandom
关于一般性,签名会影响使用或实施它的难度。正如您所观察到的,addRand1
从调用方的角度来看更为通用 - 如果需要,它始终可以将其转换为addRand2
。另一方面,addRand2
不太通用,因此更容易实现。在您的情况下,它并不真正适用,但在某些情况下,可能会实现像m (Int -> Int)
这样的签名,而不是Int -> m Int
。这反映在类型层次结构中 - monad比applicative functors更具体,这意味着它们为用户提供更多的权力,但是更难以&#34;实施 - 每个monad都是一个应用程序,但不是每个应用程序都可以成为monad。
答案 1 :(得分:2)
很容易将
addRand1
转换为与addRand2具有相同签名的函数,但not vice versa。
咳咳。
-- | Adds a random value to its input
addRand2 :: MonadRandom m => Int -> m Int
addRand2 x = fmap (+x) getRand
-- | Returns a function which adds a (randomly chosen) fixed value to its input
addRand1 :: MonadRandom m => m (Int -> Int)
addRand1 = fmap (+) (addRand2 0)
为什么会这样?好吧,addRand1
的工作是提出一个随机选择的值,并部分应用+
。将随机数添加到虚拟值是提出随机数的一种非常好的方法!
我想你可能会对@chi's statement中的量词感到困惑。他没有说
对于所有monad
m
以及类型a
和b
,您无法将a -> m b
转换为m (a -> b)
∀ m a b. ¬ ∃ f. f :: Monad m => (a -> m b) -> m (a -> b)
他说
对于所有monad
a -> m b
以及m (a -> b)
和m
类型,您无法将a
转换为b
。
¬ ∃ f. f :: ∀ m a b. Monad m => (a -> m b) -> m (a -> b)
没有什么能阻止您为{em>特定 monad (a -> m b) -> m (a -> b)
以及m
和a
类型的b
撰写sendToBack
。
答案 2 :(得分:1)
答案 3 :(得分:0)
很容易将
addRand1
转换为与addRand2
具有相同签名的函数,但反之亦然。
这是事实,但不要忘记这种“转换”不必保留预期的语义。
我的意思是,如果我们有foo :: IO (Int -> ())
,我们可以写
bogusPrint :: Int -> IO ()
bogusPrint x = ($ x) <$> foo
但是这将对所有x
执行相同的IO操作!很难用。
你的论点似乎是“我可以定义一个对象x :: A
或另一个y :: B
。嗯,我也知道我可以写f :: A->B
,所以x :: A
更通用因为y可以让y = f x :: B
“。就个人而言,我认为这是一个很好的方法来推理你的代码!但是,必须检查获得y
的{{1}}是否是预期的f x
。仅仅因为类型匹配,它并不意味着值是正确的。
所以,在一般情况下,我认为这取决于手边的monad。我写了x
和y
(正如Daniel Wagner建议的那样),然后检查一个是否真的比另一个更通用 - 不仅因为类型更通用,而且因为值可以(有效)从y
恢复x
。