考虑以下示例函数,它们都向纯输入添加随机值:
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)
但会对结果感到非常惊讶!
我对如何编写一元代码感到非常矛盾。在许多情况下,“版本 2”似乎更好,但我最近遇到了一个需要“版本 1”签名的示例。*
什么样的因素会影响我的设计决策 w.r.t.一元签名?有没有办法调和“通用签名”和“自然、干净、易于使用、难以误用的语法”这两个明显冲突的目标?
*:我写了一个函数
foo :: a -> m b
,这在(字面上)很多年都工作得很好。当我试图将它整合到一个新的应用程序(带有 HOAS 的 DSL)中时,我发现我做不到,直到我意识到 foo
可以重写为具有签名 m (a -> b)
.突然我的新应用程序成为可能。
最佳答案
这取决于多种因素:
了解
Int -> m Int
之间区别的关键和 m (Int -> Int)
是在前一种情况下,效果( m ...
)可以取决于输入参数。例如,如果 m
是 IO
, 你可以有一个启动 n
的函数导弹,其中n
是函数参数。另一方面,m (Int -> Int)
中的效果不依赖于任何东西——效果不会“看到”它返回的函数的参数。回到你的情况:你接受一个纯输入,生成一个随机数并将其添加到输入中。我们可以看到效果(生成随机数)不依赖于输入。这就是为什么我们可以有签名
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
.这反射(reflect)在类型层次结构中 - monad 比 applicative functors 更具体,这意味着它们为用户提供更多权力,但实现起来“更难” - 每个 monad 都是一个 applicative,但不是每个 applicative 都可以变成一个 monad .
关于haskell - 关于写单子(monad)签名的建议,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/43280155/