【发布时间】:2017-04-07 14:04:46
【问题描述】:
考虑以下示例函数,它们都向纯输入添加随机值:
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 可能是一个更好的签名,即使它可以写成addRand1s 签名。
-
带接口:
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)与
getRandomR2相比,getRandomR1突然允许更多实例(例如,重复调用getRandom直到结果在范围内)似乎“更通用”,这似乎需要某种排序还原技术。 -
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)。突然间我的新应用成为可能。
【问题讨论】:
-
这两个签名在语义上确实不同。您是根据输入选择要执行的操作,还是在执行诸如读取翻译文件并返回执行翻译的函数之类的操作?
a -> m b的使用肯定比另一个多得多。 -
@Lazersmoke 我给出了两者的定义。可以看到一元动作不依赖于
addRand2中的输入;它的行为就像addRand1。我的感觉是a -> m b的使用频率也更高;我试图理解为什么会这样。 -
你的函数
addRand1基本上只是使用 Functor 实例,它是每个 Monad 的约束。您可以将其重写为:addRand1 = fmap (+) getRandom。由于getRandom无论如何都需要在 Monad 上下文中,而不仅仅是 Functor,我认为以第二种方式编写函数并没有那么强大。如果你想使用addRand2来实现无点风格,你可以这样做:addRand2 = liftM2 (+) getRandom . return但你的版本更具可读性:-) -
@Boomerang “因为 getRandom 无论如何都需要在 Monad 上下文中,而不仅仅是 Functor,我认为以第二种方式编写你的函数并没有那么强大。”你能再解释一下吗?我在注释中举了一个例子,解释了为什么我需要“第一种方式”签名。
-
FWIW、
(<*>) :: Applicative m => m (a -> b) -> m a -> m b和(=<<) :: Monad m => (a -> m b) -> m a -> m b,所以你可以说一个是“应用方式”,另一个是“一元方式”。