【问题标题】:Understanding foldTree function's type derivation理解 foldTree 函数的类型推导
【发布时间】:2021-07-17 05:14:27
【问题描述】:

看看Data.Tree中出现的这个定义:

foldTree :: (a -> [b] -> b) -> Tree a -> b
foldTree f = go where
    go (Node x ts) = f x (map go ts)

我的具体问题是:go 名称出现在(map go ts) 等式的右侧,函数的类型如何

(a -> [b] -> b)

被推断?

例如,有这行代码:

foldTree (:) (Node 1 [Node 2 []])

实例化定义:

foldTree (:) = go where
    go (Node 1 [Node 2 []]) = (:) 1 (map go [Node 2 []])

(:) 1 (map go [Node 2 []]) 没有被完全评估,所以我只看到(:) 1 的类型为Num a => [a] -> [a]。然而,缺少一个空白,为了填补它,递归应该完成。所以,计算类型似乎有一些循环性。

非常感谢任何见解。

【问题讨论】:

  • go 的类型是Tree a -> b(a -> [b] -> b) 类型的唯一内容是 f,该类型直接来自代码中的类型签名。
  • 谢谢。我想知道f 类型是如何被推断出来的,因为只填充了它的第一个参数。第二个参数是go 函数在其子树中的映射。
  • foldTree 的类型签名不需要推断其类型。 Haskell 中的类型推断比你想象的更聪明!

标签: haskell types tree type-inference fold


【解决方案1】:

这是针对此问题的类型推导电子表格。将所有东西放在一起可能会更容易:

data Tree a = Node a [Tree a]

foldTree f = go  where
  go (Node x ts) = f x (map go ts)

foldTree f t = go t  where
  go t@(Node x ts) = f x (map go ts)
--------------------------------------
     Tree a                                  t  :: Tree a       from `data Tree a`
             a         a                     x  :: a            from `data Tree a`
              [Tree a]           [Tree a]    ts :: [Tree a]     from `data Tree a`
                              Tree a -> b    go :: Tree a -> b  from type of `map`
                          [b]                                   from type of `map`
               f  :: a -> [b] -> r                some resulting type `r`
               go :: Tree a   -> r                from definition of `go`
               go :: Tree a   -> b                as derived above
                     ----------------------
                               r ~ b              `r` and `b` are the same type
                     ----------------------
               f  :: a -> [b] ->   b              more precise type now
             ------------------------------
foldTree :: (a -> [b] -> b) -> Tree a -> b
foldTree    f               =  go

这里我们从 t 的类型开始,但无论从什么开始派生类型都将始终相同,直到对其类型变量进行一致的重命名。


这个符号的解释:我们从上到下,从左到右阅读。我尝试以暗示的方式对齐相关的事物垂直。想象一下,你用它的类型来注释每个实体,在纸上写下那个实体附近的类型,上面有所有的代码(比如用蓝色写的),用更小的字母和不同的颜色(比如红色)。相反,我们在该实体下编写类型,垂直对齐(即使它们之间还有其他一些线)。

例如,推导部分的第二行在xs 下写了两个as,旁边还有一些注释。这一切意味着什么?

每个a 都写在x 下。就好像我们已经用它的类型注释了每个xa。由于x 在这两种情况下都是相同的x,因此类型是相同的类型。我们对它了解不多,所以我们称之为a——一个类型变量,如果它在推理过程中发生得更远,以后可以取另一个更精细的含义。然后,有一个旁注:x :: a 是常规的 Haskell 表示法,这意味着 x 具有类型 a。所以它说的是同样的事情,但更正式。然后有一个非正式的说明,说明这个决定是从哪里来的。

【讨论】:

  • 谢谢!你能告诉我应该怎么读吗?
  • 从上到下,从左到右。我试图以一种暗示的方式垂直对齐相关的东西。想象一下,你用它的类型注释每个实体,在纸上写下那个实体附近的类型,上面有所有的代码,用更小的字母和不同的颜色。相反,我们将类型 under 写入该实体,垂直对齐,即使中间有几行。
  • 谢谢。这看起来很有趣,我很感激你分享它。但是,我不明白。例如,第二行有两个“a”和“x :: a”。这是什么意思?
  • 感谢您的提问!我已经编辑了更多说明。 :)
  • 如果您需要更多说明,请随时询问。 :)
【解决方案2】:

Haskell 的类型推断非常聪明!我不能告诉你这实际上是如何推断的,但让我们来看看它可能是怎样的。现实可能不会太远。在这种情况下实际上不需要类型签名。

foldTree f = go where
    go (Node x ts) = f x (map go ts)

foldTree 被定义为接受一个参数,go 被定义为接受一个参数,所以我们从一开始就知道这些是函数。

foldTree :: _a -> _b
foldTree f = go where
    go :: _c -> _d
    go (Node x ts) = f x (map go ts)

现在我们看到f 是用两个参数调用的,所以它实际上必须是(至少)两个参数的函数。

foldTree :: (_x -> _y -> _z) -> _b
foldTree f = go where
    go :: _c -> _d
    go (Node x ts) = f x (map go ts)

由于foldTree f = gogo :: _c -> _d,结果类型_b实际上必须是_c -> _d *:

foldTree :: (_x -> _y -> _z) -> _c -> _d
foldTree f = go where
    go :: _c -> _d
    go (Node x ts) = f x (map go ts)

传递给f_y 类型)的第二个参数是map go ts。由于go :: _c -> _d_y必须是[_d]

foldTree :: (_x -> [_d] -> _z) -> _c -> _d
foldTree f = go where
    go :: _c -> _d
    go (Node x ts) = f x (map go ts)

go 将其参数与Node x ts 匹配,而NodeTree 的数据构造函数,因此go 的参数(_c) 必须是Tree

foldTree :: (_x -> [_d] -> _z) -> Tree _p -> _d
foldTree f = go where
    go :: Tree _p -> _d
    go (Node x ts) = f x (map go ts)

Node 构造函数的第一个字段作为f 的第一个参数传递,所以_x_p 必须相同:

foldTree :: (_x -> [_d] -> _z) -> Tree _x -> _d
foldTree f = go where
    go :: Tree _x -> _d
    go (Node x ts) = f x (map go ts)

由于go _被定义为f _ _,所以它们必须有相同类型的结果,所以_z就是_d

foldTree :: (_x -> [_d] -> _d) -> Tree _x -> _d
foldTree f = go where
    go :: Tree _x -> _d
    go (Node x ts) = f x (map go ts)

哇。现在编译器检查以确保这些类型有效(它们确实有效),并将它们从“元变量”(意味着推理引擎不知道它们代表什么类型的变量)“概括”为量化类型变量(肯定是多态的),它得到

foldTree :: forall a b. (a -> [b] -> b) -> Tree a -> b
foldTree f = go where
    go :: Tree a -> b
    go (Node x ts) = f x (map go ts)

实际情况要复杂一些,但这应该会给你一个要点。

[*] 这一步有点作弊。我忽略了一个名为“let generalization”的功能,在这种情况下不需要它,实际上它被 GHC Haskell 中的几个语言扩展禁用。

【讨论】:

  • 只是想说我非常感谢您的回答。太棒了。谢谢!
猜你喜欢
  • 2018-03-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多