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

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

misunderstood as special type syntax

experience report by John Pavlick: LETWW, Part 2: "Regular expressions are quite confusing and difficult to use.":

My prior lack of understanding was due to a mental disconnect between the two true sentences, "all type aliases are constructors" and "all constructors are functions".

You see, most of my application development experience has been in csharp. In csharp, we don't have type aliases or anything like that, but we do have classes. Classes have a type, and types have a type (and the type of a type is a type called Type, obviously). 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 (with operators like typeof(), or as function signatures) - I couldn't see what was literally right in front of my eyes: Type names, just like everything else in Elm, are Not Special. They're constructors for a value.

And this was obscured for so long, because when I saw

Parser.succeed Point |= ...

I didn't see "a function (Parser.succeed) taking another function as an argument (Point) and then passing it values collected from a parsing operation" - I saw "Parser.succeed for a Special Type Name".

succeed/constant are misused

I'd consider succeed/constant/... with a constant value in record field value Decoders/Generators/... unidiomatic.

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