记录是一种数据结构,把相关的信息用标签组织在一起。它们类似于 JavaScript 或 Java 中的对象,也类似于 C 或 Rust 中的 struct,但有一些关键区别。
记录用花括号创建,其中的元素用逗号分隔。
firefly =
{ name = "Firefly"
, creator = "Joss Whedon"
, episodes = 14
}
记录的类型也用类似的语法定义,只不过等号=换成了冒号:。
firefly : { name : String, creator : String, episodes : Int }
firefly =
{ name = "Firefly"
, creator = "Joss Whedon"
, episodes = 14
}
重复写这些类型定义很麻烦,所以用类型别名来共享定义是一种常见的做法。
type alias TvSeries =
{ name : String
, creator : String
, episodes : Int
}
firefly : TvSeries
firefly =
{ name = "Firefly"
, creator = "Joss Whedon"
, episodes = 14
}
类型别名还会自动为记录提供一个构造函数。这一点有时会让人意外,因为 Elm 中的函数必须以小写字符开头,但类型构造器是一个例外。
firefly : TvSeries
firefly = TvSeries "Firefly" "Joss Whedon" 14
访问记录实例中某个字段的值的主要方式是使用点号表示法,比如firefly.creator。Elm 编译器还支持以点.开头的特殊访问器函数,比如.creator,它可以作用于任何含有同名字段的记录。这是“函数必须以小写字符开头”这条规则的第二个例外。事实上,用户自定义的函数必须以小写字符开头,而我们刚刚发现的这两个例子是由编译器生成的,不是程序员写的。
firefly : TvSeries
firefly =
{ name = "Firefly"
, creator = "Joss Whedon"
, episodes = 14
}
firefly.name
--> "Firefly"
.creator firefly
--> "Joss Whedon"
记录还可以在绑定中通过记录模式匹配进行解构。解构适用于任何含有相应名称字段的记录。
episodesCount : TvSeries -> Int
episodesCount { episodes } =
episodes
complicatedCopy : TvSeries -> TvSeries
complicatedCopy show =
let
{ name, creator } = show
in
{ name = name
, creator = creator
, episodes = episodesCount show
}
和 Elm 中的一切一样,记录是不可变的,所以一旦定义了记录,它的字段值就永远不会改变。有一种特殊语法可以创建现有记录的副本,同时修改一个或多个字段。这种语法用管道符号|来区分原始记录和将要修改的字段,比如{ oldRecord | field1 = newValue }。
firefly : TvSeries
firefly =
{ name = "Firefly"
, creator = "Joss Whedon"
, episodes = 14
}
updatedFirefly : TvSeries
updatedFirefly =
{ firefly | creator = "J Whedon", episodes = 15 }
Elm 使用[结构相等][equality],也就是说,同一个记录的两个实例只要值完全相同,就相等。
firefly1 = TvSeries "Firefly" "Joss Whedon" 14
firefly2 = TvSeries "Firefly" "Joss Whedon" 14
firefly1 == firefly2
--> True
Elm 还支持结构类型,也就是说,如果一个函数需要一个带有 x 和 y 字段的记录,那么任何含有这些字段的记录都可以用,比如二维点、三维点、宇宙飞船等等。这类记录类型的定义中带有一个管道|,比如{ a | x : Float, y : Float },在 Elm 术语中叫做“可扩展记录”。要小心区分类型定义中的管道|(那是可扩展记录的定义,比如{ a | x : Int })和实际记录实例中的管道|(那表示我们正在修改该记录的一些字段)。
point2d = { x = 1, y = 2 }
point3d = { x = 3, y = 4, z = 7 }
.x point2d --> 1
.x point3d --> 3
length : { a | x : Float, y : Float } -> Float
length vector =
sqrt (vector.x * vector.x + vector.y * vector.y)
length point2d --> 2.236068
length point3d --> 5
translateX : { a | x : Float } -> { a | x : Float }
translateX vector =
{ vector | x = vector.x + 1 }
translateX point2d --> { x = 2, y = 2 }
translateX point3d --> { x = 4, y = 4, z = 7 }
在这个练习中,你是一位狂热的体育迷,刚刚发现自己迷上了 NBA 篮球。 作为 NBA 篮球的新手,你正在深入研究 NBA 的历史,追踪各支球队、教练以及他们的胜负数据,并把它们相互比较。
由于你还没有最喜欢的球队,你还要开发一个算法,来判断某支球队是否值得支持。 你有 7 个任务来帮你开发这套专属的_支持某支球队_算法。
定义一个Coach类型别名,表示一个包含以下两个字段的记录,字段顺序如下:
name:教练的名字,类型为String。formerPlayer:表示该教练是否曾是球员,类型为Bool。定义一个Stats类型别名,表示一个包含以下两个字段的记录,字段顺序如下:
wins:胜场数,类型为Int。losses:负场数,类型为Int。定义一个Team类型别名,表示一个包含以下三个字段的记录,字段顺序如下:
name:球队的名字,类型为String。coach:球队的教练,类型为Coach。stats:球队的数据,类型为Stats。你想与一些第三方代码对接,它们有球队的概念,但调用你的代码时实参的顺序不一样。添加一个createTeam函数,签名为String -> Stats -> Coach -> Team。
NBA 的老板们没什么耐心,你发现球队战绩不好,往往会导致教练被换掉。
实现replaceCoach函数,它接收新教练和球队作为形参,返回带上了新教练的那支球队:
let
coach = { name = "Larry Bird", formerPlayer = True }
stats = { wins = 58, losses = 24 }
team = { name = "Indiana Pacers", coach = coach, stats = stats }
newCoach = { name = "Isiah Thomas", formerPlayer = True }
in
replaceCoach newCoach team
--> { name = "Indiana Pacers"
-- , coach = { name = "Isiah Thomas", formerPlayer = True }
-- , stats = { wins = 58, losses = 24 } }
将来你打算设计一套复杂的算法,来判断一支球队是否值得支持。但第一次尝试时,你决定只要一支球队的胜场数多于负场数,就支持它。
你希望这个函数能在其他场景中复用,所以在类型签名里使用可扩展记录,来接受任何包含相关字段的记录。
为了让函数尽可能简洁,你想用模式匹配从函数形参中取出stats。
实现rootForTeam函数,它接收一支球队;如果你应该支持这支球队,就返回True,否则返回False。
let
spursCoach = { name = "Gregg Popovich", formerPlayer = False }
spursStats = { wins = 56, losses = 26 }
spursTeam = { name = "San Antonio Spurs", coach = spursCoach, stats = spursStats }
in
rootForTeam spursTeam
--> True