on master help sign in
Merge pull request #1138 from elm-lang/dev
7dfc727Evan Czaplicki
hints/infinite-type.mdnew file+116
@@ -0,0 +1,116 @@
1+# Hints for Infinite Types
2+
3+Infinite types are probably the trickiest kind of bugs to track down. Usually it means you are working on a relatively sophisticated recursive function and something is very subtly off.
4+
5+I am just going to show a couple examples of infinite types that have been reported and how they were resolved to illustrate the kinds of things you may be running into.
6+
7+
8+## Nested Record Types
9+
10+(From issue [#987](https://github.com/elm-lang/elm-compiler/issues/987))
11+
12+Someone was working with records, and wanted to do something kind of tricky with type aliases. In this case, they created a `Persisted` type alias that would say "whatever record you have, well I'm talking about one where there is an additional `id` field."
13+
14+```elm
15+type alias Persisted record =
16+ { record | id : Int }
17+
18+type alias Update record =
19+ record -> Int -> record
20+
21+type Updatable record =
22+ Updatable (Update record) (Persisted record)
23+
24+newRecord (Updatable myUpdate myRecord) =
25+ let
26+ updateThisRecord =
27+ myUpdate myRecord
28+ in
29+ "foo"
30+```
31+
32+Now just looking at the code, it seems fine. You can create `Persisted` records, you can have a record `Update`, and you can bundle them together with `Updatable`. In our `newRecord` function we are partially applying the `myUpdate` function. Everything *seems* fine, but something is causing an infinite type.
33+
34+The best thing to do is start writing down as many types for yourself as possible:
35+
36+```elm
37+myUpdate : Update record -- record -> Int -> record
38+myRecord : Persisted record -- { record | id : Int }
39+```
40+
41+The trouble is that it's the same `record` in both of these types! The `myUpdate` function wants t take a `record` and produce a `record`, but it is being given `{ record | id : Int }`. Type checking is going to say, "well, if `record` is actually an infinite sequence of `{ { ... | id : Int } | id : Int }` then they *can* be equal!" While that is true and very clever, it is not the intent of this code.
42+
43+So the fix for this person was to add one thing into the `Updatable` type:
44+
45+```elm
46+type Updatable record =
47+ Updatable (Update (Persisted record)) (Persisted record)
48+```
49+
50+Notice the new `Persisted` in the `Update` part. That was the fix!
51+
52+
53+## Exploding Either
54+
55+(From issue [#707](https://github.com/elm-lang/elm-compiler/issues/707))
56+
57+There is a relatively obscure package called `evancz/automaton` that captures state and functionality using callbacks. So far, it does not seem to be extremely useful idea given its downsides, but it did lead to quite a tough infinite type.
58+
59+Here is a snippet of code that combines two bacic automatons into one. The resulting automaton takes in either a `Left` or `Right` value. If it is a `Left` we will use the left automaton, if it is a `Right` we will use the right one.
60+
61+```elm
62+type Either a b = Left a | Right b
63+
64+type Automaton a b =
65+ Step (a -> (Automaton a b, b))
66+
67+(+++) : Automaton a b -> Automaton c d -> Automaton (Either a c) (Either b d)
68+(+++) (Step leftStep) (Step rightStep) =
69+ Step <| \either ->
70+ case either of
71+ Left a ->
72+ let (newLeft, b) = leftStep a
73+ in (newLeft, Left b)
74+
75+ Right c ->
76+ let (newRight, d) = rightStep c
77+ in (newRight, Right d)
78+```
79+
80+This triggers a relatively confusing error message. The right thing to do in these scenarios is try to figure out the type of literally every piece of the definition. If you can, break parts out into other top-level declarations with type annotations so the compiler can check your work. So you might write down some notes to yourself like this:
81+
82+```elm
83+left : Automaton a b
84+leftStep : a -> (Automaton a b, b)
85+
86+right : Automaton c d
87+rightStep : c -> (Automaton c d, d)
88+
89+either : Either a c
90+
91+newLeft : Automaton a b
92+newRight : Automaton c d
93+```
94+
95+Knowing that both branches of a `case` must end with the same type, it looks like `newLeft` and `newRight` are causing the issue here. So in the revised code we make it so both branches return the same type.
96+
97+```elm
98+type Either a b = Left a | Right b
99+
100+type Automaton a b =
101+ Step (a -> (Automaton a b, b))
102+
103+(+++) : Automaton a b -> Automaton c d -> Automaton (Either a c) (Either b d)
104+(+++) (Step leftStep as left) (Step rightStep as right) =
105+ Step <| \either ->
106+ case either of
107+ Left a ->
108+ let (newLeft, b) = leftStep a
109+ in (newLeft +++ right, Left b)
110+
111+ Right c ->
112+ let (newRight, d) = rightStep c
113+ in (left +++ newRight, Right d)
114+```
115+
116+So yeah, if you are skimming this, you probably did not see the change immediately. My experience debugging these kinds of things is that my brain thinks what I wrote is correct so much that it becomes kind of blind to the issue. The best thing to do is slow down and break the problem apart. Or ask someone.
src/Reporting/Error/Type.hs+36−5
@@ -1,6 +1,7 @@
1{-# OPTIONS_GHC -Wall #-}
1{-# OPTIONS_GHC -Wall #-}
2module Reporting.Error.Type where
2module Reporting.Error.Type where
3
3
4+import qualified Data.Map as Map
4import qualified Data.Maybe as Maybe
5import qualified Data.Maybe as Maybe
5import Text.PrettyPrint.ANSI.Leijen
6import Text.PrettyPrint.ANSI.Leijen
6 ( Doc, (<>), (<+>), colon, dullyellow, equals
7 ( Doc, (<>), (<+>), colon, dullyellow, equals
@@ -230,7 +231,7 @@ mismatchToReport localizer (MismatchInfo hint leftType rightType maybeReason) =
230 ( cmpHint
231 ( cmpHint
231 (prettyName op ++ " is expecting the left argument to be a:")
232 (prettyName op ++ " is expecting the left argument to be a:")
232 "But the left argument is:"
233 "But the left argument is:"
233− []
234+ (binopHint op leftType rightType)
234 )
235 )
235
236
236 BinopRight op region ->
237 BinopRight op region ->
@@ -240,10 +241,13 @@ mismatchToReport localizer (MismatchInfo hint leftType rightType maybeReason) =
240 ( cmpHint
241 ( cmpHint
241 (prettyName op ++ " is expecting the right argument to be a:")
242 (prettyName op ++ " is expecting the right argument to be a:")
242 "But the right argument is:"
243 "But the right argument is:"
244+ ( binopHint op leftType rightType
245+ ++
243− [ "I always figure out the type of the left argument first and if it is\
246+ [ "I always figure out the type of the left argument first and if it is\
244− \ acceptable on its own, I assume it is \"correct\" in subsequent checks.\
247+ \ acceptable on its own, I assume it is \"correct\" in subsequent checks.\
245− \ So the problem may actually be in how the left and right arguments interact."
248+ \ So the problem may actually be in how the left and right arguments interact."
246− ]
249+ ]
250+ )
247 )
251 )
248
252
249 Binop op ->
253 Binop op ->
@@ -446,6 +450,33 @@ comparisonHint localizer leftType rightType leftWords rightWords finalHints =
446
450
447
451
448
452
453+-- BINOP HINTS
454+
455+binopHint :: Var.Canonical -> Type.Canonical -> Type.Canonical -> [String]
456+binopHint op leftType rightType =
457+ let
458+ leftString =
459+ show (RenderType.toDoc Map.empty leftType)
460+
461+ rightString =
462+ show (RenderType.toDoc Map.empty rightType)
463+ in
464+ if Var.is ["Basics"] "+" op && elem "String" [leftString, rightString] then
465+ [ "To append strings in Elm, you need to use the (++) operator, not (+). "
466+ ++ "<http://package.elm-lang.org/packages/elm-lang/core/latest/Basics#++>"
467+ ]
468+
469+ else if Var.is ["Basics"] "/" op && elem "Int" [leftString, rightString] then
470+ [ "The (/) operator is specifically for floating point division, and (//) is\
471+ \ for integer division. You may need to do some conversions between ints and\
472+ \ floats to get both arguments matching the division operator you want."
473+ ]
474+
475+ else
476+ []
477+
478+
479+
449-- MISMATCH HELPERS
480-- MISMATCH HELPERS
450
481
451
482