Skip to content

Commit 047a256

Browse files
committed
try to provide better guidance on certain types that are not comparable
1 parent 86583d0 commit 047a256

File tree

3 files changed

+206
-3
lines changed

3 files changed

+206
-3
lines changed

compiler/src/Reporting/Error/Type.hs

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -559,11 +559,31 @@ badFlexSuper :: T.Direction -> T.Super -> T.Type -> [D.Doc]
559559
badFlexSuper direction super tipe =
560560
case super of
561561
T.Comparable ->
562-
[ D.toSimpleHint "Only ints, floats, chars, strings, lists, and tuples are comparable."
563-
]
562+
case tipe of
563+
T.Record _ _ ->
564+
[ D.link "Hint"
565+
"I do not know how to compare records. I can only compare ints, floats,\
566+
\ chars, strings, lists of comparable values, and tuples of comparable values.\
567+
\ Check out" "comparing-records" "for ideas on how to proceed."
568+
]
569+
570+
T.Type _ name _ ->
571+
[ D.toSimpleHint $
572+
"I do not know how to compare `" ++ Name.toChars name ++ "` values. I can only\
573+
\ compare ints, floats, chars, strings, lists of comparable values, and tuples\
574+
\ of comparable values."
575+
, D.reflowLink
576+
"Check out" "comparing-custom-types" "for ideas on how to proceed."
577+
]
578+
579+
_ ->
580+
[ D.toSimpleHint $
581+
"I only know how to compare ints, floats, chars, strings, lists of\
582+
\ comparable values, and tuples of comparable values."
583+
]
564584

565585
T.Appendable ->
566-
[ D.toSimpleHint "Only strings and lists are appendable."
586+
[ D.toSimpleHint "I only know how to append strings and lists."
567587
]
568588

569589
T.CompAppend ->

hints/comparing-custom-types.md

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Comparing Custom Types
2+
3+
The built-in comparison operators work on a fixed set of types, like `Int` and `String`. That covers a lot of cases, but what happens when you want to compare custom types?
4+
5+
This page aims to catalog these scenarios and offer alternative paths that can get you unstuck.
6+
7+
8+
## Wrapped Types
9+
10+
It is common to try to get some extra type safety by creating really simple custom types:
11+
12+
```elm
13+
type Id = Id Int
14+
type Age = Age Int
15+
16+
type Comment = Comment String
17+
type Description = Description String
18+
```
19+
20+
By wrapping the primitive values like this, the type system can now help you make sure that you never mix up a `Id` and an `Age`. Those are different types! This trick is extra cool because it has no runtime cost in `--optimize` mode. The compiler can just use an `Int` or `String` directly when you use that flag!
21+
22+
The problem arises when you want to use a `Id` as a key in a dictionary. This is a totally reasonable thing to do, but the current version of Elm cannot handle this scenario.
23+
24+
Instead of creating a `Dict Id Info` type, one thing you can do is create a custom data structure like this:
25+
26+
```elm
27+
module User exposing (Id, Table, empty, get, add)
28+
29+
import Dict exposing (Dict)
30+
31+
32+
-- USER
33+
34+
type Id = Id Int
35+
36+
37+
-- TABLE
38+
39+
type Table info =
40+
Table Int (Dict Int info)
41+
42+
empty : Table info
43+
empty =
44+
Table 0 Dict.empty
45+
46+
get : Id -> Table info -> Maybe info
47+
get (Id id) (Table _ dict) =
48+
Dict.get id dict
49+
50+
add : info -> Table info -> (Table info, Id)
51+
add info (Table nextId dict) =
52+
( Table (nextId + 1) (Dict.insert nextId info dict)
53+
, nextId
54+
)
55+
```
56+
57+
There are a couple nice thing about this approach:
58+
59+
1. The only way to get a new `User.Id` is to `add` information to a `User.Table`.
60+
2. All the operations on a `User.Table` are explicit. Does it make sense to remove users? To merge two tables together? Are there any special details to consider in those cases? This will always be captured explicitly in the interface of the `User` module.
61+
3. If you ever want to switch the internal representation from `Dict` to `Array` or something else, it is no problem. All the changes will be within the `User` module.
62+
63+
So while this approach is not as convenient as using a `Dict` directly, it has some benefits of its own that can be helpful in some cases.
64+
65+
66+
## Enumerations to Ints
67+
68+
Say you need to define a `trafficLightToInt` function:
69+
70+
```elm
71+
type TrafficLight = Green | Yellow | Red
72+
73+
trafficLightToInt : TrafficLight -> Int
74+
trafficLightToInt trafficLight =
75+
???
76+
```
77+
78+
We have heard that some people would prefer to use a dictionary for this sort of thing. That way you do not need to write the numbers yourself, they can be generated such that you never have a typo.
79+
80+
I would recommend using a `case` expression though:
81+
82+
```elm
83+
type TrafficLight = Green | Yellow | Red
84+
85+
trafficLightToInt : TrafficLight -> Int
86+
trafficLightToInt trafficLight =
87+
case trafficLight of
88+
Green -> 1
89+
Yellow -> 2
90+
Red -> 3
91+
```
92+
93+
This is really straight-forward while avoiding questions like “is `Green` less than or greater than `Red`?”
94+
95+
96+
## Something else?
97+
98+
If you have some other situation, please tell us about it [here](https://github.com/elm/error-message-catalog/issues). That is a log of error messages that can be improved, and we can use the particulars of your scenario to add more advice on this page!

hints/comparing-records.md

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# Comparing Records
2+
3+
The built-in comparison operators work on a fixed set of types, like `Int` and `String`. That covers a lot of cases, but what happens when you want to compare records?
4+
5+
This page aims to catalog these scenarios and offer alternative paths that can get you unstuck.
6+
7+
8+
## Sorting Records
9+
10+
Say we want a `view` function that can show a list of students sorted by different characterists.
11+
12+
We could create something like this:
13+
14+
```elm
15+
import Html exposing (..)
16+
17+
type alias Student =
18+
{ name : String
19+
, age : Int
20+
, gpa : Float
21+
}
22+
23+
type Order = Name | Age | GPA
24+
25+
viewStudents : Order -> List Student -> Html msg
26+
viewStudents order studentns =
27+
let
28+
orderlyStudents =
29+
case order of
30+
Name -> List.sortBy .name students
31+
Age -> List.sortBy .age students
32+
GPA -> List.sortBy .gpa students
33+
in
34+
ul [] (List.map viewStudent orderlyStudents)
35+
36+
viewStudent : Student -> Html msg
37+
viewStudent student =
38+
li [] [ text student.name ]
39+
```
40+
41+
If you are worried about the performance of changing the order or updating information about particular students, you can start using the [`Html.Lazy`](https://package.elm-lang.org/packages/elm/html/latest/Html-Lazy) and [`Html.Keyed`](https://package.elm-lang.org/packages/elm/html/latest/Html-Keyed) modules. The updated code would look something like this:
42+
43+
```elm
44+
import Html exposing (..)
45+
import Html.Lazy exposing (lazy)
46+
import Html.Keyed as Keyed
47+
48+
type Order = Name | Age | GPA
49+
50+
type alias Student =
51+
{ name : String
52+
, age : Int
53+
, gpa : Float
54+
}
55+
56+
viewStudents : Order -> List Student -> Html msg
57+
viewStudents order studentns =
58+
let
59+
orderlyStudents =
60+
case order of
61+
Name -> List.sortBy .name students
62+
Age -> List.sortBy .age students
63+
GPA -> List.sortBy .gpa students
64+
in
65+
Keyed.ul [] (List.map viewKeyedStudent orderlyStudents)
66+
67+
viewKeyedStudent : Student -> (String, Html msg)
68+
viewKeyedStudent student =
69+
( student.name, lazy viewStudent student )
70+
71+
viewStudent : Student -> Html msg
72+
viewStudent student =
73+
li [] [ text student.name ]
74+
```
75+
76+
By using `Keyed.ul` we help the renderer move the DOM nodes around based on their key. This makes it much cheaper to reorder a bunch of students. And by using `lazy` we help the renderer skip a bunch of work. If the `Student` is the same as last time, the render can skip over it.
77+
78+
> **Note:** Some people are skeptical of having logic like this in `view` functions, but I think the alternative (maintaining sort order in your `Model`) has some serious downsides. Say a colleague is adding a message to `Add` students, but they do not know about the sort order rules needed for presentation. Bug! So in this alternate design, you must diligently test your `update` function to make sure that no message disturbs the sort order. This is bound to lead to bugs over time!
79+
>
80+
> With all the optimizations possible with `Html.Lazy` and `Html.Keyed`, I would always be inclined to work on optimizing my `view` functions rather than making my `update` functions more complicated and error prone.
81+
82+
83+
## Something else?
84+
85+
If you have some other situation, please tell us about it [here](https://github.com/elm/error-message-catalog/issues). That is a log of error messages that can be improved, and we can use the particulars of your scenario to add more advice on this page!

0 commit comments

Comments
 (0)