Elm 中 type 和 type alias 有什么区别?

103
在 Elm 中,我无法确定何时适用于 type 关键字,以及何时适用于 type alias。文档似乎没有解释这一点,版本发布说明中也找不到相关信息。这个问题有没有被记录在某处?
5个回答

142

我的想法:

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"。尽管我们指定了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

1
不确定你最后一段的意思。你是想说无论如何别名,它们仍然是相同类型的吗? - ZHANG Cheng
10
是的,只是指出编译器认为别名类型与原始类型相同。 - robertjlooby
1
当您使用{}记录语法时,您正在定义一个新类型? - user9903
4
{ lat:Int, long:Int }并不定义一个新的类型,而是一个已经存在的有效类型。type alias Location = { lat:Int, long:Int } 也不会定义新的类型,只是为现有类型提供另一个(或许更加具有描述性的)名称。type Location = Geo { lat:Int, long:Int } 则会定义一个新类型 (Location)。 - robertjlooby
2
何时应该使用类型(type)而不是类型别名(type alias)?总是使用类型(type)的缺点在哪里? - Richard Haven
显示剩余2条评论

11
关键词是单词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 }

因此,别名是某个东西的简称。在这里,它是记录类型的简称。您可以将其视为给经常使用的记录类型命名。这就是为什么它被称为别名——它是表示由{ x:Int, y:Int }代表的裸记录类型的另一个名称。
type解决了不同的问题。如果您来自OOP,那么您要解决的问题是继承、运算符重载等。有时,您希望将数据视为通用的东西,有时则希望将其视为特定的东西。
这种情况常见于传递消息时,就像邮政系统一样。当您发送信件时,您希望邮政系统将所有消息视为相同的东西,因此您只需要设计一次邮政系统。此外,路由消息的工作应该独立于其中包含的消息。只有当信件到达目的地时,您才关心消息是什么。
以同样的方式,我们可能定义一个type作为可能发生的所有不同类型的消息的联合。假设我们正在实现一个大学生向父母发送消息的消息系统。因此,只有两种消息可以发送给大学生:'我需要啤酒钱'和'我需要内裤'。
type MessageHome = NeedBeerMoney | NeedUnderpants

现在,当我们设计路由系统时,函数的类型只需要传递MessageHome即可,而不必担心它可能是各种不同类型的消息。路由系统并不关心具体是什么类型,它只需要知道这是一个MessageHome。只有当消息到达其目的地——父级的主页时,你才需要弄清楚它的具体内容。

case message of
  NeedBeerMoney ->
    sayNo()
  NeedUnderpants ->
    sendUnderpants(3)

如果您熟悉Elm架构,update函数就是一个巨大的case语句,因为这是消息路由和处理的目的地。我们使用联合类型来处理传递的消息时只有一个单一类型,然后可以使用case语句来确定确切的消息内容,以便我们可以处理它。

8
让我通过聚焦使用案例和提供关于构造函数和模块的一些背景来补充之前的回答。


type alias 的用法

  1. Create an alias and a constructor function for a record
    This the most common use-case: you can define an alternate name and constructor function for a particular kind of record format.

    type alias Person =
        { name : String
        , age : Int
        }
    

    Defining the type alias automatically implies the following constructor function (pseudo code):
    Person : String -> Int -> { name : String, age : Int }
    This can come handy, for instance when you want to write a Json decoder.

    personDecoder : Json.Decode.Decoder Person
    personDecoder =
        Json.Decode.map2 Person
            (Json.Decode.field "name" Json.Decode.String)
            (Json.Decode.field "age" Int)
    


  2. Specify required fields
    They sometimes call it "extensible records", which can be misleading. This syntax can be used to specify that you are expecting some record with particular fields present. Such as:

    type alias NamedThing x =
        { x
            | name : String
        }
    
    showName : NamedThing x -> Html msg
    showName thing =
        Html.text thing.name
    

    Then you can use the above function like this (for example in your view):

    let
        joe = { name = "Joe", age = 34 }
    in
        showName joe
    

    Richard Feldman's talk on ElmEurope 2017 may provide some further insight into when this style is worth using.

  3. Renaming stuff
    You might do this, because the new names could provide extra meaning later on in your code, like in this example

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

    Perhaps a better example of this kind of usage in core is Time.

  4. Re-exposing a type from a different module
    If you are writing a package (not an application), you may need to implement a type in one module, perhaps in an internal (not exposed) module, but you want to expose the type from a different (public) module. Or, alternatively, you want to expose your type from multiple modules.
    Task in core and Http.Request in Http are examples for the first, while the Json.Encode.Value and Json.Decode.Value pair is an example of the later.

    You can only do this when you otherwise want to keep the type opaque: you don't expose the constructor functions. For details see usages of type below.

值得注意的是在上面的例子中,只有#1提供了构造函数。如果你像这样在#1中暴露你的类型别名module Data exposing (Person),那么将会同时暴露类型名称和构造函数。

type的用法

  1. Define a tagged union type
    This is the most common use-case, a good example of it is the Maybe type in core:

    type Maybe a
        = Just a
        | Nothing
    

    When you define a type, you also define its constructor functions. In case of Maybe these are (pseudo-code):

    Just : a -> Maybe a
    
    Nothing : Maybe a
    

    Which means that if you declare this value:

    mayHaveANumber : Maybe Int
    

    You can create it by either

    mayHaveANumber = Nothing
    

    or

    mayHaveANumber = Just 5
    

    The Just and Nothing tags not only serve as constructor functions, they also serve as destructors or patterns in a case expression. Which means that using these patterns you can see inside a Maybe:

    showValue : Maybe Int -> Html msg
    showValue mayHaveANumber =
        case mayHaveANumber of
            Nothing ->
                Html.text "N/A"
    
            Just number ->
                Html.text (toString number)
    

    You can do this, because the Maybe module is defined like

    module Maybe exposing 
        ( Maybe(Just,Nothing)
    

    It could also say

    module Maybe exposing 
        ( Maybe(..)
    

    The two are equivalent in this case, but being explicit is considered a virtue in Elm, especially when you are writing a package.


  1. Hiding implementation details
    As pointed out above it is a deliberate choice that the constructor functions of Maybe are visible for other modules.

    There are other cases, however, when the author decides to hide them. One example of this in core is Dict. As the consumer of the package, you should not be able to see the implementation details of the Red/Black tree algorithm behind Dict and mess with the nodes directly. Hiding the constructor functions forces the consumer of your module/package to only create values of your type (and then transform those values) through the functions you expose.

    This is the reason why sometimes stuff like this appears in code

    type Person =
        Person { name : String, age : Int }
    

    Unlike the type alias definition at the top of this post, this syntax creates a new "union" type with only one constructor function, but that constructor function can be hidden from other modules/packages.

    If the type is exposed like this:

    module Data exposing (Person)
    

    Only code in the Data module can create a Person value and only that code can pattern match on it.


1
我认为主要的区别在于类型检查器是否会在您使用“同义词”类型时发出警告。
创建以下文件,将其放在某个地方并运行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

0

别名只是另一种类型的缩写名称,类似于面向对象编程中的class。例如:

type alias Point =
  { x : Int
  , y : Int
  }

一个没有别名的type可以让你定义自己的类型,这样你就可以为你的应用程序定义像IntString等类型。例如,在通常情况下,它可以用于描述应用程序的状态:
type AppState = 
  Loading          --loading state
  |Loaded          --load successful
  |Error String    --Loading error

这样你就可以在 view 中轻松处理它了:

-- VIEW
...
case appState of
    Loading -> showSpinner
    Loaded -> showSuccessData
    Error error -> showError

...

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

但是为什么以及如何在elm应用程序中使用typetype alias非常重要,你们可以参考Josh Clayton的文章


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接