on master help sign in
Write a bunch about breaking module cycles
commit44e663fd5710da07360c8a8add3924bf811ab123 authorEvan Czaplicki <evancz@users.noreply.github.com> dateTue 25 Jul 2017, 12:42 (9 years ago) parentbc3983f
1 file changed+184
hints/module-cycles.md+184
hints/module-cycles.mdnew file+184
@@ -0,0 +1,184 @@
1+
2+# Module Cycles
3+
4+In practice, the simplest example of a module cycle will be two modules with types like this:
5+
6+```elm
7+module Comment exposing (..)
8+
9+import User
10+
11+type alias Comment =
12+ { comment : String
13+ , author : User.User
14+ }
15+```
16+
17+```elm
18+module User exposing (..)
19+
20+import Comment
21+
22+type alias User =
23+ { name : String
24+ , comments : List Comment.Comment
25+ }
26+```
27+
28+Notice that to compile `Comment` we need to `import User`. And notice that to compile `User` we need to `import Comment`. We need both to compile either!
29+
30+Now this is *possible* if the compiler figures out any module cycles and puts them all in one big file to compile them together. That seems fine in our small example, but imagine we have a cycle of 20 modules. If you change *one* of them, you must now recompile *all* of them. In a large code base, this causes extremely long compile times. It is also very hard to disentangle them in practice, so you just end up with slow builds. That is your life now.
31+
32+The thing is that you can always write the code *without* cycles by shuffling declarations around, and the resulting code is ofter much clearer.
33+
34+
35+# Breaking Cycles
36+
37+There are quite a few ways to break our `Comment` and `User` cycle from above. Let’s go through a couple strategies!
38+
39+
40+## Combine the Modules
41+
42+One approach is to just combine the two modules. If we check out the resulting code, we have actually revealed a problem in how we are representing our data:
43+
44+```elm
45+module BadCombination1 exposing (..)
46+
47+type alias Comment =
48+ { comment : String
49+ , author : User
50+ }
51+
52+type alias User =
53+ { name : String
54+ , comments : List Comment
55+ }
56+```
57+
58+Notice that the `Comment` type alias is defined in terms of the `User` type alias and vice versa. Having recursive type aliases like this does now work! That problem is described in depth [here](recursive-alias.md), but the quick takeaway is that one `type alias` needs to become a `type` to break the recursion. So let’s try again:
59+
60+```elm
61+module BadCombination2 exposing (..)
62+
63+type alias Comment =
64+ { comment : String
65+ , author : User
66+ }
67+
68+type alias User =
69+ { name : String
70+ , comments : AllUserComments
71+ }
72+
73+type AllUserComments = AllUserComments (List Comment)
74+```
75+
76+Okay, now we have broken the recursion, but we need to ask ourselves, how are we going to actually instantiate these `Comment` and `User` types that we have described. A `Comment` will always have an author, and that `User` will always refer back to the `Comment`. So we seem to want cyclic data here. If we were in JavaScript we might instantiate all the comments in one pass, and then go back through and mutate the users to point to all the relevant comments. In other words, we need *mutation* to create this cyclic data!
77+
78+All values are immutable in Elm, so we need to use a more functional strategy. One common approach is to use unique identifiers. Instead of referring directly to “the user object” we can refer to a user ID:
79+
80+```elm
81+module GoodCombination exposing (..)
82+
83+import Dict
84+
85+type alias Comment =
86+ { comment : String
87+ , author : UserId
88+ }
89+
90+type alias UserId = String
91+
92+type alias AllComments =
93+ Dict.Dict UserId (List Comment)
94+```
95+
96+Now in this world, we do not even have cycles in our types anymore! That means we can actually break these out into separate modules again:
97+
98+```elm
99+module Comment exposing (..)
100+
101+import Dict
102+import User
103+
104+type alias Comment =
105+ { comment : String
106+ , author : User.Id
107+ }
108+
109+type alias AllComments =
110+ Dict.Dict User.Id (List Comment)
111+```
112+
113+```elm
114+module User exposing (..)
115+
116+type alias Id = String
117+```
118+
119+So now we are back to the two modules we wanted, but we have data structures that are going to work much better in a functional language like Elm! **This is the common approach, and it is what you hope will happen!**
120+
121+
122+## Make a New Module
123+
124+Now say there are actually a ton of functions and values in the `Comment` and `User` modules. Combining them into one does not seem like a good strategy. Instead you can create a *third* module that just has the shared types and functions. Let’s pretend we call that third module `GoodCombination`. So rather than having `Comment` and `User` depend on each other, they now both depend on `GoodCombination`. We broke our cycle!
125+
126+**This strategy is less common.** You generally want to keep the core `type` of a module with all the functions that act upon it directly, so separating a `type` from everything else is a bad sign. So maybe there is a `User` module that contains a bunch of helper functions, but you *use* all those helper functions in a bunch of other modules that interact with users in various ways. In that scenario, it is still more sophisticated than “just throw the types in a module together” and hope it turns out alright.
127+
128+
129+## Use Type Variables
130+
131+Another way to avoid module cycles is to be more generic in how you represent your data:
132+
133+```elm
134+module Comment exposing (..)
135+
136+type alias Comment author =
137+ { comment : String
138+ , author : author
139+ }
140+```
141+
142+```elm
143+module User exposing (..)
144+
145+type alias User comment =
146+ { name : String
147+ , comments : List comment
148+ }
149+```
150+
151+Notice that `Comment` and `User` no longer need to import each other! Instead, whenever we use these modules, we need to fill in the type variable. So we may import both `Comment` and `User` and try to combine them into a `Comment (User (Comment (User ...)))`. Gah, we ran into the recursive type alias thing again!
152+
153+So this strategy fails pretty badly with our particular example. The code is more complicated and it still does not work! So **this strategy is rarely useful**, but when it works, it can simplify things quite a lot.
154+
155+
156+## Hiding Implementation Details in Packages
157+
158+This gets a little bit trickier when you are creating a package like `elm-lang/parser` which is built around the `Parser` type.
159+
160+That package has a couple exposed modules: `Parser`, `Parser.LanguageKit`, and `Parser.LowLevel`. All of these modules want access to the internal details of the `Parser` type, but we do not want to ever expose those internal details to the *users* of this package. So where should the `Parser` type live?!
161+
162+Usually you know which module should expose the type for the best public API. In this case, it makes sense for it to live in the `Parser` module. The way to manage this is to create a `Parser.Internal` module with a definition like:
163+
164+```elm
165+module Parser.Internal exposing (..)
166+
167+type Parser a =
168+ Parser ...
169+```
170+
171+Now we can `import Parser.Internal` and use it in any of the modules in our package. The trick is that we never expose the `Parser.Internal` module to the *users* of our package. We can see what is inside, but they cannot! Then in the `Parser` module we can say:
172+
173+```elm
174+module Parser exposing (..)
175+
176+import Parser.Internal as Internal
177+
178+type alias Parser a =
179+ Internal.Parser a
180+```
181+
182+So now folks see a `Parser` type exposed by the `Parser` module, and it is the one that is used throughout all the modules in the package. Do not screw up your data representation to avoid this trick! I think we can improve how this appears in documentation, but overall this is the best way to go.
183+
184+Now again, this strategy is particularly useful in packages. It is not as worthwhile in application code.