阅读类型

在本指南的 核心语言 部分,我们已经通过一堆交互式示例对该语言形成了一定的认识。那么,我们打算再次进行这样的学习,但这次我们有一个新的问题。该值是什么类型

基元和列表

让我们输入一些简单的表达式,看看会发生什么

[ { "input": "\"hello\"", "value": "\u001b[93m\"hello\"\u001b[0m", "type_": "String" }, { "input": "not True", "value": "\u001b[96mFalse\u001b[0m", "type_": "Bool" }, { "input": "round 3.1415", "value": "\u001b[95m3\u001b[0m", "type_": "Int" } ]

单击这个黑框 ⬆️ 光标应该会开始闪烁。输入 3.1415 并按 ENTER 键。它应该会打印出 3.1415,后跟类型 Float

好的,但这里到底发生了什么?每条记录都显示了一个值,以及该值碰巧是什么类型。你可以这样大声朗读这些示例

  • "hello" 是一个 String
  • False 是一个 Bool
  • 3 是一个 Int
  • 3.1415 是一个 Float

Elm 能够弄清楚你输入的任何值的类型!让我们看看列表会发生什么

[ { "input": "[ \"Alice\", \"Bob\" ]", "value": "[\u001b[93m\"Alice\"\u001b[0m,\u001b[93m\"Bob\"\u001b[0m]", "type_": "List String" }, { "input": "[ 1.0, 8.6, 42.1 ]", "value": "[\u001b[95m1.0\u001b[0m,\u001b[95m8.6\u001b[0m,\u001b[95m42.1\u001b[0m]", "type_": "List Float" } ]

你可以将这些类型读作

  1. 我们有一个用 String 值填充的 List
  2. 我们有一个用 Float 值填充的 List

类型是对我们正在查看的特定值的大致描述。

函数

我们看看一些函数的类型

[ { "input": "String.length", "value": "\u001b[36m<function>\u001b[0m", "type_": "String -> Int" } ]

尝试输入 roundsqrt 来查看其他一些函数类型 ⬆️

String.length 函数的类型为 String -> Int。这意味着它必须采用 String 参数,并且肯定会返回一个 Int 值。因此,让我们尝试为它提供一个参数

[ { "input": "String.length \"Supercalifragilisticexpialidocious\"", "value": "\u001b[95m34\u001b[0m", "type_": "Int" } ]

因此,我们从一个 String -> Int 函数开始,并为它提供一个 String 参数。这会产生 Int

但是,当您不提供 String 时会发生什么?尝试输入 String.length [1,2,3]String.length True 以查看会发生什么 ⬆️

您将发现 String -> Int 函数必须获取 String 参数!

注意:采用多个参数的函数最终会有越来越多的箭头。例如,这是一个采用两个参数的函数

[ { "input": "String.repeat", "value": "\u001b[36m<function>\u001b[0m", "type_": "Int -> String -> String" } ]

提供两个参数,如 String.repeat 3 "ha",将会产生 "hahaha"。将 -> 视为一种分离参数的奇怪方式很有用,但我解释了 此处 的真正原因。这相当简洁!

类型注释

到目前为止,我们只是让 Elm 找出类型,但它还允许您在定义上方的行中编写一个类型注释。因此,在编写代码时,您可以说这样的话

half : Float -> Float
half n =
  n / 2

-- half 256 == 128
-- half "3" -- error!

hypotenuse : Float -> Float -> Float
hypotenuse a b =
  sqrt (a^2 + b^2)

-- hypotenuse 3 4  == 5
-- hypotenuse 5 12 == 13

checkPower : Int -> String
checkPower powerLevel =
  if powerLevel > 9000 then "It's over 9000!!!" else "Meh"

-- checkPower 9001 == "It's over 9000!!!"
-- checkPower True -- error!

添加类型注释并不是必需的,但绝对推荐!优点包括

  1. 错误信息质量 — 当您添加类型注释时,它告诉编译器您尝试做什么。您的实现可能存在错误,现在编译器可以根据您声明的意图进行比较。“您说参数 powerLevel 是一个 Int,但它被用作一个 String!”
  2. 文档 — 当您稍后重新审阅代码(或当同事第一次审阅它时),确切地知道函数的输入和输出将非常有帮助,而不必非常仔细地阅读该实现。

不过,人们在类型注释中可能会犯错误,所以如果注释与实现不匹配,会发生什么情况?编译器会自行找出所有类型,并检查您的注释是否与实际答案匹配。换句话说,编译器始终会验证您添加的所有注释是否正确。因此,您会获得更好的错误消息,而且文档始终保持最新!

类型变量

在浏览更多 Elm 代码时,您将开始看到类型注释中包含小写字母。一个常见的示例是 List.length 函数

[ { "input": "List.length", "value": "\u001b[36m<function>\u001b[0m", "type_": "List a -> Int" } ]

注意到类型中的小写a吗?它称为类型变量。它可根据List.length的使用方式而变化

[ { "input": "List.length [1,1,2,3,5,8]", "value": "\u001b[95m6\u001b[0m", "type_": "Int" }, { "input": "List.length [ \"a\", \"b\", \"c\" ]", "value": "\u001b[95m3\u001b[0m", "type_": "Int" }, { "input": "List.length [ True, False ]", "value": "\u001b[95m2\u001b[0m", "type_": "Int" } ]

我们只需要长度即可,因此列表中存放什么并不重要。因此,类型变量a表示我们可以匹配任何类型。我们来看看另一个常见的示例

[ { "input": "List.reverse", "value": "\u001b[36m<function>\u001b[0m", "type_": "List a -> List a" }, { "input": "List.reverse [ \"a\", \"b\", \"c\" ]", "value": "[\u001b[93m\"c\"\u001b[0m,\u001b[93m\"b\"\u001b[0m,\u001b[93m\"a\"\u001b[0m]", "type_": "List String" }, { "input": "List.reverse [ True, False ]", "value": "[\u001b[96mFalse\u001b[0m,\u001b[96mTrue\u001b[0m]", "type_": "List Bool" } ]

同样,类型变量a可根据List.reverse的使用方式而变化。但在本例中,我们有一个a在参数和结果中。这意味着如果你给出一个List Int,你也必须得到一个List Int。一旦我们决定了a是什么,那么它在任何地方都是这个。

注意:类型变量必须以小写字母开头,但它们可以是完整的单词。我们可以将List.length的类型写为List value -> Int,并且我们可以将List.reverse的类型写为List element -> List element。只要它们以小写字母开头即可。根据惯例,很多地方都使用了类型变量ab,但是一些类型注释受益于更具体的名称。

受限类型变量

Elm 中有一种称为受限类型变量的特殊类型的类型变量。最常见的示例是number类型。negate函数使用它

[ { "input": "negate", "value": "\u001b[36m<function>\u001b[0m", "type_": "number -> number" } ]

尝试高于的表达,例如 negate 3.1415negate (round 3.1415)negate "hi" ⬆️

通常,类型变量可以用任何东西填充,但number只能由IntFloat值填充。它限制了可能性。

受限类型变量的完整列表是

  • number允许IntFloat
  • appendable允许StringList a
  • comparable允许IntFloatCharStringcomparable值列表/元组
  • compappend允许StringList comparable

这些受约束的类型变量的存在是为了使得像 (+)(<) 的操作符更加灵活。

现在我们已经相当透彻地介绍了值和函数的类型,但是当我们开始想要更复杂的数据结构的时候,这些内容看起来会是什么样子呢?

符合 "" 的结果

    没有符合 "" 的结果