类型和类型别名之间的 Elm 差异?

Difference in Elm between type and type alias?

在 Elm 中,我不知道什么时候 typetype alias 是合适的关键字。文档似乎没有对此的解释,我也无法在发行说明中找到。这在某处记录了吗?

我的看法:

type用于定义新的联合类型:

type Thing = Something | SomethingElse

在此定义之前 SomethingSomethingElse 没有任何意义。现在它们都是我们刚刚定义的 Thing 类型。

type alias 用于给其他一些已经存在的类型命名:

type alias Location = { lat:Int, long:Int }

{ lat = 5, long = 10 } 具有类型 { lat:Int, long:Int },这已经是一个有效类型。但是现在我们也可以说它有类型 Location 因为那是同一类型的别名。

值得注意的是,下面会编译正常并显示"thing"。即使我们指定 thingString 并且 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

的用法
  1. 为记录创建别名和构造函数
    这是最常见的用例:您可以为特定类型的记录格式定义备用名称和构造函数。

    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)
    


  2. 指定必填字段
    他们有时称它为 "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 可以进一步了解何时值得使用此样式。

  3. 重命名内容
    您可以这样做,因为新名称可以提供额外的含义 稍后在你的代码中,就像这个例子

    type alias Id = String
    
    type alias ElapsedTime = Time
    
    type SessionStatus
        = NotStarted
        | Active Id ElapsedTime
        | Finished Id
    

    也许是 this kind of usage in core is Time 的更好例子。

  4. 重新公开来自不同模块的类型
    如果您正在编写一个包(而不是应用程序),您可能需要在一个模块中实现一种类型,也许是在一个内部(未公开)模块中,但您希望从不同的 (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

的用法
  1. 定义标记联合类型
    这是最常见的用例,一个很好的例子是 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
    

    JustNothing 标签不仅用作构造函数,它们还用作 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 中明确被认为是一种美德,尤其是在编写包时。


  1. 隐藏实现细节
    正如上面所指出的,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(没有别名)可以让您定义自己的类型,因此您可以为您的应用程序定义 IntString、...等类型。例如,通常情况下,它可以用于描述应用程序的状态:

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

...

我想你知道 typetype alias 之间的区别。

但是为什么以及如何使用typetype alias对于elm应用程序很重要,你们可以参考article from Josh Clayton