trick: no record
type alias
constructor 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 alias
constructor functions:elm-review
ruleNoRecordAliasConstructor
-
insert
RecordWithoutConstructorFunction
/... where necessary:elm-review
ruleNoRecordAliasWithConstructor
Fields in a record don't have a "natural order".
{ age = 42, name = "Balsa" }
== { name = "Balsa", age = 42 }
--> True
So it shouldn't matter whether you write
type alias User =
{ name : String, age : Int }
or { age : Int, name : String }
as well.
User "Balsa" 42
however 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 =
Record
Record
has a constructor functionIndirect
doesn't have a constructor function
type alias Extended record =
{ record | someField : () }
type alias Constructed =
Extended {}
Constructed
,Extended
don'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
TypeExpose
type 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 Decoder
s/Generator
s/... unidiomatic.
projectDecoder : Decoder Project
projectDecoder =
map3
(\name scale selected ->
{ name = name
, scale = scale
, selected = selected
}
)
(field "name" string)
(field "scale" float)
(succeed NothingSelected) -- weird
Constants 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 Codec
s (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)
|> finishRecord
succeed
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
|> finishRecord
Why 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
RecordWithoutConstructorFunction
s hanging around - a single place for up to date public documentation
- a common recognizable name
- safety that
RecordWithoutConstructorFunction
will 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" string
would 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
/constant
misuse 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.int
or 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.int
Making 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.int
with
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).