haskell - 关于写单子(monad)签名的建议

标签 haskell monads

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

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的签名。
  • 带接口(interface):
    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)
    

    突然getRandomR1getRandom 相比,它允许更多实例(例如,重复调用 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) .突然我的新应用程序成为可能。

    最佳答案

    这取决于多种因素:

  • 哪些签名实际上是可能的(这里都是)。
  • 什么签名好用。
  • 或者更一般地说,如果您想拥有最通用的接口(interface)或双重最通用的实现。

  • 了解Int -> m Int之间区别的关键和 m (Int -> Int)是在前一种情况下,效果( m ... )可以取决于输入参数。例如,如果 mIO , 你可以有一个启动 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/

    相关文章:

    scala - monad 的一些语言和用途

    Javascript:如何用更实用的模式替换嵌套的 if/else?

    windows - 在 Haskell 中,如何在 Windows 上安装编码包?

    haskell - Haskell 中写入文件递归实现

    haskell - 对 "Learn you a Haskell"上的 State Monad 代码感到困惑

    haskell - 等式推理与喜结连理

    java - Java 中的 Null 安全取消引用,如 ?.在 Groovy 中使用 Maybe monad

    haskell - 恒等仿函数到底是做什么的?

    haskell - 主要功能错误

    haskell - 无法启动 "yesod devel"