Commit | Line | Data |
---|---|---|
7f918cf1 CE |
1 | TipsForWritingConciseSML |
2 | ======================== | |
3 | ||
4 | SML is a rich enough language that there are often several ways to | |
5 | express things. This page contains miscellaneous tips (ideas not | |
6 | rules) for writing concise SML. The metric that we are interested in | |
7 | here is the number of tokens or words (rather than the number of | |
8 | lines, for example). | |
9 | ||
10 | == Datatypes in Signatures == | |
11 | ||
12 | A seemingly frequent source of repetition in SML is that of datatype | |
13 | definitions in signatures and structures. Actually, it isn't | |
14 | repetition at all. A datatype specification in a signature, such as, | |
15 | ||
16 | [source,sml] | |
17 | ---- | |
18 | signature EXP = sig | |
19 | datatype exp = Fn of id * exp | App of exp * exp | Var of id | |
20 | end | |
21 | ---- | |
22 | ||
23 | is just a specification of a datatype that may be matched by multiple | |
24 | (albeit identical) datatype declarations. For example, in | |
25 | ||
26 | [source,sml] | |
27 | ---- | |
28 | structure AnExp : EXP = struct | |
29 | datatype exp = Fn of id * exp | App of exp * exp | Var of id | |
30 | end | |
31 | ||
32 | structure AnotherExp : EXP = struct | |
33 | datatype exp = Fn of id * exp | App of exp * exp | Var of id | |
34 | end | |
35 | ---- | |
36 | ||
37 | the types `AnExp.exp` and `AnotherExp.exp` are two distinct types. If | |
38 | such <:GenerativeDatatype:generativity> isn't desired or needed, you | |
39 | can avoid the repetition: | |
40 | ||
41 | [source,sml] | |
42 | ---- | |
43 | structure Exp = struct | |
44 | datatype exp = Fn of id * exp | App of exp * exp | Var of id | |
45 | end | |
46 | ||
47 | signature EXP = sig | |
48 | datatype exp = datatype Exp.exp | |
49 | end | |
50 | ||
51 | structure Exp : EXP = struct | |
52 | open Exp | |
53 | end | |
54 | ---- | |
55 | ||
56 | Keep in mind that this isn't semantically equivalent to the original. | |
57 | ||
58 | ||
59 | == Clausal Function Definitions == | |
60 | ||
61 | The syntax of clausal function definitions is rather repetitive. For | |
62 | example, | |
63 | ||
64 | [source,sml] | |
65 | ---- | |
66 | fun isSome NONE = false | |
67 | | isSome (SOME _) = true | |
68 | ---- | |
69 | ||
70 | is more verbose than | |
71 | ||
72 | [source,sml] | |
73 | ---- | |
74 | val isSome = | |
75 | fn NONE => false | |
76 | | SOME _ => true | |
77 | ---- | |
78 | ||
79 | For recursive functions the break-even point is one clause higher. For example, | |
80 | ||
81 | [source,sml] | |
82 | ---- | |
83 | fun fib 0 = 0 | |
84 | | fib 1 = 1 | |
85 | | fib n = fib (n-1) + fib (n-2) | |
86 | ---- | |
87 | ||
88 | isn't less verbose than | |
89 | ||
90 | [source,sml] | |
91 | ---- | |
92 | val rec fib = | |
93 | fn 0 => 0 | |
94 | | 1 => 1 | |
95 | | n => fib (n-1) + fib (n-2) | |
96 | ---- | |
97 | ||
98 | It is quite often the case that a curried function primarily examines | |
99 | just one of its arguments. Such functions can be written particularly | |
100 | concisely by making the examined argument last. For example, instead | |
101 | of | |
102 | ||
103 | [source,sml] | |
104 | ---- | |
105 | fun eval (Fn (v, b)) env => ... | |
106 | | eval (App (f, a) env => ... | |
107 | | eval (Var v) env => ... | |
108 | ---- | |
109 | ||
110 | consider writing | |
111 | ||
112 | [source,sml] | |
113 | ---- | |
114 | fun eval env = | |
115 | fn Fn (v, b) => ... | |
116 | | App (f, a) => ... | |
117 | | Var v => ... | |
118 | ---- | |
119 | ||
120 | ||
121 | == Parentheses == | |
122 | ||
123 | It is a good idea to avoid using lots of irritating superfluous | |
124 | parentheses. An important rule to know is that prefix function | |
125 | application in SML has higher precedence than any infix operator. For | |
126 | example, the outer parentheses in | |
127 | ||
128 | [source,sml] | |
129 | ---- | |
130 | (square (5 + 1)) + (square (5 * 2)) | |
131 | ---- | |
132 | ||
133 | are superfluous. | |
134 | ||
135 | People trained in other languages often use superfluous parentheses in | |
136 | a number of places. In particular, the parentheses in the following | |
137 | examples are practically always superfluous and are best avoided: | |
138 | ||
139 | [source,sml] | |
140 | ---- | |
141 | if (condition) then ... else ... | |
142 | while (condition) do ... | |
143 | ---- | |
144 | ||
145 | The same basically applies to case expressions: | |
146 | ||
147 | [source,sml] | |
148 | ---- | |
149 | case (expression) of ... | |
150 | ---- | |
151 | ||
152 | It is not uncommon to match a tuple of two or more values: | |
153 | ||
154 | [source,sml] | |
155 | ---- | |
156 | case (a, b) of | |
157 | (A1, B1) => ... | |
158 | | (A2, B2) => ... | |
159 | ---- | |
160 | ||
161 | Such case expressions can be written more concisely with an | |
162 | <:ProductType:infix product constructor>: | |
163 | ||
164 | [source,sml] | |
165 | ---- | |
166 | case a & b of | |
167 | A1 & B1 => ... | |
168 | | A2 & B2 => ... | |
169 | ---- | |
170 | ||
171 | ||
172 | == Conditionals == | |
173 | ||
174 | Repeated sequences of conditionals such as | |
175 | ||
176 | [source,sml] | |
177 | ---- | |
178 | if x < y then ... | |
179 | else if x = y then ... | |
180 | else ... | |
181 | ---- | |
182 | ||
183 | can often be written more concisely as case expressions such as | |
184 | ||
185 | [source,sml] | |
186 | ---- | |
187 | case Int.compare (x, y) of | |
188 | LESS => ... | |
189 | | EQUAL => ... | |
190 | | GREATER => ... | |
191 | ---- | |
192 | ||
193 | For a custom comparison, you would then define an appropriate datatype | |
194 | and a reification function. An alternative to using datatypes is to | |
195 | use dispatch functions | |
196 | ||
197 | [source,sml] | |
198 | ---- | |
199 | comparing (x, y) | |
200 | {lt = fn () => ..., | |
201 | eq = fn () => ..., | |
202 | gt = fn () => ...} | |
203 | ---- | |
204 | ||
205 | where | |
206 | ||
207 | [source,sml] | |
208 | ---- | |
209 | fun comparing (x, y) {lt, eq, gt} = | |
210 | (case Int.compare (x, y) of | |
211 | LESS => lt | |
212 | | EQUAL => eq | |
213 | | GREATER => gt) () | |
214 | ---- | |
215 | ||
216 | An advantage is that no datatype definition is needed. A disadvantage | |
217 | is that you can't combine multiple dispatch results easily. | |
218 | ||
219 | ||
220 | == Command-Query Fusion == | |
221 | ||
222 | Many are familiar with the | |
223 | http://en.wikipedia.org/wiki/Command-Query_Separation[Command-Query | |
224 | Separation Principle]. Adhering to the principle, a signature for an | |
225 | imperative stack might contain specifications | |
226 | ||
227 | [source,sml] | |
228 | ---- | |
229 | val isEmpty : 'a t -> bool | |
230 | val pop : 'a t -> 'a | |
231 | ---- | |
232 | ||
233 | and use of a stack would look like | |
234 | ||
235 | [source,sml] | |
236 | ---- | |
237 | if isEmpty stack | |
238 | then ... pop stack ... | |
239 | else ... | |
240 | ---- | |
241 | ||
242 | or, when the element needs to be named, | |
243 | ||
244 | [source,sml] | |
245 | ---- | |
246 | if isEmpty stack | |
247 | then let val elem = pop stack in ... end | |
248 | else ... | |
249 | ---- | |
250 | ||
251 | For efficiency, correctness, and conciseness, it is often better to | |
252 | combine the query and command and return the result as an option: | |
253 | ||
254 | [source,sml] | |
255 | ---- | |
256 | val pop : 'a t -> 'a option | |
257 | ---- | |
258 | ||
259 | A use of a stack would then look like this: | |
260 | ||
261 | [source,sml] | |
262 | ---- | |
263 | case pop stack of | |
264 | NONE => ... | |
265 | | SOME elem => ... | |
266 | ---- |