Haskell:如何手动推断表达式的类型

时间:2013-01-15 10:28:44

标签: haskell types type-inference unification

给出了Haskell函数:

head . filter fst

现在的问题是如何手动“手动”找到类型。如果我让Haskell告诉我我得到的类型:

head . filter fst :: [(Bool, b)] -> (Bool, b) 

但是我想了解它是如何工作的,只使用已定义如下的已使用函数的签名:

head :: [a] -> a
(.) :: (b -> c) -> (a -> b) -> a -> c
filter :: (a -> Bool) -> [a] -> [a]
fst :: (a, b) -> a

编辑:这么多很好的解释......选择最好的一个并不容易!

5 个答案:

答案 0 :(得分:15)

使用通常称为unification的过程来推断类型。 Haskell属于Hindley-Milner家族,这是统一的 它用于确定表达式类型的算法。

如果统一失败,则表达式是类型错误。

表达式

head . filter fst

通过。让我们手动进行统一,看看我们为什么会这样做 我们得到了什么。

让我们从filter fst开始:

filter :: (a -> Bool) -> [a] -> [a]
fst :: (a' , b') -> a'                -- using a', b' to prevent confusion

filter获取(a -> Bool),然后[a]获得另一个[a]。在表达中 filter fst,我们传递filter参数fst,其类型为(a', b') -> a'。 为此,类型fst 必须使用filter的第一个参数类型统一

(a -> Bool)  UNIFY?  ((a', b') -> a')

算法统一两个类型表达式并尝试绑定尽可能多的变量(例如a或{{ 1}})到实际类型(例如a')。

只有这样Bool才能生成有效的类型化表达式:

filter fst

filter fst :: [a] -> [a] 显然是a'。因此,变量 Bool类型解析为a'Bool可以统一到(a', b')。因此,如果aa(a', b')a', 然后Bool只是a

如果我们向(Bool, b')传递了一个不兼容的参数,例如filter(a 42), NumNum a => a的统一将作为两个表达式失败 永远不能统一到正确的类型表达。

回到

a -> Bool

这与我们正在谈论的filter fst :: [a] -> [a] 相同,所以我们替换它的位置 以前统一的结果:

a

下一位,

filter fst :: [(Bool, b')] -> [(Bool, b')]

可以写成

head . (filter fst)

所以请(.) head (filter fst)

(.)

因此,要使统一成功,

  1. (.) :: (b -> c) -> (a -> b) -> a -> c 必须统一head :: [a] -> a
  2. (b -> c)必须统一filter fst :: [(Bool, b')] -> [(Bool, b')]
  3. 从(2)我们在表达式中得到(a -> b) IS a b)`

    所以类型变量(.) :: (b -> c) -> (a -> b) -> a -> ca的值在 表达式c很容易辨别 (1)为我们提供(.) head (filter fst) :: a -> cb之间的关系:cb的列表。 我们知道ca[(Bool, b')]只能统一到c

    所以(Bool, b')成功进行了类型检查:

    head . filter fst

    <强>更新

    有趣的是,您可以从各个方面统一启动流程。 我首先选择了head . filter fst :: [(Bool, b')] -> (Bool, b') ,然后选择了filter fst(.),但是作为其他示例 显示,统一可以通过几种方式进行,与数学方式不同 证据或定理推导可以多种方式完成!

答案 1 :(得分:9)

filter :: (a -> Bool) -> [a] -> [a]使用函数(a -> Bool),相同类型的列表a,并返回该类型a的列表。

在您的定义中,您使用filter fstfst :: (a,b) -> a,因此类型

filter (fst :: (Bool,b) -> Bool) :: [(Bool,b)] -> [(Bool,b)]
推断是

。 接下来,您将结果[(Bool,b)]head :: [a] -> a一起撰写。

(.) :: (b -> c) -> (a -> b) -> a -> c是两个函数func2 :: (b -> c)func1 :: (a -> b)的组合。在你的情况下,你有

func2 = head       ::               [ a      ]  -> a

func1 = filter fst :: [(Bool,b)] -> [(Bool,b)]

所以head这里以[(Bool,b)]为参数,每个定义返回(Bool,b)。最后你有:

head . filter fst :: [(Bool,b)] -> (Bool,b)

答案 2 :(得分:8)

让我们从(.)开始吧。它的类型签名是

(.) :: (b -> c) -> (a -> b) -> a -> c

说 “给定从bc的函数,以及从ab的函数, 还有a,我可以给你一个b“。我们希望将其用于headfilter fst,所以`:

(.) :: (b -> c) -> (a -> b) -> a -> c
       ^^^^^^^^    ^^^^^^^^
         head     filter fst

现在head,这是一个从一个东西到一个数组的函数 单身。所以现在我们知道b将是一个数组, 并且c将成为该数组的一个元素。所以出于目的 我们的表达式,我们可以认为(.)具有签名:

(.) :: ([d] -> d) -> (a -> [d]) -> a -> d -- Equation (1)
                     ^^^^^^^^^^
                     filter fst

filter的签名是:

filter :: (e -> Bool) -> [e] -> [e] -- Equation (2)
          ^^^^^^^^^^^
              fst

(请注意,我已更改了类型变量的名称以避免混淆 使用a s 我们已经拥有!)这说“给定e到Bool的函数, 以及e的列表,我可以为您提供e s的列表。函数fst 有签名:

fst :: (f, g) -> f

说,“如果一对包含fg,我可以给你一个f”。 与公式2相比,我们知道这一点 e将是一对值,是第一个元素 必须是Bool。所以在我们的表达中,我们可以想到filter 有签名:

filter :: ((Bool, g) -> Bool) -> [(Bool, g)] -> [(Bool, g)]

(我在这里所做的就是用公式2中的e替换(Bool, g)。) 表达式filter fst的类型为:

filter fst :: [(Bool, g)] -> [(Bool, g)]

回到等式1,我们可以看到(a -> [d])现在必须 [(Bool, g)] -> [(Bool, g)]a必须为[(Bool, g)]d 必须是(Bool, g)。所以在我们的表达中,我们可以将(.)视为 有签名:

(.) :: ([(Bool, g)] -> (Bool, g)) -> ([(Bool, g)] -> [(Bool, g)]) -> [(Bool, g)] -> (Bool, g)

总结:

(.) :: ([(Bool, g)] -> (Bool, g)) -> ([(Bool, g)] -> [(Bool, g)]) -> [(Bool, g)] -> (Bool, g)
       ^^^^^^^^^^^^^^^^^^^^^^^^^^    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
                head                         filter fst
head :: [(Bool, g)] -> (Bool, g)
filter fst :: [(Bool, g)] -> [(Bool, g)]

全部放在一起:

head . filter fst :: [(Bool, g)] -> (Bool, g)

除了我使用g作为类型变量而不是b之外,这相当于你所拥有的。

这可能听起来很复杂,因为我用血淋淋的细节描述了它。然而,这种推理很快成为第二天性,你可以在脑海中做到这一点。

答案 3 :(得分:3)

(向下翻页进行手动推导) 查找head . filter fst == ((.) head) (filter fst)的类型,给定 < / p>

head   :: [a] -> a
(.)    :: (b -> c) -> ((a -> b) -> (a -> c))
filter :: (a -> Bool) -> ([a] -> [a])
fst    :: (a, b) -> a

这是通过一个小的Prolog程序以纯机械的方式实现

type(head,    arrow(list(A)       , A)).                 %% -- known facts
type(compose, arrow(arrow(B, C)   , arrow(arrow(A, B), arrow(A, C)))).
type(filter,  arrow(arrow(A, bool), arrow(list(A)    , list(A)))).
type(fst,     arrow(pair(A, B)    , A)).

type([F, X], T):- type(F, arrow(A, T)), type(X, A).      %% -- application rule

在Prolog解释器中运行时自动生成

3 ?- type([[compose, head], [filter, fst]], T).
T = arrow(list(pair(bool, A)), pair(bool, A))       %% -- [(Bool,a)] -> (Bool,a)

其中类型以纯粹的语法方式表示为复合数据术语。例如。 [a] -> a类型由arrow(list(A), A)表示,可能的Haskell等效Arrow (List (Logvar "a")) (Logvar "a"),给定相应的data定义。

只使用了一个推理规则,即应用程序的推理规则,以及Prolog的结构统一,其中复合词匹配如果它们具有相同的形状且它们的成分匹配: f(a 1 2 ,... a n g(b 1 ,b 2 ,... b m 匹配iff f g n == m a i 匹配 b i ,逻辑变量可以根据需要采用任何值,但只能一次(无法更改)。

4 ?- type([compose, head], T1).     %% -- (.) head   :: (a -> [b]) -> (a -> b)
T1 = arrow(arrow(A, list(B)), arrow(A, B))

5 ?- type([filter, fst], T2).       %% -- filter fst :: [(Bool,a)] -> [(Bool,a)]
T2 = arrow(list(pair(bool, A)), list(pair(bool, A)))

以机械方式执行类型推断 手动 ,涉及将事物一个接一个地写下来,注意到一边的等价物并执行替换,从而模仿Prolog的操作。我们可以将任何->, (_,_), []等纯粹视为句法标记,而不理解它们的含义,并使用结构统一机械地执行该过程,此处只有一个rule of type inference,即。 应用程序的规则: (a -> b) c ⊢ b {a ~ c} (在等价下用(a -> b)替换cb的并置ac)。一致地重命名逻辑变量非常重要,以避免名称冲突:

(.)  :: (b    -> c ) -> ((a -> b  ) -> (a -> c ))           b ~ [a1], 
head ::  [a1] -> a1                                         c ~ a1
(.) head ::              (a ->[a1]) -> (a -> c ) 
                         (a ->[c] ) -> (a -> c ) 
---------------------------------------------------------
filter :: (   a    -> Bool) -> ([a]        -> [a])          a ~ (a1,b), 
fst    ::  (a1, b) -> a1                                    Bool ~ a1
filter fst ::                   [(a1,b)]   -> [(a1,b)]  
                                [(Bool,b)] -> [(Bool,b)] 
---------------------------------------------------------
(.) head   :: (      a    -> [     c  ]) -> (a -> c)        a ~ [(Bool,b)]
filter fst ::  [(Bool,b)] -> [(Bool,b)]                     c ~ (Bool,b)
((.) head) (filter fst) ::                   a -> c      
                                    [(Bool,b)] -> (Bool,b)

答案 4 :(得分:1)

您可以采用“技术”方式,通过大量复杂的统一步骤。或者你可以用“直观”的方式做到这一点,只看着事情并思考“好吧,我有什么在这里?这是什么期待?”等等。

好吧,filter需要一个函数和一个列表,并返回一个列表。 filter fst指定一个函数,但没有给出列表 - 所以我们仍在等待列表输入。所以filter fst正在获取一个列表并返回另一个列表。 (顺便说一下,这是一个非常常见的Haskell短语。)

接下来,.运算符将输出“管道”到head,它需要一个列表并返回该列表中的一个元素。 (第一个,当它发生时。)因此无论filter出现什么,head都会给你第一个元素。在这一点上,我们可以得出结论

head . filter foobar :: [x] -> x

但是x是什么?好吧,filter fstfst应用于列表的每个元素(以决定是保留还是抛出它)。因此fst必须适用于列表元素。并且fst期望一个2元素元组,并返回该元组的第一个元素。现在filter期待fst返回Bool,这意味着元组的第一个元素必须是Bool

把所有这些放在一起,我们得出结论

头。 filter fst :: [(Bool,y)] - &gt; (Bool,y)

什么是y?我们不知道。我们实际上并不关心!无论它是什么,上述功能都将起作用。这就是我们的类型签名。


在更复杂的例子中,弄清楚发生了什么可能更难。 (特别是当涉及奇怪的类实例时!)但是对于像这样的小型实例,涉及常见功能,你通常可以只想“好了,这里发生了什么?那里有什么?这个函数有什么期望?”并且在没有太多手动算法追逐的情况下直接回答答案。