In Elm Europe 2019, Richard Feldman made a fun talk exploring “What if the only language feature Elm had was custom types?”
Why would you want a language with only custom types? Who cares! It’s fun to explore.
So here’s my what if: “What if Elm had no type classes?”
Note: This is not a proposal. I’m not saying this is a good idea. It’s just fun talking about programming languages!
If you feel like this sounds like an interesting thought experiment – read on!
If you feel like commenting – please do! But please avoid “Why would you do/want/need this?” type of comments – that’s not the point here.
The four type classes of Elm
As far as I know, Elm has four type classes:
- number
- comparable
- appendable
- compappend
Let’s explore what things would look like if those didn’t exist!
appendable
I think the only affected function would be ++
. It currently works with both strings and lists, but would have to be restricted to just one of those.
Idea: Remove ++
altogether! And replace it with:
- String interpolation:
"Total: ${String.fromInt total} USD. Your share: ${String.fromInt yourShare} USD."
- A variant of
List.append
that lets you do[1, 2, 3] |> List.append [4, 5, 6]
to get[1, 2, 3, 4, 5, 6]
. Not sure what a good name would be! Or maybe even allow[1, 2, ...someList]
like in JS.
comparable
Here’s what I use comparisons for:
- To figure out which number is bigger.
- To sort things.
There’s one more use case: Set
and Dict
needs to be able to compare its keys.
Idea:
- Let
<
,>
,<=
,>=
,compare
,min
,max
,List.maximum
,List.minimum
only work with numbers. (Notice how I’ve already mentioned “number” twice? We’ll get back to that in the section about thenumber
typeclass!) - Introduce something like localeCompare for comparing strings.
-
List.sortBy (\person -> (person.lastName, person.firstName))
wouldn’t work anymore. This can be replaced with new sorting API:s! See below. - For data structures such as
Set
andDict
– I’ll get back to that below.
Sorting
Here are some functions I’ve been toying with:
thenBy : (a -> a -> Order) -> a -> a -> Order -> Order
thenBy f a1 a2 order =
case order of
LT ->
LT
EQ ->
f a1 a2
GT ->
GT
x =
List.sortWith
(\p1 p2 ->
String.localeCompare p1.lastName p2.lastName
|> thenBy String.localeCompare p1.firstName p2.firstName
)
compareMap : (a -> b) -> (b -> b -> Order) -> (a -> a -> Order)
compareMap mapper comparer a1 a2 =
comparer (mapper a1) (mapper a2)
y =
List.sortWith
(\p1 p2 ->
compareMap .lastName String.localeCompare p1 p2
|> thenBy (compareMap .firstName String.localeCompare) p1 p2
)
thenBy2 : (a -> a -> Order) -> (a -> a -> Order) -> (a -> a -> Order)
thenBy2 f1 f2 a1 a2 =
f1 a1 a2
|> thenBy f2 a1 a2
z =
List.sortWith
(compareMap .lastName String.localeCompare
|> thenBy2 (compareMap .firstName String.localeCompare)
)
There’s also elm-sorter-experiment by Richard Feldman.
Set and Dict
Let’s say you have a custom type:
type Status
= New
| Open
| Closed
And now you’d like to make a filters : Set Status
. Set
needs to be able to order its elements (implementation detail).
Idea: The compiler could just decide that Status
values should be ordered in the order they where defined: New
, then Open
, then Closed
.
A downside of that is that if somebody sorted a list of Status
, and someone else were in a refactoring mood and felt like ordering all custom types alphabetically:
type Status
= Closed
| New
| Open
Then they would accidentally have changed how that list was sorted. Oops!
So my idea is to have a special function with an annoying name that is only intended to be used for data structures (where the order doesn’t matter, just the fact that things can be ordered, unless I’m mistaken). Something like Basics.orderForUseInDataStructures : a -> a -> Order
(but with a better name). Maybe that function shouldn’t return Order
to be incompatible with sorting functions (instead, it could return InteralOrder
that is otherwise identical to Order
).
The idea here is that the compiler could “just know” how to order any values, just like it “just knows” how values are equal (but there’s no equalable
type class). And this ordering should only be used for data structures, not sorting a list before displaying it or so.
compappend
compappend
is comparable
and appendable
at the same time. (“Only strings and lists are both comparable and appendable” according to the compiler.)
We’ve already removed comparable
and appendable
above, so there shouldn’t be a need for this combination anymore.
number
Finally, the tricky one.
Here’s a thread about it: The problems with numbers in Elm
And a GitHub issue which suggests having different operators for Int and Float: https://github.com/elm/compiler/issues/2078
The clever thing about the “different operators” proposal is that it adds a .
at the end of operators, just like you add a .
in a literal to make it a float. You’d have 5 + 3
and 5.0 +. 3.1
. And 5 / 2 == 2
while 5.0 /. 2.0 == 2.5
. And 5 > 2
vs 5.0 >. 2.0
.
In this universe of “no typeclasses” (for no particular reason), that’s the best I can come up with.
If so, I think it would make sense to separate out most functions from Basics
to new Int
and Float
modules.
(If you want to take everything one step further – what if there was no operators at all (except pipes)? Like, 1 |> Int.add 2 |> Int.equals 3
instead of 1 + 2 == 3
. But let’s not go there in this thread.)