在最后一个定义中,x的类型是什么?
在讨论此问题之前,可以先问问 GHC!GHC 7.8及以上版本支持TypedHoles,这意味着如果你在表达式(而不是模式)中放置一个下划线,然后加载或编译,你会得到一条消息,其中包含下划线的预期类型以及本地作用域变量的类型。
newtype Compose f g a = Compose { getCompose :: f (g a) }
instance (Functor f, Functor g) => Functor (Compose f g) where
fmap f (Compose x) = _
GHC现在说,省略了一些部分:
Notes.hs:6:26: Found hole ‘_’ with type: Compose f g b …
Relevant bindings include
x :: f (g a)
(bound at /home/kutta/home/Dropbox/src/haskell/Notes.hs:6:21)
f :: a -> b
(bound at /home/kutta/home/Dropbox/src/haskell/Notes.hs:6:10)
fmap :: (a -> b) -> Compose f g a -> Compose f g b
(bound at /home/kutta/home/Dropbox/src/haskell/Notes.hs:6:5)
In the expression: _
In an equation for ‘fmap’: fmap f (Compose x) = _
In the instance declaration for ‘Functor (Compose f g)’
这就是你要的:x :: f (g a)
。而且经过一些练习,TypedHoles
可以帮助你极大地理解复杂的多态代码。让我们尝试通过从头开始编写右侧来理解当前的代码。
我们已经看到洞的类型为 Compose f g b
。因此,右侧必须有一个 Compose _
:
instance (Functor f, Functor g) => Functor (Compose f g) where
fmap f (Compose x) = Compose _
新孔的类型为
f (g b)
。现在我们应该看上下文:
Relevant bindings include
x :: f (g a)
(bound at /home/kutta/home/Dropbox/src/haskell/Notes.hs:6:21)
f :: a -> b
(bound at /home/kutta/home/Dropbox/src/haskell/Notes.hs:6:10)
fmap :: (a -> b) -> Compose f g a -> Compose f g b
(bound at /home/kutta/home/Dropbox/src/haskell/Notes.hs:6:5)
目标是从上下文的成分中获得一个
f (g b)
。上面的代码清单中的
fmap
不幸地指的是正在定义的函数,这有时很有帮助,但在这里不是。我们最好知道
f
和
g
都是函子,因此它们可以被
fmap
操作。由于我们有
x :: f (g a)
,我们猜想应该对
x
进行
fmap
操作以获得
f (g b)
。
fmap f (Compose x) = Compose (fmap _ x)
现在这个映射变成了g a -> g b
。但是现在g a -> g b
很容易,因为我们有一个f :: a -> b
,而且g
是一个Functor
,所以我们还有一个fmap :: (a -> b) -> g a -> g b
,从而得到fmap f :: g a -> g b
。
fmap f (Compose x) = Compose (fmap (fmap f) x)
完成了。
总结一下这个技巧:
首先在你不知道如何进行的地方开一个洞。在这里,我们是在整个右侧放置了一个洞,但通常情况下,您对实现的大部分部分都有很好的想法,并且需要在特定的问题子表达式中放置洞。
通过查看类型,尝试缩小可能导致目标的实现范围以及不能导致目标的实现范围。填写新表达式并重新定位洞。在证明助手的术语中,这称为“细化”。
重复步骤2,直到您获得目标,此时您完成了,或者当前目标似乎不可能,在这种情况下回溯到您最后做出的非显然选择,然后尝试另一种细化。
以上技巧有时被戏称为“类型俄罗斯方块”。可能的缺点是,您可以通过玩“俄罗斯方块”来实现复杂的代码,而实际上不理解自己在做什么。有时候会陷入游戏中无法自拔的状态,那么您就必须开始真正思考这个问题。但最终,它可以让您理解本来可能难以理解的代码。
我个人一直都在经常使用TypedHoles
,而且基本上成为了一种反射动作。我已经如此依赖它,以至于有一次不得不回到GHC 7.6时感到非常不舒服(但幸运的是,甚至在那里,您也可以模拟洞口)。
fmap @f
,fmap @g
。 - Cactus