关于撰写monadic签名的建议

时间:2017-04-07 14:04:46

标签: haskell monads

考虑以下示例函数,它们都向纯输入添加随机值:

addRand1 :: (MonadRandom m) => m (Int -> Int)

addRand2 :: (MonadRandom m) => Int -> m Int -- *can* be written as m (Int -> Int)

很容易将addRand1转换为与addRand2but not vice versa具有相同签名的函数。

对我来说,这提供了强有力的证据,证明我应该addRand1addRand2。在此示例中,addRand1具有更真实/通用的类型,通常捕获Haskell中的重要抽象。

虽然拥有“正确”签名似乎是函数式编程的一个重要方面,但我也有很多实际原因可以解释为什么addRand2可能是更好的签名,即使它可以是用addRand1签名写的。

  1. 使用接口:

    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直到结果在范围内),这似乎需要某种减少技术。

  2. 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
    
  3. 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'
    
  4. 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)
    

    但结果会非常惊讶!

  5. 我对如何编写monadic代码感到非常矛盾。在许多情况下,“版本2”似乎更好,但我最近遇到了需要“版本1”签名的示例。*

    什么样的因素会影响我的设计决策w.r.t. monadic签名?有没有办法调和“一般签名”和“自然,干净,易于使用,难以滥用的语法”这些明显冲突的目标?

    *:我写了一个函数foo :: a -> m b,它在(字面上)很多年都很好用。当我尝试将其合并到一个新的应用程序(带有HOAS的DSL)时,我发现我无法,直到我意识到foo可以被重写为具有签名m (a -> b)。突然间,我的新申请成为可能。

4 个答案:

答案 0 :(得分:5)

这取决于多种因素:

  • 实际上可能有哪些签名(此处两者都有)。
  • 哪些签名便于使用。
  • 或者更一般地说,如果你想拥有最通用的界面或双重最常见的实现。

理解Int -> m Intm (Int -> Int)之间差异的关键是,在前一种情况下,效果(m ...)可能取决于输入参数。例如,如果mIO,则可以使用启动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以及类型ab,您无法将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)以及ma类型的b撰写sendToBack

答案 2 :(得分:1)

Why not both?

或者,用英语:为什么不两者?两种签名都很少有可能,但是当它们存在时,每个版本在不同的上下文中都很有用。

答案 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。我写了xy(正如Daniel Wagner建议的那样),然后检查一个是否真的比另一个更通用 - 不仅因为类型更通用,而且因为值可以(有效)从y恢复x