trick: no record
type aliasconstructor function
forms example in guide.elm-lang.org:
type alias Model =
{ name : String
, password : String
, passwordAgain : String
}
init : Model
init =
Model "" "" ""↑ Every directly aliased record type gets its default constructor function.
You can trick the compiler into not creating a Model record constructor function:
import RecordWithoutConstructorFunction exposing (RecordWithoutConstructorFunction)
type alias Model =
RecordWithoutConstructorFunction
{ name : String
, password : String
, passwordAgain : String
}
init : Model
init =
-- Model "" "" "" <- error
{ name = "", password = "", passwordAgain = "" }where
type alias RecordWithoutConstructorFunction record =
record-
find & fix your current usages of record
type aliasconstructor functions:elm-reviewruleNoRecordAliasConstructor -
insert
RecordWithoutConstructorFunction/... where necessary:elm-reviewruleNoRecordAliasWithConstructor
Fields in a record don't have a "natural order".
{ age = 42, name = "Balsa" }
== { name = "Balsa", age = 42 }
--> TrueSo it shouldn't matter whether you write
type alias User =
{ name : String, age : Int }
or { age : Int, name : String }as well.
User "Balsa" 42however relies on a specific field order in the type and is more difficult to understand/read. These constructors also open up the possibility for bugs to sneak in without the compiler warning you:
type alias User =
{ status : String
, name : String
}
decodeUser : Decoder User
decodeUser =
map2 User
(field "name" string)
(field "status" string)Did you spot the mistake? ↑ a similar example
To avoid these kinds of bugs, just forbid type alias constructors:
type alias User =
RecordWithoutConstructorFunction ...Problems don't end there.
Most record type aliases are not intended to work with positional arguments!
Model is the perfect example.
Even if you think it's ok currently, no one reminds you when you add new fields.
It's so easy to create an explicit constructor
xy : Float -> Float -> { x : Float, y : Float }As argued, unnamed arguments shouldn't be the default.
Additionally, your record will be more descriptive and type-safe as a type
type Cat
= Cat { mood : Mood, birthTime : Time.Posix }to make wrapping, unwrapping and combining easier, you can try typed-value.
type alias Record =
{ someField : () }
type alias Indirect =
RecordRecordhas a constructor functionIndirectdoesn't have a constructor function
type alias Extended record =
{ record | someField : () }
type alias Constructed =
Extended {}Constructed,Extendeddon't have constructor functions
example adapted from Elm.Syntax.Exposing
type TopLevelExpose
= ...
| TypeExpose TypeExpose
type alias TypeExpose =
{ name : String
, open : Maybe Range
}NAME CLASH - multiple defined
TypeExposetype constructors.How can I know which one you want? Rename one of them!
Either rename type alias TypeExpose to TypeExposeData/... or
type alias TypeExpose =
RecordWithoutConstructorFunction ...and get rid of the compile-time error
My prior lack of understanding was due to a mental disconnect between the two true sentences, "record type aliases come with implicit constructors" and "all constructors are functions"
[...] After marinating for over a decade in a type system where type names are "special" and have to be invoked only in certain special-case contexts - I couldn't see [...]:
Type names, just like everything else in Elm, are Not Special. They're constructors for a value [= functions].
I'd consider succeed/constant/... with a constant value in record field value Decoders/Generators/... unidiomatic.
projectDecoder : Decoder Project
projectDecoder =
map3
(\name scale selected ->
{ name = name
, scale = scale
, selected = selected
}
)
(field "name" string)
(field "scale" float)
(succeed NothingSelected) -- weirdConstants should much rather be introduced explicitly in a translation step:
projectDecoder : Decoder Project
projectDecoder =
map2
(\name scale ->
{ name = name
, scale = scale
, selected = NothingSelected
}
)
(field "name" string)
(field "scale" float)For record Codecs (from MartinSStewart's elm-serialize in this example) where we don't need to encode every field value:
serializeProject : Codec String Project
serializeProject =
record Project
|> field .name string
|> field .scale float
|> field .selected
(succeed NothingSelected)
|> finishRecordsucceed is a weird concept for codecs because some dummy value must be encoded which will never be read.
It does not exist in elm-serialize, but it does exist in miniBill's elm-codec (, prozacchiwawa's elm-json-codec, ...):
Create a Codec that produces null as JSON and always decodes as the same value.
Do you really want this behavior? If not, you'll need
serializeProject : Codec String Project
serializeProject =
record
(\name scale ->
{ name = name
, scale = scale
, selected = NothingSelected
}
)
|> field .name string
|> field .scale float
|> finishRecordWhy not consistently use this record constructing method?
This will also be used often for versioning
enum ProjectVersion0 [ ... ]
|> andThen
(\version ->
case version of
ProjectVersion0 ->
record
(\name -> { name = name, scale = 1 })
|> field .name string
|> finishRecord
...
)Again: Why not consistently use this record constructing method?
→ contributing.
RecordWithoutConstructorFunction.elm can simply be copied to your project.
However, if you want
- no separate
RecordWithoutConstructorFunctions hanging around - a single place for up to date public documentation
- a common recognizable name
- safety that
RecordWithoutConstructorFunctionwill never be aliased to a different type
consider
elm install lue-bird/elm-no-record-type-alias-constructor-function
decodeUser =
map2 (\name status -> { name = name, status = status })
(field "name" string)
(field "status" string)is rather verbose.
There are languages that introduce extra sugar:
succeed {}
|> field &name "name" string
|> field &status "status" stringwould be simple and neat. elm dropped this for simplicity.
This is present in purescript and other languages
map2 (\name status -> { name, status })
(field "name" string)
(field "status" string)Jeroen's made a convincing argument on negative consequences for descriptiveness in contexts of functions growing larger.
map2 { name, status }
(field "name" string)
(field "status" string)with either
{ x, y } : Int -> Int -> { x : Int, y : Int }
{ x =, y = } : Int -> Int -> { x : Int, y : Int }
{ x = _, y = _ } : Int -> Int -> { x : Int, y : Int }
{ \x y } : Int -> Int -> { x : Int, y : Int }The last one was proposed in a very old discussion
It's concise but quite limited in what it can do while not fixing many problems:
-
problems with
succeed/constantmisuse remain- → confusing
{ x, y = 0, z }syntax necessary
- → confusing
-
less intuitive?
- recognizing it as a function
- unlike punning in other languages
-
less explicit than record field punning a.k.a "doesn't scale well" (arguments are taken as they come, can't be combined, ...)
Explored in "Safe and explicit records constructors exploration".
Instead of
Point : Int -> Int -> Point
Point : { x : Int } -> { y : Int } -> Point- problems with "doesn't scale: can't expose extensible, extended indirect" remain (if not fixed somehow)
- defined field ordering should explicitly not matter on by-nature-non-positional records
- could again seem magical and unintuitive
Codec.group (\{ x } { y } -> { x = x, y = y })
|> Codec.part ( .x, \x -> { x = x })
Codec.int
|> Codec.part ( .y, \y -> { y = y })
Codec.intor better, to always avoid shadowing errors:
Codec.group (\p0 p1 -> { x = .x p0, y = .y p1 })
|> Codec.part ( .x, \x -> { x = x })
Codec.int
|> Codec.part ( .y, \y -> { y = y })
Codec.intMaking this less verbose with not-necessarily-language-sugared-but-code-generable field stuff:
Codec.group (\p0 p1 -> { x = .x p0, y = .y p1 })
|> Codec.part Record.x Codec.int
|> Codec.part Record.y Codec.intwith
import Record exposing (x, y)
x : Part x { x : x } { record_ | x : x }
x =
part
{ access = .x
, named = \x -> { x = x }
, alter = \alter r -> { r | x = r.x |> alter }
, description = "x"
}I bet ideas like this won't be widely adopted (in packages) because setting up tools alongside might be seen as too much work (for beginners).