类型同义词在函数中展开?

时间:2018-09-29 21:50:29

标签: haskell

西蒙·马洛(Simon Marlow)读的书“ Parallel and Concurrent Programming in Haskell ”我遇到了一个不确定的事情,为什么它以GHC的方式工作。即:

evalPair :: Strategy a -> Strategy b -> Strategy (a,b)
evalPair sa sb (a,b) = do
  a' <- sa a
  b' <- sb b
  return (a',b')

在类型级别具有两个输入参数(我看到Strategy aStrategy b的方式)和一个输出-Strategy (a,b)。 但是下面的行有三个参数:sasb(a,b)。令人困惑。

但是由于Strategy是类型同义词:

type Strategy a = a -> Eval a

我认为,如果我将策略展开为(a->评估a),可能会更清楚。所以:

evalPair :: (a -> Eval a) -> (b -> Eval b) -> (a,b) -> Eval (a,b)

和(末尾添加了括号)

evalPair :: (a -> Eval a) -> (b -> Eval b) -> ((a,b) -> Eval (a,b))

都编译。

然后我以这种方式编写它,并且借助Stack工具提示,发现函数evalPair返回的内容(故意返回到Strategy以使其更加混乱)不是 Strategy (a,b) ,而是 Eval (a,b)

evalPair :: Strategy a -> Strategy b -> Strategy (a,b)
evalPair sa sb (a,b) = 
let res = do
          a' <- sa a
          b' <- sb b
          return (a', b')
in res

因此很明显,编译器从类型同义词中解开了最后一个参数,但仅解开了最后一个参数-因为我们不需要为Strategy a提供值 和Strategy b

这是我的问题:

  1. 在哪里可以获得有关此编译器行为的更多信息?为什么函数返回Eval a,即使它说它返回Strategy a

  2. 如果发生拆包,那为什么我不需要(实际上也不能)像这样提供Strategy aStrategy b的值:

    evalPair :: Strategy a -> Strategy b -> Strategy (a,b)
    evalPair a sa a sb (a,b) = do
      a' <- sa a
      b' <- sb b
      return (a',b')
    

2 个答案:

答案 0 :(得分:4)

给出类型同义词

type Strategy a = a -> Eval a

和类型

Strategy a -> Strategy b -> Strategy (a,b)

我们可以通过将同义词的每次使用替换为其定义来对类型进行“解糖”:

(a -> Eval a) -> (b -> Eval b) -> ((a,b) -> Eval (a,b))

请注意,在此处必须使用括号以阐明正在发生的情况。函数evalPair仍带有两个参数。它的两个参数是两个 function 。如果我在视觉上将类型与其相应的参数对齐,则可能会更清楚:

evalPair :: (a -> Eval a) -> (b -> Eval b) -> (a,b) -> Eval (a,b)
evalPair    sa               sb               (a,b)  = ...

因此,sa的类型为a -> Eval a,而sb的类型为b -> Eval b

请注意,Haskell报告指出:

  

类型同义词是一种方便但严格的句法机制,可使类型签名更具可读性。同义词及其定义是完全可以互换的

因此,编译器可以自由地“包装”或“解开”类型的同义词,因为它们是“完全可互换的”。

您可以在Haskell报告的第4.2.2节中了解有关类型同义词的信息: https://www.haskell.org/onlinereport/haskell2010/haskellch4.html#x10-730004.2.2

答案 1 :(得分:4)

您误解了Haskell函数的工作原理。

所有Haskell函数只有个参数。如果函数f的类型为A,并且返回类型为B,则我们将写为f :: A -> B

但是,当然,我们可以编写带有多个参数的函数,但是以一种聪明的方式称为currying。如果我们想要一个函数g接受一个A和一个B并产生一个C,我们实际上编写了一个接受A的函数,并且然后返回一个接受B并返回C的函数。

我们将其写为g :: A -> (B -> C)。但是,由于这是我们通常要对函数执行的操作,因此Haskell编译器将读取签名g :: A -> B -> C相同。

这意味着当我们写A -> B -> C -> ... -> Z时,我们实际上是指A -> (B -> (C -> (... -> Z...)))

但是,如果我们写g :: (A -> B) -> C这不一样!括号很重要! g现在是一个函数,它接受一个函数并产生一个C

现在,让我们检查这个具体示例:

 evalPair :: (a -> Eval a) -> (b -> Eval b) -> (a,b) -> Eval (a,b)

我读为:

  

函数evalPair具有以下功能:

     
      
  • a -> Eval a类型的事物
  •   
  • b -> Eval b类型的事物
  •   
  • (a,b)类型的事物
  •   
     

并产生Eval (a,b)类型的东西。

所以声明自然看起来像

evalPair first_arg second_arg third_arg = -- ...

evalPair a sa a sb (a,b) = -- ...是荒谬的,因为它需要5个参数,因此evalPair的类型为a -> b -> c -> (d, e) -> f,与我们的期望完全不同。