After only glancing a little over your blog post last time, I’ve now committed to thoroughly reading it this time.
First of all, some nitpicking:
(please skip to “Now, an Idea”, if you want to read interesting stuff)
1
One of the code examples lists this code:
type alias T1 = @A () or @B () or @C ()
decodeFromString1 : String -> Maybe (a or T1)
decodeFromString1 str = case str of
"a" -> Just <| @A ()
"b" -> Just <| @B ()
"c" -> Just <| @C ()
_ -> Nothing
There’s no syntax for something like { a | Person }
in Elm right now, so I don’t quite understand why there should be (a or T1)
.
Shouldn’t you add the type parameter to T1
directly? Sorry if this came up in the thread already. I think I’ve read through all of this, but that was two weeks ago already.
2
In general, the elm community seems to not recommend using extensible record types.
One of the reasons is that an alternative to extensible records is simply nesting records. That’s what Evan recommended years ago (maybe his views have changed).
But your blog says the recommendation is to ‘Keep your models flat’.
Trying to combine the two recommendations, I’d argue that ‘Keep your models flat’ doesn’t mean ‘Keep your models flat and abstract with extensible records instead of nesting records’, but something like ‘Keep your models flat for now and don’t abstract prematurely’.
3
You make two big points to advocate for extensible union types.
At a high level extensible unions generally bring two things to the table:
- The ability to make a type’s size “just right” when a union type is the output of a function.
- The ability to use flat hierarchies of union types rather than being forced into nested hierarchies.
I really like how concise they are, btw.
But (unless I’m missing something) these points seems to be more about the structuralness than about the extensiblity of union types. I think it would make sense to ‘pitch’ your Idea to be more about structural union types than about extensible union types. Obviously, you’d need extensible union types, as you need to infer something useful, for e.g. f = @A ()
, but the big benefits of your proposal could be used without ever writing any extensible union type signatures, just like today many people use the huge benefits of structural record types without ever writing extensible record type signatures, even though the ‘extensible’ feature is absolutely necessary for structural record types to exist in conjunction with type inference.
Though that’s just a mad level of nitpicking about how to pitch this.
Now, an Idea
We can (almost) try out (a little less ergonomic version of) extensible union types today!
One huge problem this proposal is set out to solve is to break up huge message types that naturally grow, when you grow an application.
Let’s say you have such a message type Msg
. Now you’ve got a view function:
view : Model -> Html Msg
view model =
layoutSite
(viewHeader model)
(viewBody model)
(viewFooter model)
viewHeader : Model -> Html Msg
viewHeader = ...
viewFooter : Model -> Html Msg
viewFooter = ...
Now, viewHeader
and viewFooter
have this problem that you mention in your article: They’re over-constrained.
Just like
-- An exaggerated example that people don't do, but illustrative
addOne : Int -> Maybe Int
addOne x = Just (x + 1)
which doesn’t need to return Maybe
, viewHeader
doesn’t produce any constructor of Msg
, but maybe just 6-10 of them, like GoToPage
, when you click on a settings icon, or ToggleDarkTheme
and some more.
But as you noted in your article, records and unions are dual to each other. So when you have extensible record types, you almost have extensible union types:
viewHeader :
{ a
-- these fields mirror the signatures of your union constructors
| goToPage : Url -> msg
, toggleDarkTheme : msg
... -- other messages used by viewHeader
-- but no messages used by e.g. viewFooter
}
-> Html msg
viewHeader =
This way, you make the msg type of your viewHeader
function ‘just right’.
What you’d do to use this, would be to create one huge record of all of your constructors for Msg
, let’s call that msgConstructors
and pass that to all view functions:
view : Model -> Html Msg
view model =
layoutSite
(viewHeader msgConstructors model)
(viewBody msgConstructors model)
(viewFooter msgConstructors model)
msgConstructors =
{ goToPage = GoToPage
, toggleDarkTheme = ToggleDarkTheme
, emailFormSubmit = EmailFormSubmit -- used in viewFooter
... -- a field for all other message constructor
}
However, there are still differences with this approach and actual structural union types:
- You have to define this
msgConstructors
record!
- When adding a message constructor, you need to also add it to
msgConstructors
. Also, msgConstructors
and Msg
can get out-of-sync and there can be errors in translation.
- Abstracting out these ‘extensible union types’ doesn’t take only 1, but 2 type parameters:
type alias HeaderMsgs a msg =
{ a
| goToPage : Url -> msg -- what else, if not 'msg'?
, toggleDarkTheme : msg
...
}
With actual extensible union types, it’s only 1 type parameter.
But I like that it’s almost possible to try out how extensible union types could be used to structure an elm application today. In such a way that it’s also possible to run it and verify that it gives you the benefits you hope for.
Sidenote:
I also remember that something like msgConstructors
gets auto-generated in the functional programming language ‘Dhall’ for their extensible union types. They essentiall support this:
type alias HeaderMsgs =
@GoToPage or @ToggleDarkTheme
msgConstructors = HeaderMsgs -- a value-level identifier generated for the type-level type alias definition.
For more info on that feature see the dhall language cheatsheet.