【问题标题】:Types in HaskellHaskell 中的类型
【发布时间】:2011-02-18 07:12:56
【问题描述】:

我是 Haskell 的新手,我很难理解推断类型和此类工作的原理。

map :: (a -> b) -> [a] -> [b]
(.) :: (a -> b) -> (c -> a) -> c -> b

这到底是什么意思?

foldr :: (a -> b -> b) -> b -> [a] -> b
foldl :: (a -> b -> a) -> a -> [b] -> a
foldl1 :: (a -> a -> a) -> [a] -> a

这些有什么区别?

以及推断的类型

foldr map[a] -> [a -> a] -> [a]

但是为什么会这样呢?

谢谢!

【问题讨论】:

  • 你熟悉柯里化的概念吗?
  • 我相信这是使多参数函数只接受一个参数的概念。类似于 f (x,y) = (f y) o (g x)?
  • 你可以在 ghci (interactive haskell shell) 中使用 :t 命令找出某物的类型: >> :t foldr map foldr map :: [b] -> [b -> b] -> [b]
  • “我将如何定义某事物的推断类型...”是什么意思?如果你声明了某物的类型,就不需要推断了。
  • 顺便说一句,在 Stack Overflow 上接受给出正确解决方案的答案被认为是一种很好的形式。到目前为止,您已经提出了六个问题,但没有接受任何答案。

标签: haskell types type-inference inferred-type


【解决方案1】:

在 Haskell 中,小写变量是类型变量。而Char 仅表示Charab 可能表示Char、或BoolInteger 或其他类型。关键是,所有as 都将是同一类型。并且所有bs 都将是同一类型。

例如,map 将一个函数作为其第一个参数,该函数接受一个 a 类型的变量并返回另一个 b 类型的变量。然后map 返回一个新函数,它采用a 类型的列表。这个新函数返回b 类型的列表。

假设您有一个函数increment,它将一个整数加一。 map increment [1,2,3] 最终会返回一个列表 [2,3,4]。

替换了类型变量的map 的早期定义如下所示:

map :: increment -> [1,2,3] -> [2,3,4]

increment,顺便说一句,看起来像

increment :: Integer -> Integer

之所以写得有趣,是因为你可以部分应用函数。这里我将根据地图的部分应用做一个新的功能:

incrementList = map increment

那么你可以使用

incrementList [1,2,3]

这会产生相同的结果

【讨论】:

    【解决方案2】:

    如果你举个例子

    map :: (a -> b) -> [a] -> [b]
    

    这意味着 map 需要 2 个参数

    1. 从a型到b型的函数
    2. a 类型的列表

    然后它返回

    1. b 列表

    您已经可以看到这里的模式,但是当我们替换 'a' 和 'b' 时会更清楚

    • a = 字符串
    • b = 内部

    所以这个类型定义将是

    map :: (String -> Int) -> [String] -> [Int]
    

    所以现在它是一个函数,它接受一个字符串并返回一个整数,一个字符串列表并返回一个整数列表。

    假设我们的函数接受一个字符串并返回,而 Int 是readread 使用您给它的字符串将其转换为不同的东西。因为我们这里把它放到了一个Int上下文中,它会把字符串转换成int

    map read ["1", "2", "112", 333"]
    

    这将导致

    [1, 2, 112, 333]
    

    因为map 将您的函数read映射(应用)到列表的每个元素。您不必告诉read 这是read "1"read n 之类的参数,因为map 会为您处理这些问题。

    这就是它的全部内容。函数的类型只说明它采用哪些类型以及返回什么类型。当然,也有柯里化,但你稍后会有意或无意地做到这一点!这基本上意味着,一个函数不接受许多个参数,而只有一个。假设你接受函数+。如果您评估1+2,它会返回一个函数,该函数将另一个数字添加到1,并且因为这里还有另一个数字2,它将使用它。您可以将其评估为 (1+) 并传递它,可能稍后再添加数字。当您没有+ 的中缀语法时会更清楚。它本来可以写成(+) 1 2,所以首先这个语句变成(+) 1,它的类型是Int -> Int(简化了!这里就不介绍Num类型类了)。所以(+) 1(或(1+))只是另一个你可以应用值的函数,此时结果将能够计算为3。

    这是它在实践中的样子:(如果您感到困惑,请忽略此处的 (Num a) => 部分,这是您稍后将了解更多信息的概念。如果您愿意,只需将此处的 a 类型替换为 Int,并且完全忽略(Num a) => 部分。)

     (+)  :: (Num a) => a -> a -> a
     (+2) :: (Num a) => a -> a
    (1+)  :: (Num a) => a -> a
    (1+2) :: (Num a) => a
    
    Prelude> (+2) 5
    7
    Prelude> map (+3) [1,2,3]
    [4,5,6]
    

    关于您的第二个问题:您根本没有定义推断类型。它们被称为“推断”,因为编译器/解释器“推断”(读取:计算)类型本身,而无需您明确命名它们。

    关于foldX 的区别:它们都做同样的事情:将列表缩减为单个值。功能之间的区别仅仅是内部定义。 foldl 从左侧折叠列表,foldr 从右侧折叠。

    所以总结一个列表,你可以像这样使用它们......

    foldl1 (+) [1,2,3] == 6
    foldr (+) 0 [1,2,3] == 6
    foldl (+) 0 [1,2,3] == 6
    

    你看,除了 foldl1,你提供了一个折叠函数、一个起始值(累加器)和一个要折叠的列表。 fold1 有它自己的累加器,你不要给它自己的。

    在实践中,您最好使用foldr,因为与fold 不同,它适用于大列表而不会因堆栈溢出而崩溃(tee,hee)。这条规则的例外是foldl'(注意“'”!)在Data.Map - 这是一个严格的折叠,也适用于大列表。

    如果您想自己为函数指定类型(如果您编写了它们),请考虑以下示例:

    double :: Int -> Int
    double n = 2 * n
    

    或者以一种haskellish方式

    double :: Int -> Int
    double = (*2)
    

    这两个示例的第一行 (double :: Int -> Int) 就是您要查找的内容。这就是你可以强制编译器或解释器识别函数的方式——你可以省略,编译器/解释器会发现它们(有时甚至比人们最初想到的更好)

    【讨论】:

      【解决方案3】:

      类型声明中的小写字母是类型变量。这意味着当有a 时,它必须是相同的类型。

      括号(例如(a -> b))子表达式往往意味着该函数接受一个函数作为其参数之一。

      方括号表示某物的列表。

      所以map 接受这些参数:

      • (a -> b) 一个接受一个参数的函数
      • [a] 第一个参数函数可以作用的列表

      并生成[b] 一个列表作为结果。

      我们可以用同样的方式解码(.)

      • (a -> b) 是一个接受单个参数的函数
      • (c -> a) 是一个函数,它产生与第一个函数的参数相同的类型
      • c 与第二个函数使用相同类型的参数

      然而,Haskell 可以做一件巧妙的事情,那就是如果你省略一个参数,它会返回一个具有剩余签名的函数。所以(.)常用来链接函数。

      假设我们有 2 个函数 ord :: Char -> Int 接受一个字符并返回它的整数表示,add32 :: Int -> Int 将 32 添加到它的参数。

      当我们编写 upcaseVal = add32 . ord 时,函数 upcaseVal 现在具有签名 upcaseVal :: Char -> Int,因为 (.) 函数会留下 c -> b,我们在函数定义中替换它们。

      所以foldr map 必须是这样的:

      1. foldr 的第一个参数是一个函数,它接受 map 的两个参数。
      2. 由于函数输出必须与第二个参数 ((a -> b -> b) 的类型相同,我们在 map a = b 中看到

      因此最终的类型将是:

      (b -> b) -> [b] -> [b] -> b -> [b] -> b
      

      但我想这不必打扰你,因为编译器大部分时间都在处理这样的事情。

      【讨论】:

      • 虽然我可能对 foldr map 类型感到困惑 - 请谨慎对待。
      • foldr map的类型是[b] -> [b -> b] -> [b]
      【解决方案4】:

      Haskell 中的函数使用一种称为柯里化的符号。类似的定义

      plus :: Int -> Int -> Int
      

      表示plus 是一个函数,它接受两个Ints 并返回最右边类型的值(又是Int)。更详细地说,柯里化意味着你使用部分应用的函数,它允许你编写类似的代码

      plus1 :: Int -> Int
      plus1 = plus 1
      

      类型签名中的小写标识符表示所谓的类型变量,可以将其视为任何可能类型的占位符。

      定义

      map :: (a -> b) -> [a] -> [b]
      

      因此意味着对于任何类型abmap 采用从ab 的函数和a 的列表,从中生成一个列表b

      举个例子

      map show [1, 2, 3]
      

      我们看到,给定的列表是Int 的列表,所以a = Int。此外,map 应该将show 应用于每个元素,这将返回一个String。由于show 必须符合定义中的a -> b 类型,我们可以推导出b = String

      我们的表达式返回一个[b],所以结果是一个[String]

      为了让这些签名更清晰,我们可以用更详细的语法来写它

      map :: forall a b . (a -> b) -> [a] -> [b]
      

      现在明确指出 map 应该适用于所有类型 ab


      对于折叠,看看他们的definitions。鉴于上述知识,当函数应该做什么很清楚时,类型签名应该是不言自明的。

      示例:factorial n = foldl (*) 1 [1..n]

      foldr map 的签名变为。

      foldr map :: [a] -> [a -> a] -> [a]
      

      您也可以通过在 Haskell 解释器 (GHCi) 中输入 :t foldr map 来获取这些信息。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2019-07-20
        相关资源
        最近更新 更多