diff --git a/data/tutorials/lg_07_objects.md b/data/tutorials/lg_07_objects.md index b19c5844de..2c1a83b5f0 100644 --- a/data/tutorials/lg_07_objects.md +++ b/data/tutorials/lg_07_objects.md @@ -10,16 +10,16 @@ date: 2021-05-27T21:07:30-00:00 # Objects ## Objects and Classes -OCaml is an object-oriented, imperative, functional programming language -:-) It mixes all these paradigms and lets you use the most appropriate +OCaml is an [object-oriented, imperative, functional programming language](https://ocaml.org/about). +It mixes all these paradigms and lets you use the most appropriate (or most familiar) programming paradigm for the task at hand. In this -chapter I'm going to look at object-oriented programming in OCaml, but -I'm also going to talk about why you might or might not want to write +chapter, we're going to look at object-oriented programming in OCaml, but +we'll also talk about why you might or might not want to write object-oriented programs. -The classic noddy example used in text books to demonstrate +The classic noddy example used in textbooks to demonstrate object-oriented programming is the stack class. This is a pretty -terrible example in many ways, but I'm going to use it here to show the +terrible example in many ways, but let's use it here to show the basics of writing object-oriented OCaml. Here's some basic code to provide a stack of integers. The class is @@ -54,16 +54,16 @@ The basic pattern `class name = object (self) ... end` defines a class called `name`. The class has one instance variable, which is mutable (not constant), -called `the_list`. This is the underlying linked list. We initialize -this (each time a `stack_of_ints` object is created) using a bit of code +called `the_list`. This is the underlying linked list. We initialise +this (each time a `stack_of_ints` object is created) using some code that you may not be familiar with. The expression `( [] : int list )` -means "an empty list, of type `int list`". Recall that the simple empty +means "an empty list, of type `int list`." Recall that the simple empty list `[]` has type `'a list`, meaning a list of any type. However we -want a stack of `int`, not anything else, and so in this case we want to +want a stack of `int`, not anything else, so in this case we want to tell the type inference engine that this list isn't the general "list of -anything" but is in fact the narrower "list of `int`". The syntax -`( expression : type )` means `expression` which is in fact of type -`type`. This *isn't* a general type cast, because you can't use it to +anything" but is in fact the narrower "list of `int`." The syntax +`( expression : type )` means `expression`, which is a of type +`type`. This *isn't* a general type cast because you can't use it to overrule the type inference engine, only to narrow a general type to make it more specific. So you can't write, for example, `( 1 : float )`: @@ -77,17 +77,19 @@ Error: This expression has type int but an expression was expected of type Type safety is preserved. Back to the example ... -This class has four simple methods. `push` pushes an integer onto the -stack. `pop` pops the top integer off the stack and returns it. Notice +This class has four simple methods: +- `push` pushes an integer onto the +stack. +- `pop` pops the top integer off the stack and returns it. Notice the `<-` assignment operator used for updating our mutable instance variable. It's the same `<-` assignment operator which is used for updating mutable fields in records. +- `peek` returns the top of the stack (i.e., head of the list) without +affecting the stack +- `size` returns the number of elements in the +stack (i.e., the length of the list). -`peek` returns the top of the stack (ie. head of the list) without -affecting the stack, while `size` returns the number of elements in the -stack (ie. the length of the list). - -Let's write some code to test stacks of ints. First let's create a new +Let's write some code to test stacks of `int`s. First let's create a new object. We use the familiar `new` operator: ```ocaml @@ -120,7 +122,7 @@ Notice the syntax. `object#method` means call `method` on `object`. This is the same as `object.method` or `object->method` that you will be familiar with in imperative languages. -In the OCaml toplevel we can examine the types of objects and methods in +In the OCaml toplevel, we can examine the types of objects and methods in more detail: ```ocaml @@ -130,7 +132,7 @@ val s : stack_of_ints = - : int -> unit = ``` -`s` is an opaque object. The implementation (ie. the list) is hidden +`s` is an opaque object. The implementation (i.e., the list) is hidden from callers. ### Polymorphic Classes @@ -163,10 +165,10 @@ class ['a] stack : method size : int end ``` -The `class ['a] stack` doesn't really define just one class, but a whole -"class of classes", one for every possible type (ie. an infinitely large -number of classes!) Let's try and use our `'a stack` class. In this -instance we create a stack and push a floating point number onto the +The `class ['a] stack` doesn't really define just one class. It defines a whole +"class of classes," one for every possible type (i.e., an infinitely large +number of classes!). Let's try and use our `'a stack` class. In this +instance, we create a stack and push a floating point number onto the stack. Notice the type of the stack: ```ocaml @@ -206,7 +208,7 @@ stack. Our first attempt is this one: val drain_stack : < pop : 'a; size : int; .. > -> unit = ``` -Notice the type of `drain_stack`. Cleverly - perhaps *too* cleverly - +Notice the type of `drain_stack`. Cleverly (perhaps *too* cleverly), OCaml's type inference engine has worked out that `drain_stack` works on *any* object which has `pop` and `size` methods! So if we defined another, entirely separate class which happened to contain `pop` and @@ -225,11 +227,11 @@ this: val drain_stack : 'a stack -> unit = ``` -### Inheritance, Virtual Classes, Initializers +### Inheritance, Virtual Classes, Initialisers I've noticed programmers in Java tend to overuse inheritance, possibly because it's the only reasonable way of extending code in that language. A much better and more general way to extend code is usually to use -hooks (cf. Apache's module API). Nevertheless in certain narrow areas +hooks (cf. Apache's module API). Nevertheless, in certain narrow areas inheritance can be useful, and the most important of these is in writing GUI widget libraries. @@ -260,7 +262,6 @@ widget. This was my first attempt: name method virtual repaint : unit end;; -Lines 1-6, characters 1-6: Error: Some type variables are unbound in this type: class virtual widget : 'a -> @@ -282,10 +283,10 @@ the type of `name` like this: class virtual widget : string -> object method get_name : string method virtual repaint : unit end ``` -Now there are several new things going on in this code. Firstly the -class contains an **initializer**. This is an argument to the class +Now there are several new things going on in this code. Firstly, the +class contains an **initialiser**. This is an argument to the class (`name`) which you can think of as exactly the equivalent of an argument -to a constructor in, eg., Java: +to a constructor in, e.g., Java: ```java public class Widget @@ -296,7 +297,7 @@ public class Widget } } ``` -In OCaml a constructor constructs the whole class, it's not just a +In OCaml, a constructor constructs the whole class; it's not just a specially named function, so we write the arguments as if they are arguments to the class: @@ -305,14 +306,14 @@ arguments to the class: class foo arg1 arg2 ... = ``` -Secondly the class contains a virtual method, and thus the whole class +Secondly, the class contains a virtual method, so the whole class is marked as virtual. The virtual method is our `repaint` method. We need to tell OCaml it's virtual (`method virtual`), *and* we need to -tell OCaml the type of the method. Because the method doesn't contain +tell OCaml the method's type. Because the method doesn't contain any code, OCaml can't use type inference to automatically work out the -type for you, so you need to tell it the type. In this case the method +type for you, so you need to tell it the type. In this case, the method just returns `unit`. If your class contains any virtual methods (even -just inherited ones) then you need to specify the whole class as virtual +just inherited ones), you need to specify the whole class as virtual by using `class virtual ...`. As in C++ and Java, virtual classes cannot be directly instantiated @@ -320,7 +321,6 @@ using `new`: ```ocaml # let w = new widget "my widget";; -Line 1, characters 9-19: Error: Cannot instantiate the virtual class widget ``` @@ -354,14 +354,14 @@ class virtual container : Notes: 1. The `container` class is marked as virtual. It doesn't contain any - virtual methods, but in this case I just want to prevent people + virtual methods, but in this case, it serves to prevent people creating containers directly. -1. The `container` class has a `name` argument which is passed directly +1. The `container` class has a `name` argument, which is passed directly up when constructing the `widget`. 1. `inherit widget name` means that the `container` inherits from `widget`, and it passes the `name` argument to the constructor for `widget`. -1. My `container` contains a mutable list of widgets and methods to +1. This `container` contains a mutable list of widgets and methods to `add` a widget to this list and `get_widgets` (return the list of widgets). 1. The list of widgets returned by `get_widgets` cannot be modified by @@ -375,25 +375,31 @@ Notes: ``` Would this modify the private internal representation of my `container` -class, by prepending `x` to the list of widgets? No it wouldn't. The -private variable `widgets` would be unaffected by this or any other +class, by prepending `x` to the list of widgets? No it wouldn't. If you run +the above code, you'll see it throws an error: + +```ocaml +Error: Unbound value container +``` + +Yet, the private variable `widgets` would be unaffected by this or any other change attempted by the outside code. This means, for example, that you could change the internal representation to use an array at some later date, and no code outside the class would need to be changed. Last, but not least, we have implemented the previously virtual `repaint` function so that `container#repaint` will repaint all of the -contained widgets. Notice I use `List.iter` to iterate over the list, -and I also use a probably unfamiliar anonymous function expression: +contained widgets. Notice the use of `List.iter` to iterate over the list, +and I also use an anonymous function expression that might be unfamiliar: ```ocaml # (fun w -> w#repaint);; - : < repaint : 'a; .. > -> 'a = ``` -which defines an anonymous function with one argument `w` that just +This defines an anonymous function with one argument `w` that just calls `w#repaint` (the `repaint` method on widget `w`). -In this instance our `button` class is simple (rather unrealistically +In this instance, our `button` class is simple (rather unrealistically simple in fact, but nevermind that): ```ocaml @@ -434,24 +440,24 @@ class button : Notes: -1. This function has an optional argument (see the previous chapter) +1. This function has an optional argument ([see the previous chapter](https://ocaml.org/docs/functors)) which is used to pass in the optional callback function. The callback is called when the button is pressed. 1. The expression `inherit container name as super` names the superclass `super`. I use this in the `repaint` method: `super#repaint`. This expressly calls the superclass method. 1. Pressing the button (calling `button#press` in this rather - simplistic code) sets the state of the button to `Pressed` and calls + simplistic code) sets the button to `Pressed` and calls the callback function, if one was defined. Notice that the - `callback` variable is either `None` or `Some f`, in other words it - has type `(unit -> unit) option`. Reread the previous chapter if you + `callback` variable is either `None` or `Some f`, meaning it + has type `(unit -> unit) option`. Reread the [previous chapter](https://ocaml.org/docs/functors) if you are unsure about this. 1. Notice a strange thing about the `callback` variable. It's defined as an argument to the class, but any method can see and use it. In other words, the variable is supplied when the object is - constructed, but persists over the lifetime of the object. + constructed, and it also persists over the lifetime of the object. 1. The `repaint` method has been implemented. It calls the superclass - (to repaint the container), then repaints the button, displaying the + (to repaint the container) then repaints the button, displaying the current state of the button. Before defining our `label` class, let's play with the `button` class in @@ -460,6 +466,7 @@ the OCaml toplevel: ```ocaml # let b = new button ~callback:(fun () -> print_endline "Ouch!") "button";; val b : button = + # b#repaint;; Button being repainted, state is Released - : unit = () @@ -500,7 +507,7 @@ Button being repainted, state is Released ``` ### A Note About `self` -In all the examples above we defined classes using the general pattern: +In all the examples above, we defined classes using the general pattern: ```ocaml @@ -509,13 +516,13 @@ class name = (* ... *) end ``` -I didn't explain the reference to `self`. In fact this names the object, +The reference to `self` names the object, allowing you to call methods in the same class or pass the object to functions outside the class. In other words, it's exactly the same as `this` in C++/Java. You may completely omit the -`(self)` part if you don't need to refer to yourself - indeed in all the -examples above we could have done exactly that. However, I would advise -you to leave it in there because you never know when you might modify +`(self)` part if you don't need to refer to yourself. Indeed, in all the +examples above, we could have done exactly that. However, we advise that +you leave it in because you never know when you might modify the class and require the reference to `self`. There is no penalty for having it. @@ -527,21 +534,19 @@ val b : button = # let l = new label "label" "Press me!";; val l : label = # [b; l];; -Line 1, characters 5-6: Error: This expression has type label but an expression was expected of type button The first object type has no method add ``` -I created a button `b` and a label `l` and I tried to create a list -containing both, but I got an error. Yet `b` and `l` are both `widget`s, -so why can't I put them into the same list? Perhaps OCaml can't guess -that I want a `widget list`? Let's try telling it: +We created a button `b` and a label `l` and then tried to create a list +containing both, but we got an error. Yet `b` and `l` are both `widget`s, +so maybe we can't put them into the same list because OCaml can't guess +that we want a `widget list`. Let's try telling it: ```ocaml # let wl = ([] : widget list);; val wl : widget list = [] # let wl = b :: wl;; -Line 1, characters 15-17: Error: This expression has type widget list but an expression was expected of type button list Type widget = < get_name : string; repaint : unit > @@ -553,8 +558,8 @@ Error: This expression has type widget list The first object type has no method add ``` -It turns out that OCaml doesn't coerce subclasses to the type of the -superclass by default, but you can tell it to by using the `:>` +It turns out that OCaml doesn't coerce subclasses to the superclass type +by default, but you can tell it to by using the `:>` (coercion) operator: ```ocaml @@ -565,18 +570,18 @@ val wl : widget list = [; ] ``` The expression `(b :> widget)` means "coerce the button `b` to have type -`widget`". Type-safety is preserved because it is possible to tell +`widget`." Type-safety is preserved because it is possible to tell completely at compile time whether the coercion will succeed. -Actually, coercions are somewhat more subtle than described above, and -so I urge you to read the manual to find out the full details. +Actually, coercions are somewhat more subtle than described above, +so please read the manual to find out the full details. -The `container#add` method defined above is actually incorrect, and +The `container#add` method defined above is actually incorrect; it fails if you try to add widgets of different types into a `container`. A coercion would fix this. -Is it possible to coerce from a superclass (eg. `widget`) to a subclass -(eg. `button`)? The answer, perhaps surprisingly, is NO! Coercing in +Is it possible to coerce from a superclass (e.g., `widget`) to a subclass +(e.g., `button`)? The answer, perhaps surprisingly, is NO! Coercing in this direction is *unsafe*. You might try to coerce a `widget` which is in fact a `label`, not a `button`. @@ -589,7 +594,7 @@ classes). `=` and `<>` can be used to compare objects for *physical* equality (an object and its copy are not physically identical). You can also use `<` -etc. which provides an ordering of objects based apparently on their +etc., which provides an ordering of objects based apparently on their IDs. ## Objects Without Class @@ -597,11 +602,11 @@ Here we examine how to use objects pretty much like records, without necessarily using classes. ### Immediate Objects and Object Types -Objects can be used instead of records, and have some nice properties +Objects can be used instead of records. Plus, they have some nice properties that can make them preferable to records in some cases. We saw that the -canonical way of creating objects is to first define a class, and use +canonical way of creating objects is to first define a class, then use this class to create individual objects. This can be cumbersome in some -situations since class definitions are more than a type definition and +situations becaus class definitions are more than a type definition and cannot be defined recursively with types. However, objects have a type that is very analog to a record type, and it can be used in type definitions. In addition, objects can be created without a class. They @@ -650,14 +655,14 @@ but each solution has its own advantages: * **speed**: slightly faster field access in records * **field names**: it is inconvenient to manipulate records of different types when some fields are named identically but it's not - a problem with objects -* **subtyping**: it is impossible to coerce the type of a record to a - type with less fields. That is however possible with objects, so + a problem with objects. +* **subtyping**: it is impossible to coerce the record type to a + type with fewer fields. However, that is possible with objects, so objects of different kinds that share some methods can be mixed in a data structure where only their common methods are visible (see next - section) + section). * **type definitions**: there is no need to define an object type in - advance, so it lightens the dependency constraints between modules + advance, so it lightens the dependency constraints between modules. ### Class Types vs. Just Types Beware of the confusion between *class types* and object *types*. A @@ -691,7 +696,7 @@ val x : < get : int > = val l : t list = [; ] ``` -Mixing objects that share a common subtype can be done, but requires +Mixing objects that share a common subtype can be done, but it requires explicit type coercion using the `:>` operator: ```ocaml @@ -700,7 +705,6 @@ val x : < get : int > = # let y = object method get = 80 method special = "hello" end;; val y : < get : int; special : string > = # let l = [x; y];; -Line 1, characters 13-14: Error: This expression has type < get : int; special : string > but an expression was expected of type < get : int > The second object type has no method special