You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In this chapter we've looked at using types to represent constraints,
5
+
which allows the compiler to help us ensure these constraints are met throughout our program.
6
+
We call this strategy "types as constraints".
7
+
We constrasted this strategy to the better known view of types that focuses on representation.
8
+
Finally, we saw opaque types as a lightweight tool that decouples types from their representation,
9
+
allowing us to define a type that uses the same representation as some other type.
10
+
4
11
The view of types as constraints is perhaps best presented in Alexis King's blog post #href("https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/")[Parse, don't validate].
12
+
#narrative-cite(<morris73:types>) is a very early paper (typewritten in two column justified text, a truly virtuoso performance on the type writer!) that also presents the intensional view of types. I feel it ends a bit abruptly, but has the seed of many ideas that will only be fully developed much later. You can see the suggestion of opaque types as discussed in this chapter, and also module systems and existential types.
5
13
6
14
From a programming language perspective, #narrative-cite(<pierce02:tapl>) is the standard reference on type systems.
7
15
They define a type system as "a tractable syntactic method for proving the absence of certain program behaviours by classifying phrases by the kinds of values they compute".
8
16
The introduction provides a very nice overview of the role of type systems in programming languages, as well as pointers to the broader study of type systems in mathematics and philosophy.
9
17
10
-
#narrative-cite(<morris73:types>) is a very early paper (typewritten in two column justified text, a truly virtuoso performance on the type writer!) that presents the intensional view of types. I feel it ends a bit abruptly, but has the seed of many ideas that will only be fully developed much later. You can see the suggestion of opaque types as discussed in this chapter, and also module systems and existential types.
11
-
12
-
Having said that types are not sets, it feels only fair to mention there are type systems that treat types as sets. #narrative-cite(<castagna23:elixir>) describes one such system.
18
+
Having said that types are not sets, it feels only fair to mention there are type systems that do treat types as sets. #narrative-cite(<castagna23:elixir>) describes one such system.
13
19
These type systems emphasize the extensional view, and have a very different feel to conventional type systems.
14
20
15
21
I'm very far from an expert in mathematical type theory. As such, I found #narrative-cite(<klev19:comparison>) useful to relate type theory to something I better understand, set theory.
Copy file name to clipboardExpand all lines: src/pages/types/constraints.typ
+22-22Lines changed: 22 additions & 22 deletions
Original file line number
Diff line number
Diff line change
@@ -3,24 +3,22 @@
3
3
4
4
Most applications work by progressively adding structure to inputs.
5
5
We might receive data from, say, the network or a database.
6
-
We perform some checks on that data, and remove instances that are invalid.
6
+
We perform some checks on that data and remove instances that are invalid.
7
7
We then do some more work, which entails further checks, and so on.
8
8
9
9
For example, imagine we're implementing a sign up flow.
10
10
We start by asking for a user name and email address.
11
11
Basic checks could be requiring names that are not empty, and email addresses that contain an `@`.
12
12
We won't even let the user submit the form if these checks fail.
13
-
If this looks ok, we'll move on to further checks.
14
-
For example, we might validate email addresses
15
-
by sending them a verification email.
13
+
Once the form is submitted we'll move on to further checks.
14
+
For example, we might validate email addresses by sending them a verification email.
16
15
17
16
How should we represent these multiple levels of validation?
18
17
For example, how do we distinguish a string representing a name from one that is an email address?
19
18
How about an unverified email from a verified one?
20
-
If you've worked on enough projects you've probably seen many approaches to this.
21
-
Many code bases use names in an ad hoc do this.
19
+
The most common approach that I've seen, across many code bases, is to use ad hoc naming conventions.
22
20
For example, we might use the name `email` and `verifiedEmail` to distinguish the different kinds of email addresses
23
-
in method parameters and data structure members,
21
+
in method parameters and data structure,
24
22
while still representing both as strings.
25
23
#footnote[
26
24
#href("https://en.wikipedia.org/wiki/Hungarian_notation")[Hungarian notation] is a more formal approach to
@@ -30,13 +28,14 @@ while still representing both as strings.
30
28
]
31
29
32
30
Types provide a compelling alternative to naming schemes.
33
-
It provides all the advantages of naming schemes,
34
-
while also representing this information in a form the compiler can check for us.
31
+
They enforce consistency of nomenclature,
32
+
while also representing this information in a form the compiler can check.
35
33
For example, if we have `EmailAddress` and `VerifiedEmailAddress` types,
36
-
the compiler will tell us if we try to use an `EmailAddress` where a `VerifiedEmailAddress` is required,
34
+
not only do we have standard names,
35
+
but the compiler will tell us if we try to use an `EmailAddress` where a `VerifiedEmailAddress` is required,
37
36
or a `String` where an `EmailAddress` is required.
38
37
Furthermore, when we see an `EmailAddress` we know it's already been through some validation,
39
-
so we don't need to repeat this validation (or worse, forget to do it.)
38
+
so we don't needlessly repeat validation, or worse, forget to do it.
40
39
This brings us to two principles:
41
40
42
41
1. Types should represent what we know about values, or in other words the invariants or constraints on values. A `String` could be any sequence of characters. A `VerifiedEmailAddress` is also a sequence of characters, but it's one that represents an email address that we have verified is active.
@@ -14,7 +14,7 @@ opaque type EmailAddress = String
14
14
15
15
This is enough to define the type `EmailAddress` as represented by a `String`.
16
16
However, it's a useless definition as it lacks any way to construct an `EmailAddress`.
17
-
To properly understand how we can define a constructor, we need to understand that opaque types divide our code base into two distinct parts: that where our type is transparent, where we know the underlying representation, and the remainder where it is opaque.
17
+
To properly understand how we can define a constructor, we need to understand that opaque types divide our code base into two distinct parts: where our type is transparent, which is where we know the underlying representation, and the remainder where it is opaque.
18
18
The rule is pretty simple: an opaque type is transparent within the scope in which it is defined, so within an enclosing object or class.
19
19
If there is no enclosing scope, as in the example above,
20
20
it is transparent only within the file in which it is defined.
@@ -94,13 +94,13 @@ yet it is a different type.
94
94
Alternatively, we can view it as a semantic gain.
95
95
An `EmailAddress`_is_ a sequence of characters,
96
96
the same as a `String`,
97
-
but it has additional properties.
97
+
but it has additional constraints.
98
98
In this case we verify it contains exactly one `@` character,
99
-
and our email addresses are case insensitive.
99
+
and ensure it is case insensitive.
100
100
101
101
We've seen how to define opaque types and their constructors.
102
102
What about other methods?
103
-
For example, for an `EmailAddress` we might want to get the username and domain.
103
+
For example, for an `EmailAddress` we might want methods to get the username and domain.
104
104
We can use extension methods to do this.
105
105
As with the constructor, we just need to define these extension methods in a place where the type is transparent.
106
106
@@ -138,17 +138,108 @@ email.username
138
138
email.domain
139
139
```
140
140
141
+
There are two other features of opaque types that we should mention:
142
+
143
+
1. they can have type parameters; and
144
+
2. they can have type bounds.
145
+
146
+
Let's see an example of these two features used together.
147
+
Earlier we saw an example of using an `Option` to represent two different types of database columns:
148
+
nullable columns, where `None` mean to set the column to null, and non-nullable, where `None` means to keep the existing value.
149
+
We can define these as opaque types with a type parameter.
150
+
151
+
```scala mdoc:silent
152
+
// null is a reserved word in Scala, so we use the name nil
because the methods on `Option` that we call have return type `Option`.
229
+
We can easily convert back to `Nilable` or `Default` as required by using the `fromOption` constructor.
230
+
231
+
141
232
=== Best Practices
142
233
143
234
We've seen all the important technical details for opaque types,
144
-
so let's now discuss some of the best practices---the craft---of using them.
235
+
so let's now discuss some of the best practicesof using them.
145
236
146
-
The first point I want to address is the constructor. "Types as constraints" is the strategy we're covering in this chapter.
237
+
The first point I want to address is illustrated by the constructor for `EmailAddress`.
147
238
There is a constraint on the `String` input to the constructor: it must contain an `@` character.
148
-
We should represent this as a type!
149
-
We could create another opaque type, called something like `StringWithAnAtCharacter`, but this approaches leads to infinite regress.
150
-
We cannot push constraints forward indefinitely.
151
-
At some point we have to work with primitive types and return a result that indicates the possibility of error.
239
+
This is a constraint and we should represent this as a type!
240
+
We could create another opaque type, called something like `StringWithAnAtCharacter`, but this approach leads to infinite regress.
241
+
We cannot push constraints upstream indefinitely.
242
+
At some point we have to return a result that indicates the possibility of error.
152
243
So our constructor would be better if it returned, say, an `Option` or `Either` to indicate that construction can fail.
153
244
154
245
There are cases where we know the constructor cannot fail,
@@ -157,7 +248,7 @@ For example, if we're loading email addresses from a list that is known to be go
157
248
For this reason I recommend including a constructor that doesn't do any validation.
158
249
I usually call this method `unsafeApply`, to indicate to the reader that certain checks are not being done.
159
250
These changes are shown below.
160
-
For simplicity I've used `Option` as the result type.
251
+
For simplicity I've used `Option` as the result type to indicate the possibility of failure.
161
252
162
253
```scala mdoc:reset:silent
163
254
type EmailAddress = String
@@ -173,9 +264,9 @@ object EmailAddress {
173
264
}
174
265
```
175
266
176
-
At some point we'll almost certainly need to convert from our opaque type back to its underlying type.
267
+
We'll almost certainly need to convert from our opaque type back to its underlying type at some point in our code.
177
268
I've seen a few conventions for naming such a method; `value` and `get` are popular.
178
-
However, I prefer a more descriptive `toType`, replacing `Type` with the concreate type name,
269
+
However, I prefer a more descriptive `toType`, replacing `Type` with the concrete type name,
179
270
as this extends to conversions to other types.
180
271
For `EmailAddress` this means an extension method `toString`, as shown below.
181
272
Notice that the method simply returns the `address` value,
@@ -197,12 +288,11 @@ For example, a (two-dimensional) point requires two coordinates, so there is no
197
288
We could use an `Array[Double]` or `Tuple2[Double, Double]`,
198
289
but it's simpler to just define a class in the usual way.
199
290
].
200
-
We also cannot define opaque types with type parameters.
201
291
In these cases we're probably looking for an algebraic data type,
202
292
which is discussed in @sec:adt.
203
293
204
294
The second case is when we need to reimplement one of the methods, most commonly `toString`, that opaque types cannot override.
205
295
For example,
206
-
if we're creating types that represent personal information such as addresses and passwords, we might want to ensure they cannot be accidentally exposed in logs.
296
+
we might want to ensure that types representing personal information, such as addresses and passwords, cannot be accidentally exposed in logs.
207
297
Overriding `toString` helps ensure this, but we cannot do this for opaque types.
0 commit comments