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

问题描述 投票:89回答:5

在Elm中,我无法弄清楚type是否适合关键词type alias。文档似乎没有对此进行解释,也无法在发行说明中找到。这是在某处记录的吗?

elm
5个回答
132
投票

我怎么想呢:

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"。即使我们指定thingStringaliasedStringIdentity采用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

7
投票

关键是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体系结构,则更新函数是一个巨大的case语句,因为它是消息路由的目的地,因此被处理。我们在传递消息时使用union类型来处理一个类型,但是然后可以使用case语句来确切地说明它是什么消息,所以我们可以处理它。


4
投票

让我通过关注用例并在构造函数和模块上提供一些上下文来补充以前的答案。


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. 指定必填字段 他们有时称之为“可扩展记录”,这可能会产生误导。此语法可用于指定您希望某些记录包含特定字段。如: 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. 从不同模块重新暴露类型 如果您正在编写程序包(而不是应用程序),则可能需要在一个模块中实现一个类型,可能在内部(未公开)模块中,但您希望从另一个(公共)模块公开该类型。或者,您也希望从多个模块中公开您的类型。 核心和TaskHttp.Request in Http是第一个的例子,而Json.Encode.ValueJson.Decode.Value对是后者的一个例子。 只有在您希望保持类型不透明时才能执行此操作:您不公开构造函数。有关详细信息,请参阅下面的type用法。

值得注意的是,在上面的例子中,只有#1提供了构造函数。如果在#1中公开类型别名,如module Data exposing (Person),它将公开类型名称以及构造函数。

type的用法

  1. 定义标记的联合类型 这是最常见的用例,它的一个很好的例子是Maybe type in coretype Maybe a = Just a | Nothing 定义类型时,还要定义其构造函数。在Maybe的情况下这些是(伪代码): 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背后的红/黑树算法的实现细节,并直接弄乱节点。隐藏构造函数会强制模块/包的使用者仅通过您公开的函数创建类型的值(然后转换这些值)。 这就是为什么有时这样的东西出现在代码中的原因 type Person = Person { name : String, age : Int } 与本文顶部的type alias定义不同,此语法创建一个只有一个构造函数的新“联合”类型,但该构造函数可以从其他模块/包中隐藏。 如果类型暴露如下: module Data exposing (Person) 只有Data模块中的代码才能创建Person值,并且只有该代码可以在其上进行模式匹配。

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
投票

alias只是其他类型的一个较短的名字,类似于OOP中的class。经验:

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

一个type(没有别名)将允许您定义自己的类型,因此您可以为您的app定义类型IntString,....例如,在常见情况下,它可以用于描述app的状态:

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 aliaselm app很重要,你们可以参考article from Josh Clayton

© www.soinside.com 2019 - 2024. All rights reserved.