Reputation: 4593
In Elm, I can't figure out when type
is the appropriate keyword vs. type alias
. The documentation doesn't seem to have an explanation of this, nor can I find one in the release notes. Is this documented somewhere?
Upvotes: 104
Views: 7902
Reputation: 2120
An alias
is just an alternate (usually shorter) name for some other type, often used similarly to a struct
in OOP. For example:
type alias Point =
{ x : Int
, y : Int
}
-- The above type alias allows writing this type signature
-- for a function taking two points and returning a float:
distance : Point -> Point -> Float
-- ...instead of needing to write this equivalent, long-winded one:
distance : { x : Int, y : Int } -> { x : Int, y : Int } -> Float
A type
(without alias) lets you define a custom type.
A custom type is similar to an enum
in other languages, but with an extra feature which is that each variant of a custom type may carry along relevant data.
For example, here's a custom type describing the state of an app:
type Model
= Loading -- Loading...
| Loaded SomeData -- Loaded successfully
| Failed String -- Failure, with a string to describe the error
-- SomeData represents the data we were loading, and is defined
-- elsewhere, as either a custom type or a type alias.
Custom types are the bedrock of data modeling in Elm. When you have a variable that is a Model
(the type defined above), you can't try to, for example, access the error description String when not in the Failed
state; there isn't even syntax to do so. This means that you're able to precisely model which data is available for any given state (that is, in any given variant of a custom type). You don't end up with any variables that may or may not be set (if the code compiles, it won't try to dereference null
or undefined
). For times when there legitimately may or may not be a value for a variable, the core library's Maybe
type (a custom type) allows using optional values safely.
Here's an example of using the Model
type defined above, in a view
function:
case model of
Loading ->
viewLoadingSpinner
Loaded instanceOfSomeData ->
viewRemoteData instanceOfSomeData
Failed errorDescription ->
viewError errorDescription
Hopefully that helps clarify the differences between custom types and type aliases!
This article may also be helpful.
Upvotes: 0
Reputation: 1676
Let me complement the previous answers by focusing on use-cases and providing a little context on constructor functions and modules.
type alias
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)
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.
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
.
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.
It is worth noticing that in the above examples only #1 provides a constructor function. If you expose your type alias in #1 like module Data exposing (Person)
that will expose the type name as well as the constructor function.
type
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.
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.
Upvotes: 8
Reputation: 6676
The key is the word alias
. In the course of programming, when you want to group things that belong together, you put it in a record, like in the case of a point
{ x = 5, y = 4 }
or a student record.
{ name = "Billy Bob", grade = 10, classof = 1998 }
Now, if you needed to pass these records around, you'd have to spell out the entire type, like:
add : { x:Int, y:Int } -> { x:Int, y:Int } -> { x:Int, y:Int }
add a b =
{ a.x + b.x, a.y + b.y }
If you could alias a point, the signature would be so much easier to write!
type alias Point = { x:Int, y:Int }
add : Point -> Point -> Point
add a b =
{ a.x + b.x, a.y + b.y }
So an alias is a shorthand for something else. Here, it's a shorthand for a record type. You can think of it as giving a name to a record type you'll be using often. That's why it's called an alias--it's another name for the naked record type that's represented by { x:Int, y:Int }
Whereas type
solves a different problem. If you're coming from OOP, it's the problem you solve with inheritance, operator overloading, etc.--sometimes, you want to treat the data as a generic thing, and sometimes you want to treat it like a specific thing.
A commonplace where this happens is when passing around messages--like the postal system. When you send a letter, you want the postal system to treat all messages as the same thing, so you only have to design the postal system once. And besides, the job of routing the message should be independent of the message contained within. It's only when the letter reaches its destination do you care about what the message is.
In the same way, we might define a type
as a union of all the different types of messages that could happen. Say we're implementing a messaging system between college students to their parents. So there are only two messages college kids can send: 'I need beer money' and 'I need underpants'.
type MessageHome = NeedBeerMoney | NeedUnderpants
So now, when we design the routing system, the types for our functions can just pass around MessageHome
, instead of worrying about all the different types of messages it could be. The routing system doesn't care. It only needs to know it's a MessageHome
. It's only when the message reaches its destination, the parent's home, that you need to figure out what it is.
case message of
NeedBeerMoney ->
sayNo()
NeedUnderpants ->
sendUnderpants(3)
If you know the Elm architecture, the update function is a giant case statement, because that's the destination of where the message gets routed, and hence processed. And we use union types to have a single type to deal with when passing the message around, but then can use a case statement to tease out exactly what message it was, so we can deal with it.
Upvotes: 11
Reputation: 6545
The main difference, as I see it, is whether type checker will yell on you if you use "synomical" type.
Create the following file, put it somewhere and run elm-reactor
, then go to http://localhost:8000
to see the difference:
-- 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
If you uncomment 2.
and comment 1.
you will see:
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
Upvotes: 1
Reputation: 7220
How I think of it:
type
is used for defining new union types:
type Thing = Something | SomethingElse
Before this definition Something
and SomethingElse
didn't mean anything. Now they are both of type Thing
, which we just defined.
type alias
is used for giving a name to some other type that already exists:
type alias Location = { lat:Int, long:Int }
{ lat = 5, long = 10 }
has type { lat:Int, long:Int }
, which was already a valid type. But now we can also say it has type Location
because that is an alias for the same type.
It is worth noting that the following will compile just fine and display "thing"
. Even though we specify thing
is a String
and aliasedStringIdentity
takes an AliasedString
, we won't get an error that there is a type mismatch between 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
Upvotes: 142