类型和类型别名之间的 Elm 差异?
Difference in Elm between type and type alias?
在 Elm 中,我不知道什么时候 type
和 type alias
是合适的关键字。文档似乎没有对此的解释,我也无法在发行说明中找到。这在某处记录了吗?
我的看法:
type
用于定义新的联合类型:
type Thing = Something | SomethingElse
在此定义之前 Something
和 SomethingElse
没有任何意义。现在它们都是我们刚刚定义的 Thing
类型。
type alias
用于给其他一些已经存在的类型命名:
type alias Location = { lat:Int, long:Int }
{ lat = 5, long = 10 }
具有类型 { lat:Int, long:Int }
,这已经是一个有效类型。但是现在我们也可以说它有类型 Location
因为那是同一类型的别名。
值得注意的是,下面会编译正常并显示"thing"
。即使我们指定 thing
是 String
并且 aliasedStringIdentity
接受 AliasedString
,我们也不会收到 String
/ 之间存在类型不匹配的错误AliasedString
:
import Graphics.Element exposing (show)
type alias AliasedString = String
aliasedStringIdentity: AliasedString -> AliasedString
aliasedStringIdentity s = s
thing : String
thing = "thing"
main =
show <| aliasedStringIdentity thing
关键是alias
这个词。在编程过程中,当你想把属于一起的东西分组时,你把它放在一个记录中,比如一个点
{ x = 5, y = 4 }
或学生记录。
{ name = "Billy Bob", grade = 10, classof = 1998 }
现在,如果您需要传递这些记录,则必须拼出整个类型,例如:
add : { x:Int, y:Int } -> { x:Int, y:Int } -> { x:Int, y:Int }
add a b =
{ a.x + b.x, a.y + b.y }
如果你能给一个点起个别名,签名就容易写多了!
type alias Point = { x:Int, y:Int }
add : Point -> Point -> Point
add a b =
{ a.x + b.x, a.y + b.y }
所以别名是 shorthand 其他东西的别名。在这里,它是记录类型的 shorthand。您可以将其视为为经常使用的记录类型命名。这就是它被称为别名的原因——它是由 { x:Int, y:Int }
表示的裸记录类型的另一个名称
而 type
解决了不同的问题。如果您来自 OOP,这是您通过继承、运算符重载等方式解决的问题——有时,您希望将数据视为通用事物,有时您希望将其视为特定事物。
发生这种情况的常见情况是在传递消息时——比如邮政系统。当你寄出一封信时,你希望邮政系统把所有的信息都当作一回事,所以你只需要设计一次邮政系统。此外,路由消息的工作应该独立于其中包含的消息。只有当信到达目的地时,你才会在乎它是什么信息。
以同样的方式,我们可以将 type
定义为可能发生的所有不同类型消息的联合。假设我们正在实施一个大学生与 parent 之间的消息传递系统。所以大学生只能发送两条消息:'I need beer money' 和 'I need underpants'.
type MessageHome = NeedBeerMoney | NeedUnderpants
所以现在,当我们设计路由系统时,我们函数的类型可以直接传递 MessageHome
,而不用担心它可能是所有不同类型的消息。路由系统不关心。它只需要知道它是 MessageHome
。只有当消息到达目的地,即 parent 的家时,您才需要弄清楚它是什么。
case message of
NeedBeerMoney ->
sayNo()
NeedUnderpants ->
sendUnderpants(3)
如果您了解 Elm 架构,更新函数是一个巨大的 case 语句,因为这是消息被路由并因此被处理的目的地。我们使用联合类型在传递消息时使用单一类型来处理,但随后可以使用 case 语句来梳理出它到底是什么消息,因此我们可以处理它。
在我看来,主要区别在于如果您使用 "synomical" 类型,类型检查器是否会对您大喊大叫。
创建下面的文件,把它放在运行elm-reactor
的某个地方,然后去http://localhost:8000
看看区别:
-- Boilerplate code
module Main exposing (main)
import Html exposing (..)
main =
Html.beginnerProgram
{
model = identity,
view = view,
update = identity
}
-- Our type system
type alias IntRecordAlias = {x : Int}
type IntRecordType =
IntRecordType {x : Int}
inc : {x : Int} -> {x : Int}
inc r = {r | x = .x r + 1}
view model =
let
-- 1. This will work
r : IntRecordAlias
r = {x = 1}
-- 2. However, this won't work
-- r : IntRecordType
-- r = IntRecordType {x = 1}
in
Html.text <| toString <| inc r
如果您取消注释 2.
并注释 1.
,您将看到:
The argument to function `inc` is causing a mismatch.
34| inc r
^
Function `inc` is expecting the argument to be:
{ x : Int }
But it is:
IntRecordType
让我通过关注用例并提供有关构造函数和模块的一些上下文来补充之前的答案。
type alias
的用法
为记录创建别名和构造函数
这是最常见的用例:您可以为特定类型的记录格式定义备用名称和构造函数。
type alias Person =
{ name : String
, age : Int
}
自动定义类型别名意味着以下构造函数(伪代码):
Person : String -> Int -> { name : String, age : Int }
这可以派上用场,例如当你想编写一个 Json 解码器时。
personDecoder : Json.Decode.Decoder Person
personDecoder =
Json.Decode.map2 Person
(Json.Decode.field "name" Json.Decode.String)
(Json.Decode.field "age" Int)
指定必填字段
他们有时称它为 "extensible records",这可能会产生误导。
此语法可用于指定您期望一些具有特定字段的记录。如:
type alias NamedThing x =
{ x
| name : String
}
showName : NamedThing x -> Html msg
showName thing =
Html.text thing.name
然后你可以像这样使用上面的函数(比如在你看来):
let
joe = { name = "Joe", age = 34 }
in
showName joe
Richard Feldman's talk on ElmEurope 2017 可以进一步了解何时值得使用此样式。
重命名内容
您可以这样做,因为新名称可以提供额外的含义
稍后在你的代码中,就像这个例子
type alias Id = String
type alias ElapsedTime = Time
type SessionStatus
= NotStarted
| Active Id ElapsedTime
| Finished Id
也许是 this kind of usage in core is Time
的更好例子。
重新公开来自不同模块的类型
如果您正在编写一个包(而不是应用程序),您可能需要在一个模块中实现一种类型,也许是在一个内部(未公开)模块中,但您希望从不同的 (public) 中公开该类型模块。或者,您想要从多个模块公开您的类型。
Task
in core and Http.Request in Http are examples for the first, while the Json.Encode.Value and Json.Decode.Value pair就是后者的例子。
只有在您想要保持类型不透明时才能这样做:您不公开构造函数。有关详细信息,请参阅下面 type
的用法。
值得注意的是,在上面的例子中,只有#1提供了构造函数。如果您在 #1 中公开您的类型别名,例如 module Data exposing (Person)
,这将公开类型名称和构造函数。
type
的用法
定义标记联合类型
这是最常见的用例,一个很好的例子是 Maybe
type in core:
type Maybe a
= Just a
| Nothing
当你定义一个类型时,你也定义了它的构造函数。如果可能这些是(伪代码):
Just : a -> Maybe a
Nothing : Maybe a
这意味着如果你声明这个值:
mayHaveANumber : Maybe Int
您可以通过
mayHaveANumber = Nothing
或
mayHaveANumber = Just 5
Just
和 Nothing
标签不仅用作构造函数,它们还用作 case
表达式中的析构函数或模式。这意味着使用这些模式,您可以在 Maybe
:
中看到
showValue : Maybe Int -> Html msg
showValue mayHaveANumber =
case mayHaveANumber of
Nothing ->
Html.text "N/A"
Just number ->
Html.text (toString number)
你可以这样做,因为 Maybe 模块定义如下
module Maybe exposing
( Maybe(Just,Nothing)
也可以说
module Maybe exposing
( Maybe(..)
在这种情况下两者是等价的,但是在 Elm 中明确被认为是一种美德,尤其是在编写包时。
隐藏实现细节
正如上面所指出的,Maybe
的构造函数对其他模块可见是一种深思熟虑的选择。
然而,当作者决定隐藏它们时,还有其他情况。 One example of this in core is Dict
。作为包的消费者,不应该看到Dict
背后的Red/Black树算法的实现细节,直接乱搞节点。隐藏构造函数会强制 module/package 的使用者仅通过您公开的函数创建您的类型的值(然后转换这些值)。
这就是代码中有时会出现这样的东西的原因
type Person =
Person { name : String, age : Int }
与此 post 顶部的 type alias
定义不同,此语法创建一个新的 "union" 类型,只有一个构造函数,但该构造函数可以对其他函数隐藏modules/packages.
如果类型是这样暴露的:
module Data exposing (Person)
只有 Data
模块中的代码可以创建一个 Person 值,并且只有该代码可以对其进行模式匹配。
alias
只是一些其他类型的简称,类似于 OOP 中的 class
。 Exp:
type alias Point =
{ x : Int
, y : Int
}
type
(没有别名)可以让您定义自己的类型,因此您可以为您的应用程序定义 Int
、String
、...等类型。例如,通常情况下,它可以用于描述应用程序的状态:
type AppState =
Loading --loading state
|Loaded --load successful
|Error String --Loading error
所以你可以在 view
elm 中轻松处理它:
-- VIEW
...
case appState of
Loading -> showSpinner
Loaded -> showSuccessData
Error error -> showError
...
我想你知道 type
和 type alias
之间的区别。
但是为什么以及如何使用type
和type alias
对于elm
应用程序很重要,你们可以参考article from Josh Clayton
在 Elm 中,我不知道什么时候 type
和 type alias
是合适的关键字。文档似乎没有对此的解释,我也无法在发行说明中找到。这在某处记录了吗?
我的看法:
type
用于定义新的联合类型:
type Thing = Something | SomethingElse
在此定义之前 Something
和 SomethingElse
没有任何意义。现在它们都是我们刚刚定义的 Thing
类型。
type alias
用于给其他一些已经存在的类型命名:
type alias Location = { lat:Int, long:Int }
{ lat = 5, long = 10 }
具有类型 { lat:Int, long:Int }
,这已经是一个有效类型。但是现在我们也可以说它有类型 Location
因为那是同一类型的别名。
值得注意的是,下面会编译正常并显示"thing"
。即使我们指定 thing
是 String
并且 aliasedStringIdentity
接受 AliasedString
,我们也不会收到 String
/ 之间存在类型不匹配的错误AliasedString
:
import Graphics.Element exposing (show)
type alias AliasedString = String
aliasedStringIdentity: AliasedString -> AliasedString
aliasedStringIdentity s = s
thing : String
thing = "thing"
main =
show <| aliasedStringIdentity thing
关键是alias
这个词。在编程过程中,当你想把属于一起的东西分组时,你把它放在一个记录中,比如一个点
{ x = 5, y = 4 }
或学生记录。
{ name = "Billy Bob", grade = 10, classof = 1998 }
现在,如果您需要传递这些记录,则必须拼出整个类型,例如:
add : { x:Int, y:Int } -> { x:Int, y:Int } -> { x:Int, y:Int }
add a b =
{ a.x + b.x, a.y + b.y }
如果你能给一个点起个别名,签名就容易写多了!
type alias Point = { x:Int, y:Int }
add : Point -> Point -> Point
add a b =
{ a.x + b.x, a.y + b.y }
所以别名是 shorthand 其他东西的别名。在这里,它是记录类型的 shorthand。您可以将其视为为经常使用的记录类型命名。这就是它被称为别名的原因——它是由 { x:Int, y:Int }
而 type
解决了不同的问题。如果您来自 OOP,这是您通过继承、运算符重载等方式解决的问题——有时,您希望将数据视为通用事物,有时您希望将其视为特定事物。
发生这种情况的常见情况是在传递消息时——比如邮政系统。当你寄出一封信时,你希望邮政系统把所有的信息都当作一回事,所以你只需要设计一次邮政系统。此外,路由消息的工作应该独立于其中包含的消息。只有当信到达目的地时,你才会在乎它是什么信息。
以同样的方式,我们可以将 type
定义为可能发生的所有不同类型消息的联合。假设我们正在实施一个大学生与 parent 之间的消息传递系统。所以大学生只能发送两条消息:'I need beer money' 和 'I need underpants'.
type MessageHome = NeedBeerMoney | NeedUnderpants
所以现在,当我们设计路由系统时,我们函数的类型可以直接传递 MessageHome
,而不用担心它可能是所有不同类型的消息。路由系统不关心。它只需要知道它是 MessageHome
。只有当消息到达目的地,即 parent 的家时,您才需要弄清楚它是什么。
case message of
NeedBeerMoney ->
sayNo()
NeedUnderpants ->
sendUnderpants(3)
如果您了解 Elm 架构,更新函数是一个巨大的 case 语句,因为这是消息被路由并因此被处理的目的地。我们使用联合类型在传递消息时使用单一类型来处理,但随后可以使用 case 语句来梳理出它到底是什么消息,因此我们可以处理它。
在我看来,主要区别在于如果您使用 "synomical" 类型,类型检查器是否会对您大喊大叫。
创建下面的文件,把它放在运行elm-reactor
的某个地方,然后去http://localhost:8000
看看区别:
-- Boilerplate code
module Main exposing (main)
import Html exposing (..)
main =
Html.beginnerProgram
{
model = identity,
view = view,
update = identity
}
-- Our type system
type alias IntRecordAlias = {x : Int}
type IntRecordType =
IntRecordType {x : Int}
inc : {x : Int} -> {x : Int}
inc r = {r | x = .x r + 1}
view model =
let
-- 1. This will work
r : IntRecordAlias
r = {x = 1}
-- 2. However, this won't work
-- r : IntRecordType
-- r = IntRecordType {x = 1}
in
Html.text <| toString <| inc r
如果您取消注释 2.
并注释 1.
,您将看到:
The argument to function `inc` is causing a mismatch.
34| inc r
^
Function `inc` is expecting the argument to be:
{ x : Int }
But it is:
IntRecordType
让我通过关注用例并提供有关构造函数和模块的一些上下文来补充之前的答案。
type alias
的用法
为记录创建别名和构造函数
这是最常见的用例:您可以为特定类型的记录格式定义备用名称和构造函数。type alias Person = { name : String , age : Int }
自动定义类型别名意味着以下构造函数(伪代码):
Person : String -> Int -> { name : String, age : Int }
这可以派上用场,例如当你想编写一个 Json 解码器时。personDecoder : Json.Decode.Decoder Person personDecoder = Json.Decode.map2 Person (Json.Decode.field "name" Json.Decode.String) (Json.Decode.field "age" Int)
指定必填字段
他们有时称它为 "extensible records",这可能会产生误导。 此语法可用于指定您期望一些具有特定字段的记录。如:type alias NamedThing x = { x | name : String } showName : NamedThing x -> Html msg showName thing = Html.text thing.name
然后你可以像这样使用上面的函数(比如在你看来):
let joe = { name = "Joe", age = 34 } in showName joe
Richard Feldman's talk on ElmEurope 2017 可以进一步了解何时值得使用此样式。
重命名内容
您可以这样做,因为新名称可以提供额外的含义 稍后在你的代码中,就像这个例子type alias Id = String type alias ElapsedTime = Time type SessionStatus = NotStarted | Active Id ElapsedTime | Finished Id
也许是 this kind of usage in core is
Time
的更好例子。
重新公开来自不同模块的类型
如果您正在编写一个包(而不是应用程序),您可能需要在一个模块中实现一种类型,也许是在一个内部(未公开)模块中,但您希望从不同的 (public) 中公开该类型模块。或者,您想要从多个模块公开您的类型。
Task
in core and Http.Request in Http are examples for the first, while the Json.Encode.Value and Json.Decode.Value pair就是后者的例子。只有在您想要保持类型不透明时才能这样做:您不公开构造函数。有关详细信息,请参阅下面
type
的用法。
值得注意的是,在上面的例子中,只有#1提供了构造函数。如果您在 #1 中公开您的类型别名,例如 module Data exposing (Person)
,这将公开类型名称和构造函数。
type
的用法
定义标记联合类型
这是最常见的用例,一个很好的例子是Maybe
type in core:type Maybe a = Just a | Nothing
当你定义一个类型时,你也定义了它的构造函数。如果可能这些是(伪代码):
Just : a -> Maybe a Nothing : Maybe a
这意味着如果你声明这个值:
mayHaveANumber : Maybe Int
您可以通过
mayHaveANumber = Nothing
或
mayHaveANumber = Just 5
Just
和Nothing
标签不仅用作构造函数,它们还用作case
表达式中的析构函数或模式。这意味着使用这些模式,您可以在Maybe
:showValue : Maybe Int -> Html msg showValue mayHaveANumber = case mayHaveANumber of Nothing -> Html.text "N/A" Just number -> Html.text (toString number)
你可以这样做,因为 Maybe 模块定义如下
module Maybe exposing ( Maybe(Just,Nothing)
也可以说
module Maybe exposing ( Maybe(..)
在这种情况下两者是等价的,但是在 Elm 中明确被认为是一种美德,尤其是在编写包时。
隐藏实现细节
正如上面所指出的,Maybe
的构造函数对其他模块可见是一种深思熟虑的选择。然而,当作者决定隐藏它们时,还有其他情况。 One example of this in core is
Dict
。作为包的消费者,不应该看到Dict
背后的Red/Black树算法的实现细节,直接乱搞节点。隐藏构造函数会强制 module/package 的使用者仅通过您公开的函数创建您的类型的值(然后转换这些值)。这就是代码中有时会出现这样的东西的原因
type Person = Person { name : String, age : Int }
与此 post 顶部的
type alias
定义不同,此语法创建一个新的 "union" 类型,只有一个构造函数,但该构造函数可以对其他函数隐藏modules/packages.如果类型是这样暴露的:
module Data exposing (Person)
只有
Data
模块中的代码可以创建一个 Person 值,并且只有该代码可以对其进行模式匹配。
alias
只是一些其他类型的简称,类似于 OOP 中的 class
。 Exp:
type alias Point =
{ x : Int
, y : Int
}
type
(没有别名)可以让您定义自己的类型,因此您可以为您的应用程序定义 Int
、String
、...等类型。例如,通常情况下,它可以用于描述应用程序的状态:
type AppState =
Loading --loading state
|Loaded --load successful
|Error String --Loading error
所以你可以在 view
elm 中轻松处理它:
-- VIEW
...
case appState of
Loading -> showSpinner
Loaded -> showSuccessData
Error error -> showError
...
我想你知道 type
和 type alias
之间的区别。
但是为什么以及如何使用type
和type alias
对于elm
应用程序很重要,你们可以参考article from Josh Clayton