This post is part of a series about my work on ocgtk, an OCaml binding for GTK4. The most recent release is preview1.
ocgtk generates bindings for GTK4 from its base GObject definitions. GObject has a class design with single-parent inheritance and interfaces, making it very similar to Java or Objective-C.
GObject is implemented in C, so its implementation is "C with objects":
- "objects" is really a pointer to an opaque struct
- constructors and methods are plain function calls - the first parameter is the instance value (implicit
thisorselfpointer)
This is straightforward to bind in OCaml. For example, let's say we wanted to bind the Gtk.Button class and its set_label method:
void gtk_button_set_label (GtkButton *button, const gchar *label);
We would create a wrapper function like this:
void ml_gtk_button_set_label (value button, value label) {
// The internal implementation details are unimportant -
// see the [OCaml FFI interface](https://dev.realworldocaml.org/foreign-function-interface.html)
// for more details
CAMLparam2(button, label);
GtkButton *b = GtkButton_val(button);
const gchar *l = String_val(label);
gtk_button_set_label(b, l);
CAMLreturn0;
}
And the OCaml declaration would look like this:
module Button = struct
type t
external set_label : t -> string -> unit = "ml_gtk_button_set_label"
end
This is simple enough for the methods that are defined directly on a class, but it doesn't work for methods that are inherited from a parent class. For example, the parent class, Gtk.Widget, defines a set_focusable method which we would like to be able to use on our button wrapper. But defining it in the same way does not allow set_focusable to be used on our button type:
module Widget = struct
type t
external set_focusable : t -> bool -> unit = "ml_gtk_widget_set_focusable"
end
module Button = struct
type t
external new_button : unit -> t = "ml_gtk_button_new"
external set_label : t -> string -> unit = "ml_gtk_button_set_label"
end
let button = Button.new_button ()
Widget.set_focusable button true (* this is a type error *)
In the above example, the Button.t type is not interchangeable with the Widget.t type as they are both incompatible (opaque) types. So how can we solve this?
Poor man's polymorphism
We want to find a way to allow a Button.t (or any Widget.t) sub-class to be used in place of a Widget.t (the proverbial "is-a" relationship). Conversely, we don't want other classes to be able to be used in their place to preserve some degree of type-safety (in GObject, this will probably cause a runtime error, if not crash the application).
Redefining the method
The easiest thing to try first is to simply redefine set_focusable for the button class:
(* we'll start by putting widget and button in their own modules to avoid conflicts *)
module Widget = struct
type t
external set_focusable : t -> bool -> unit = "ml_gtk_widget_set_focusable"
end
module Button = struct
type t
external new_button : unit -> t = "ml_gtk_button_new"
external set_focusable : t -> bool -> unit = "ml_gtk_widget_set_focusable"
end
let button = Button.new_button ()
Button.set_focusable button true (* this works, but it's stupid *)
This works, but it's verbose and requires repeating the same method for every subclass.
Using a polymorphic variant
Polymorphic variants are a way of defining types that can have "subsets" of another type. They use a structural typing system (unlike the rest of the language) for their constructors, so that a set of constructors can be represented as a sub-type of another by using the coercion operator (:>) to narrow an instance.
For example:
type widget = [`widget]
type button = [`button | `widget]
(* widget is a subtype of button *)
type 'obj obj_ (* GObject type, where the inheritance constraint is captured in 'obj *)
module Button = struct
type t = button obj_
...
end
module Widget = struct
type t = widget obj_
end
The 'obj parameter here is a phantom type — it never appears in the runtime representation of a value, only in the type system. Its sole purpose is to carry the class tag so the type checker can track the GObject class identity through the hierarchy. At runtime, every 'obj obj_ is just a pointer to the same opaque GObject struct.
However, the above doesn't really make sense in terms of an inheritance system (even though it makes sense in OCaml's type system), which becomes clear with an example:
let () =
let button = Button.new_ () in
Widget.set_focusable (button :> Widget.t) true (* type error *)
We get told that this is not a subtype:
Type Button.t = [ `button | `widget ] obj_ is not a subtype of
Widget.t = [ `widget ] obj_
The second variant type does not allow tag(s) `button
In the above example, we're trying to narrow Button.t to Widget.t, but this doesn't make sense because Button.t has more constructors than Widget.t i.e. Widget.t is the sub-type of Button.t, not the other way around: we've effectively defined an inverted type hierarchy where parent classes can only be coerced to their child class!
Using contravariance
Interestingly, this works when the type in question is not abstract. Imagine for a moment we have a generic event handler type:
type 'a handler = 'a -> unit
and we define a function that accepts a handler that can handle both `Button_clicked and `Text_changed events:
let everything_handler : [`Button_clicked | `Text_changed] handler =
fun v -> match v with
| `Button_clicked -> print_endline "click"
| `Text_changed -> print_endline "change"
Technically, we should be able to assign the value of this function to a variable that has a narrower type e.g. can only handle `Button_clicked events:
(* Expect just Button_clicked *)
let narrow_handler : [`Button_clicked] handler = everything_handler
However, OCaml treats this as a type error:
The value everything_handler has type
[ `Button_clicked | `Text_changed ] handler =
[ `Button_clicked | `Text_changed ] -> unit
but an expression was expected of type
[ `Button_clicked ] handler = [ `Button_clicked ] -> unit
The second variant type does not allow tag(s) `Text_changed
This is normally where we would use the coercion (:>) operator to make this work.
let narrow_handler : [`Button_clicked] handler = (everything_handler :> [`Button_clicked ] handler)
However, in our case, the type obj_ is abstract, so we need to give the compiler a helping hand to assure it that the type-relationship is contravariant.
We know we want to constrain our 'obj obj_ type to be contravariant, so we use the little-known - (contravariance) annotation on the polymorphic component of our declaration:
Which then makes the following code work:
let () =
let button = Button.new_ () in
Widget.set_focusable (button :> Widget.t) true (* now compiles *)
This effectively inverts the relationship between Button.t and Widget.t, making Button.t reducible to Widget.t. Although we still need to provide an explicit coercion, we now can pass Button.t objects to Widget.t as the compiler knows its usage as a parameter or return type is contravariant.
Thankfully, in the C code, this requires no special handling as GObject can determine the Widget part of the type automatically, giving us (object-oriented) polymorphism on our class hierarchy.
Wrapping up
Combining phantom types with polymorphic variant subtyping and a contravariance annotation gives us GObject's "is-a" relationship enforced at compile time, without touching OCaml's object system at all.
The explicit :> coercion is still visible to users in some situations. ocgtk provides an object-oriented wrapper layer, so calling an inherited method directly on a subclass value works without any coercion. However, if a user wants to pass a Button.t to a function that expects a Widget.t (for example, as a callback argument) they will need to write the coercion themselves. This is a reasonable trade-off: the common case is ergonomic, and the coercion in the less common case is at least explicit and type-safe.
Acknowledgments
The technique of using a contravariant phantom type parameter to model GObject's class hierarchy in OCaml's core type system is not original to ocgtk — lablgtk, the long-standing OCaml binding for GTK2/GTK3, uses the same approach, and ocgtk references that design for its handling of inheritance.
0 Comments
Log in to join the conversation.No comments yet. Be the first to share your thoughts.