Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
0058608
Reorder a paragraph in functors tutorial to improve content flow
neuroevolutus Feb 3, 2024
b9c95a5
Grammar: add the word "by" in functors tutorial
neuroevolutus Feb 3, 2024
e3ee4cd
Clarify note on `with type` constraint in functors tutorial
neuroevolutus Feb 3, 2024
9a8498a
Simplify note on `with type` constraint in functors tutorial
neuroevolutus Feb 5, 2024
2a3ea11
Grammar: add the word "of" in functors tutorial
neuroevolutus Feb 5, 2024
03758f0
Add section on empty variants in basic data types tutorial
neuroevolutus Feb 5, 2024
722bd1b
Add missing definition to `Binary` functor in functors tutorial
neuroevolutus Feb 5, 2024
57f5191
Grammar: use "facilitate" over "allows" in functors tutorial
neuroevolutus Feb 6, 2024
f558f95
Add section on naming and scoping to functors tutorial
neuroevolutus Feb 6, 2024
cd87f3e
Reword section header in functors tutorial
neuroevolutus Feb 6, 2024
b443443
Use more self-explanatory constructor in functors tutorial
neuroevolutus Feb 6, 2024
7438292
Improve compiler error explanation in functors tutorial
neuroevolutus Feb 6, 2024
c6eeb26
Remove unnecessary word in functors tutorial
neuroevolutus Feb 6, 2024
5401b2d
Remove small digression from functors tutorial
neuroevolutus Feb 6, 2024
b0e29ab
Simplify sentence in functors tutorial
neuroevolutus Feb 6, 2024
22f000d
Reword explanation on scoping in functors tutorial
neuroevolutus Feb 6, 2024
1deafff
Remove extra space and add comma after "i.e." in functors tutorial
neuroevolutus Feb 6, 2024
4023527
Revert "Add section on empty variants in basic data types tutorial"
neuroevolutus Feb 6, 2024
6afcdcd
Remove empty variants as placeholders in `Binary` functor example
neuroevolutus Feb 6, 2024
98a2383
Apply suggestions from code review
cuihtlauac Feb 9, 2024
ac98365
Apply suggestions from code review
cuihtlauac Feb 9, 2024
fb1188d
Update data/tutorials/language/1ms_01_functors.md
cuihtlauac Feb 9, 2024
2fcf239
Clarify note on `with type` constraint in functors tutorial
neuroevolutus Feb 9, 2024
3a8394e
Typo: change "In" to "If" in functors tutorial
neuroevolutus Feb 9, 2024
999bb22
Apply suggestions from code review
cuihtlauac Feb 12, 2024
c1a6c07
Update data/tutorials/language/1ms_01_functors.md
cuihtlauac Feb 12, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions data/tutorials/language/0it_01_basic_datatypes.md
Original file line number Diff line number Diff line change
Expand Up @@ -482,6 +482,16 @@ Note that:
- `unit` is a variant with a unique constructor, which does not carry data: `()`.
- `bool` is also a variant with two constructors that doesn't carry data: `true` and `false`.

#### Empty Variants

Variants can optionally be defined with absolutely no constructors at all.

```ocaml
type void = |
```

Such types are not ordinarily useful in OCaml programs (as they do not have any constructible values), but they can be useful as temporary placeholders when defining types in a [functor](/docs/functors#writing-your-own-functors).

Copy link
Member

@Octachron Octachron Feb 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not the intended use case for empty types (see https://ocaml.org/manual/emptyvariants.html for an explanation of why they were introduced). An abstract type works better as a placeholder for the use case you are describing. I would thus avoid mentioning empty variant types before the GADT introduction.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. That would make sense. I'll revert the corresponding commit.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've edited the Binary functor example to make elt and t abstract types.

Thanks for the link to the manual! I'll check that out.

Copy link
Member

@Octachron Octachron Feb 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To give a better example of what I meant by "abstract type are better placeholder", if you write

type empty = |
let f ([]: empty list) = ()

the function f is total: the only list of empty values is the empty list. But this is property is only true for empty types. In other words, by using empty types as placeholders one might accidentally capture properties that are only true for empty types.

Copy link
Contributor Author

@neuroevolutus neuroevolutus Feb 6, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. I think I better understand now. As an aside, are there any examples in the wild where using empty types and refutation cases for them come in handy when it comes to GADTs? I think I'm still fuzzy on the motivation for empty types.

#### Constructors With Data

It is possible to wrap data in constructors. The following type has several constructors with data (e.g., `Hash of string`) and some without (e.g., `Head`). It represents the different means to refer to a Git [revision](https://git-scm.com/docs/gitrevisions).
Expand Down
81 changes: 73 additions & 8 deletions data/tutorials/language/1ms_01_functors.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ Here is how this reads (starting from the bottom, then going up):

**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.

Here is an example how to use `Set.Make`:
Here is an example of how to use `Set.Make`:

**`funkt.ml`**

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

However, since the module `String` defines
- Type name `t`, which is an alias for `string`
- Function `compare` of type `t -> t -> bool` compares two strings

This can be simplified using an _anonymous module_ expression:
```ocaml
module StringSet = Set.Make(struct
Expand All @@ -94,6 +90,10 @@ end)

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

However, since the module `String` already defines
- Type name `t`, which is an alias for `string`
- Function `compare` of type `t -> t -> bool` compares two strings

This can be simplified even further into this:
```ocaml
module StringSet = Set.Make(String)
Expand Down Expand Up @@ -157,14 +157,14 @@ let _ =

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

## Functors Allows Parametrising Modules
## Functors Facilitate Parametrising Modules

### Functors From the Standard Library

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.

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.
* If you provide a module that implements what is expected, as described the parameter interface
* If you provide a module that implements what is expected, as described by the parameter interface
* The functor returns a module that implements what is promised, as described by the result interface

Here is the module's signature that the functors `Set.Make` and `Map.Make` expect:
Expand Down Expand Up @@ -245,6 +245,7 @@ module Binary(Elt: OrderedType) : S = struct
type elt = | (* Replace by your own *)
type t = | (* Replace by your own *)
(* Add private functions here *)
let empty = failwith "Not yet implemented"
let is_empty h = failwith "Not yet implemented"
let insert h e = failwith "Not yet implemented"
let merge h1 h2 = failwith "Not yet implemented"
Expand Down Expand Up @@ -371,7 +372,71 @@ let _ =

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

**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.
**Note**: Modules received and returned by `IterPrint.Make` both have a type `t`. The `with type ... :=` constraint is needed to make the two `t` identical. This allows functions from the injected dependency and result module to use the 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 unnecessary.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
**Note**: Modules received and returned by `IterPrint.Make` both have a type `t`. The `with type ... :=` constraint is needed to make the two `t` identical. This allows functions from the injected dependency and result module to use the 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 unnecessary.
**Note**: Modules received and returned by `IterPrint.Make` both have a type `t`. The `with type ... :=` constraint is needed to make the two `t` modules identical. This allows functions from the injected dependency and result module to use the 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 unnecessary.

It reads funny to me to have a plural (two) with just t -- is it accurate to have t modules instead? ...or t types?

Regardless, there needs to be a comma after i.e.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for catching that. I made sure to add the comma in that part.

With regards to the t, I think it may be fine to keep it as is since the t refers to two types rather than modules per se. What are your thoughts, @cuihtlauac?


### Naming and Scoping

In the previous section, we learned how to use the `with type` constraint in order to unify a type `t` contained within both the parameter and result module of a functor. Let's go over a few more details concerning naming and scoping to get a better grasp of the mechanics of this constraint.

When reading the source of `iterPrint.ml`, it may have seemed curious as to why we could not have simply defined `Make` as follows:

```ocaml
module Make(Dep: Iterable) : S = struct
type 'a t = 'a Dep.t
let f = Dep.iter (fun s -> Out_channel.output_string stdout (s ^ "\n"))
end
```

In the absence of client code that utilises the function `f` provided by the output of `Make`, the project would compile without error.

However, since `Make` is invoked to create module `IterPrint` in `funkt.ml`, the project will fail to compile:

```shell
5 | ..stdin
6 | |> In_channel.input_lines
7 | |> List.concat_map Str.(split (regexp "[ \t.,;:()]+"))
8 | |> StringSet.of_list
9 | |> StringSet.elements
Error: This expression has type string list
but an expression was expected of type string IterPrint.t
```

This may seem odd given the fact that the modified `Make` functor successfully compiles on its own. Indeed, if we had used an inappropriate definition such as `type 'a t = MkT`, the functor itself would have failed to compile:

```shell
11 | .................................struct
12 | type 'a t = MkT
13 | let f = Dep.iter (fun s -> Out_channel.output_string stdout (s ^ "\n"))
14 | end
Error: Signature mismatch:
Modules do not match:
sig type 'a t = MkT val f : string Dep.t -> unit end
is not included in
S
Values do not match:
val f : string Dep.t -> unit
is not included in
val f : string t -> unit
The type string Dep.t -> unit is not compatible with the type
string t -> unit
Type string Dep.t is not compatible with type string t
```

The key thing to realise is that, outside the functor, client code is not privy to the fact that `type 'a t` is being set equal to `Dep.t`. In `funkt.ml`, `IterPrint.t` simply appears as an abstract type exposed by the result of `Make`. This is precisely why the `with type` constraint is needed to propagate the knowledge that `IterPrint.t` is the same as the instantiation of `Dep.t` (`List.t` in this case).

Another property of the `with type` constraint is that the type it exposes won't be shadowed by definitions within the functor body. In fact, the `Make` functor could be redefined as follows without preventing the successful compilation of module `Funkt`:

```ocaml
module Make(Dep: Iterable) : S with type 'a t := 'a Dep.t = struct
type 'a t = MkT
let g _ = "MkT"
let f = Dep.iter(fun s ->
Out_channel.output_string stdout (g MkT ^ "\n");
Out_channel.output_string stdout (s ^ "\n"))
end
```

In the example above, the local type `t` does not shadow the type `t` exposed by the `with type` constraint and can be used for the implementation of the functor. However, it is generally better to avoid availing of this behaviour since it may make your code more difficult to understand.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand your point. I must admit I had never fallen into that pitfall and wasn't aware of it.

What do you think about turning the narrative into something like:

t from with type takes over local t which only has a local scope?

Again, the idea is to make things more direct.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree; that sounds good. It sounds clearer to me as a reader.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used "takes precedence over" in the rewording, but let me know if you feel it's clearer with just saying "takes over."


## Write a Functor to Extend Modules

Expand Down