被Monad和Functor实例所吸引

11
我对在ghci中查找(->)的信息感到非常好奇。它说:
data (->) a b -- Defined in `GHC.Prim`

到目前为止还不错,但当它说 - 时,情况变得非常有趣。

instance Monad ((->) r) -- Defined in `GHC.Base`
instance Functor ((->) r) -- Defined in `GHC.Base`

这意味着什么?为什么 GHC 将其定义为一个 Monad 和 Functor 的实例,适用于 (->)

3个回答

13

一开始可能会有些困惑,但重要的概念是记住 (->) 不是一个单子或函子,但是 (->) r 是。MonadFunctor 类型都具有 * -> * 的种类,因此它们只期望一种类型参数。

这意味着 (->) rfmap 看起来像:

fmap g func = \x -> g (func x)

也被称为

fmap g func = g . func

这只是普通的函数组合!当你对func使用fmap g时,你通过应用g来改变输出类型。在这种情况下,如果func的类型为a -> b,那么g必须具有类似b -> c的类型。

Monad实例则更加有趣。它允许您在函数应用“之前”使用其结果。帮助我理解的一个例子是:

f :: Double -> (Double,Double)
f = do
    x1 <- (2*)
    x2 <- (2+)
    return (x1, x2)

> f 1.0
(2.0, 3.0)

这个函数会将隐式参数应用于绑定右侧的每个函数,对于f传入1.0,它会将值2 * 1.0绑定到x1,将2 + 1.0绑定到x2,然后返回(x1, x2)。这使得将单个参数应用于多个子表达式变得非常容易。该函数等效于

f' x = (2 * x, 2 + x)

这有什么用处呢?一个常见的用法是Reader Monad,它只是一个新的类型包装器,包装了(->) rReader Monad 使得在整个应用程序中应用一个静态全局配置变得很容易。您可以编写类似以下的代码:

[Translated]:

这个有什么用处呢?其中一个常见的用途是 Reader monad,它只是一个对 (->) r 的新型封装。 Reader monad 使得在整个应用程序中应用一个静态的全局配置变得很容易。你可以编写如下代码:

myApp :: Reader Config ()
myApp = do
    config <- ask
    -- Use config here
    return ()

然后使用 runReader myApp initialConfig 运行您的应用程序。 您可以轻松编写 Reader Config 单子中的操作,将它们组合在一起,并且所有这些操作都可以访问全局只读配置。 另外,还有一个伴随的 ReaderT 单子转换器,它允许您将其构建到转换器堆栈中,让您拥有非常复杂的应用程序,并轻松访问静态配置。


感谢分享您的答案。其中“它允许您在应用函数之前使用函数应用的结果”的部分最具启发性。 - Yogesh Sajanikar
2
f :: Double -> Double 应该改为 f :: Double -> (Double, Double) - The Internet
1
@TheInternet 感谢您修复了那个问题,我开始编写函数的方式和结束它的方式不同,然后忘记回去修复它。 - bheklilr
@YogeshSajanikar 很高兴我能帮到你 =) 就像我说的,这是让我真正理解的关键。老实说,我并不经常使用它,但当我看到一个非常复杂的函数,其中一个单一参数应用于许多子表达式时,我会重构以使用do符号。偶尔它确实可以清理您的代码并使其更易读,前提是读者了解(->) a单子的作用! - bheklilr
2
我使用这个方式的一个场合是定义“谓词组合器” ,例如 (<||>) = liftM2 (||)。现在你可以很容易地构建组合谓词,比如 isAlpha <||> isLetter <||> (== 'c') :: Char -> Bool 这是一个非常傻的例子。对我来说, satisfy $ isAlpha <||> isLetter <||> (== 'c') 看起来比 satisfy $ \c -> isAlpha c || isLetter c || c == 'c' 更好看。 - J. Abrahamson

5

我认为如果Haskell总是允许类型运算符的部分,它会少一些困惑:

data a->b

instance Monad (r -> )

看起来更自然。

简单解释:我认为考虑特殊情况Monad (Bool -> )非常有帮助,它基本上是一个两个元素的容器类型。它有两个元素。

\case
  False -> elem1
  True -> elem2

因此,您可以将函子实例与列表的实例视为相同:在“所有包含的元素”上进行映射。

适用和单子实例略有不同,明确“容器转换”可能会更有帮助:

data Pair a = Pair a a

instance Functor Pair where
  fmap f (Pair a b) = Pair (f a) (f b)

instance Monad Pair where
  return a = Pair a a
  join (Pair (Pair a _) (Pair _ b))
      = Pair       a            b

Applicative/Monad实例在不断的包装和压缩过程中是有意义的。作为一个容器,(r ->)的大小是恒定的,因此强制要求MonadApplicative实例反映这一点。 - J. Abrahamson
@leftaroundabout,关于Bool的示例真的很有帮助。 - Yogesh Sajanikar
@J.Abrahamson 你能详细说明一下吗? - Yogesh Sajanikar
1
考虑将pure/return应用于[],它的规范定义是产生一个单例列表。然而,不可能有一个“单例”(Bool ->)容器,因此这种直觉会让你感到困惑。大多数“容器”类型都是这样行为的,但像infinite stream这样的东西却不是这样——它总是必须有无限多的元素,所以pure/return只是使它们都成为相同的值。你对Applicative/Monad的直觉与我们在(r ->)中看到的完全相同。 - J. Abrahamson
1
整个概念被称为“可表示函子”。每当一个函子 f 同构于某个源数据的某个函数(即 存在 r . forall a . f a ~ r -> a)时,我们称其为可表示函子,可能会认为它是“固定大小”的(大小由所选的 r 确定),并且将具有类似于 (r ->)Applicative/Monad 实例。 - J. Abrahamson
3
通常情况下,Edward Kmett 有一个要查阅的 Haskell 库:reprsentable-functors。直接从 Haddocks 上面可以看到以下内容:"在 Haskell 类型范畴上可表示的自函子同构于 Reader Monad 并且可以免费继承非常多的属性。" 省略 "endo" 和 "category" 部分,你已经有了大概的理解。 - J. Abrahamson

1

2
这怎么解释呢? - The Internet
1
@TheInternet 抱歉,我实际上误读了问题。应该是“在哪里”而不是“为什么”。但是代码本身不就说明了一切吗? :) - kosmikus

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接