Skip to content

Commit ffe2358

Browse files
Improve some wording in the functors tutorial (#2023)
* Reorder a paragraph in functors tutorial to improve content flow * Grammar: add the word "by" in functors tutorial * Clarify note on `with type` constraint in functors tutorial * Simplify note on `with type` constraint in functors tutorial Co-authored-by: Cuihtlauac Alvarado <[email protected]> * Grammar: add the word "of" in functors tutorial * Add section on empty variants in basic data types tutorial * Add missing definition to `Binary` functor in functors tutorial * Grammar: use "facilitate" over "allows" in functors tutorial * Add section on naming and scoping to functors tutorial * Reword section header in functors tutorial Co-authored-by: Cuihtlauac Alvarado <[email protected]> * Use more self-explanatory constructor in functors tutorial Co-authored-by: Cuihtlauac Alvarado <[email protected]> * Improve compiler error explanation in functors tutorial Co-authored-by: Cuihtlauac Alvarado <[email protected]> * Remove unnecessary word in functors tutorial Co-authored-by: Cuihtlauac Alvarado <[email protected]> * Remove small digression from functors tutorial * Simplify sentence in functors tutorial Co-authored-by: Cuihtlauac Alvarado <[email protected]> * Reword explanation on scoping in functors tutorial * Remove extra space and add comma after "i.e." in functors tutorial * Revert "Add section on empty variants in basic data types tutorial" This reverts commit 03758f0. * Remove empty variants as placeholders in `Binary` functor example * Apply suggestions from code review * Apply suggestions from code review * Update data/tutorials/language/1ms_01_functors.md * Clarify note on `with type` constraint in functors tutorial Co-authored-by: Cuihtlauac Alvarado <[email protected]> * Typo: change "In" to "If" in functors tutorial * Apply suggestions from code review * Update data/tutorials/language/1ms_01_functors.md --------- Co-authored-by: Cuihtlauac Alvarado <[email protected]>
1 parent 4d6bf39 commit ffe2358

File tree

1 file changed

+54
-10
lines changed

1 file changed

+54
-10
lines changed

data/tutorials/language/1ms_01_functors.md

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ Here is how this reads (starting from the bottom, then going up):
6262

6363
**Note**: Most set operations need to compare elements to check if they are the same. To allow using a user-defined comparison algorithm, the `Set.Make` functor takes a module the specifies both the element type `t` and the `compare` function. Passing the comparison function as a higher-order parameter, as done in `Array.sort`, for example, would add a lot of boilerplate code. Providing set operations as a functor allows specifying the comparison function only once.
6464

65-
Here is an example how to use `Set.Make`:
65+
Here is an example of how to use `Set.Make`:
6666

6767
**`funkt.ml`**
6868

@@ -79,10 +79,6 @@ This defines a module `Funkt.StringSet`. What `Set.Make` needs are:
7979
- Type `t`, here `string`
8080
- Function allowing to compare two values of type `t`, here `String.compare`
8181

82-
However, since the module `String` defines
83-
- Type name `t`, which is an alias for `string`
84-
- Function `compare` of type `t -> t -> bool` compares two strings
85-
8682
This can be simplified using an _anonymous module_ expression:
8783
```ocaml
8884
module StringSet = Set.Make(struct
@@ -93,6 +89,10 @@ end)
9389

9490
The module expression `struct ... end` is inlined in the `Set.Make` call.
9591

92+
However, since the module `String` already defines
93+
- Type name `t`, which is an alias for `string`
94+
- Function `compare` of type `t -> t -> bool` compares two strings
95+
9696
This can be simplified even further into this:
9797
```ocaml
9898
module StringSet = Set.Make(String)
@@ -156,14 +156,14 @@ let _ =
156156

157157
This allows the user to seemingly extend the module `String` with a submodule `Set`. Check the behaviour using `opam exec -- dune exec funkt < dune`.
158158

159-
## Functors Allows Parametrising Modules
159+
## Parametrising Modules with Functors
160160

161161
### Functors From the Standard Library
162162

163163
A functor is almost a module, except it needs to be applied to a module. This turns it into a module. In that sense, a functor allows module parametrisation.
164164

165165
That's the case for the sets, maps, and hash tables provided by the standard library. It works like a contract between the functor and the developer.
166-
* If you provide a module that implements what is expected, as described the parameter interface
166+
* If you provide a module that implements what is expected, as described by the parameter interface
167167
* The functor returns a module that implements what is promised, as described by the result interface
168168

169169
Here is the module's signature that the functors `Set.Make` and `Map.Make` expect:
@@ -241,9 +241,10 @@ module type S = sig
241241
end
242242
243243
module Binary(Elt: OrderedType) : S = struct
244-
type elt = | (* Replace by your own *)
245-
type t = | (* Replace by your own *)
244+
type elt (* Add your own type definition *)
245+
type t (* Add your own type definition *)
246246
(* Add private functions here *)
247+
let empty = failwith "Not yet implemented"
247248
let is_empty h = failwith "Not yet implemented"
248249
let insert h e = failwith "Not yet implemented"
249250
let merge h1 h2 = failwith "Not yet implemented"
@@ -370,7 +371,50 @@ let _ =
370371

371372
Check the program's behaviour using `opam exec -- dune exec funkt < dune`.
372373

373-
**Note**: The functor `IterPrint.Make` returns a module that exposes the type from the injected dependency (here first `List.t` then `Array.t`). That's why a `with type` constraint is needed. When parametrising other something not exposed by the module (and _implementation detail_), the `with type` constraint is not needed.
374+
**Note**: Modules received and returned by `IterPrint.Make` both have a type `t`. The `with type ... := ...` constraint exposes that the two types `t` are the same. This makes functions from the injected dependency and result module use the exact same type. When the parameter's contained type is not exposed by the result module (i.e., when it is an _implementation detail_), the `with type` constraint is not necessary.
375+
376+
### Naming and Scoping
377+
378+
The `with type` constraint unifies types within a functor's parameter and result modules. We've used that in the previous section. This section addresses the naming and scoping mechanics of this constraint.
379+
380+
Naively, we might have defined `Iter.Make` as follows:
381+
382+
```ocaml
383+
module Make(Dep: Iterable) : S = struct
384+
type 'a t = 'a Dep.t
385+
let f = Dep.iter (fun s -> Out_channel.output_string stdout (s ^ "\n"))
386+
end
387+
```
388+
389+
If the function `f` isn't used, the project compiles without error.
390+
391+
However, since `Make` is invoked to create module `IterPrint` in `funkt.ml`, the project fails to compile with the following error message:
392+
393+
```shell
394+
5 | ..stdin
395+
6 | |> In_channel.input_lines
396+
7 | |> List.concat_map Str.(split (regexp "[ \t.,;:()]+"))
397+
8 | |> StringSet.of_list
398+
9 | |> StringSet.elements
399+
Error: This expression has type string list
400+
but an expression was expected of type string IterPrint.t
401+
```
402+
403+
Outside the functor, it is not known that `type 'a t` is set to `Dep.t`. In `funkt.ml`, `IterPrint.t` appears as an abstract type exposed by the result of `Make`. This is why the `with type` constraint is needed. It propagates the knowledge that `IterPrint.t` is the same type as `Dep.t` (`List.t` in this case).
404+
405+
The type constrained using `with type` isn't shadowed by definitions within the functor body. In the example, the `Make` functor can be redefined as follows:
406+
407+
```ocaml
408+
module Make(Dep: Iterable) : S with type 'a t := 'a Dep.t = struct
409+
type 'a t = LocalType
410+
let g LocalType = "LocalType"
411+
let f = Dep.iter(fun s ->
412+
Out_channel.output_string stdout (g LocalType ^ "\n");
413+
Out_channel.output_string stdout (s ^ "\n"))
414+
end
415+
```
416+
417+
In the example above, `t` from `with type` takes precedence over the local `t`, which only has a local scope. Don't shadow names too often because it makes the code harder to understand.
374418

375419
## Write a Functor to Extend Modules
376420

0 commit comments

Comments
 (0)