lue-bird/elm-no-record-type-alias-constructor-function/1.0.3

NOTE: the latest version is 1.0.8

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

tips

why

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.

implicit magic

"There are worse things than being explicit" – Evan

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.

there are better alternatives

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.

only works in very limited scenarios

type alias Record =
    { someField : () }

type alias Indirect =
    Record
  • Record has a constructor function
  • Indirect doesn't have a constructor function
type alias Extended record =
    { record | someField : () }

type alias Constructed =
    Extended {}
  • Constructed, Extended don't have constructor functions

name clash with variant

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

succeed/constant are misused

I'd consider succeed/constant/... in record Decoders/Generators/... unidiomatic. You don't decode or randomly generate anything there.

projectDecoder : Decoder Project
projectDecoder =
    map2
        (\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 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)
        |> 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?

suggestions?

→ contributing.

why a whole package

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 RecordWithoutConstructorFunction will never be aliased to a different type

consider

elm install lue-bird/elm-no-record-type-alias-constructor-function

fields constructor too verbose?

decodeUser =
    map2 (\name status -> { name = name, status = status })
        (field "name" string)
        (field "status" string)

is rather verbose.

There are languages that introduce extra sugar

  • purescript, ...

    map2 (\name status -> { name, status })
        (field "name" string)
        (field "status" string)
  • syntax with its own problems, for example still encouraging succeed/constant misuse

    map2 { name, status }
        (field "name" string)
        (field "status" string)
  • field addition

    succeed {}
        |> andField "name" string (|+ .name)
        |> andField "status" string (|+ .status)

    would be neat but... elm will probably remain a simple language