5 ms·
What I never understood is why OCaml allows row polymorphism in objects, but not in record types. For example, we can write: let bar x = x#info + 1 and OCa
by kxyvr 12y ago
What I never understood is why OCaml allows row polymorphism in objects, but not in record types. For example, we can write:
let bar x = x#info + 1
and OCaml will give a type:
val bar : < info : int; .. > -> int = <fun>
which is precisely what we want since we can now define functions on any object that has an info member. At the same time, we can't write
let buz x = x.info + 1
because we receive the error
Error: Unbound record field info
This is a big deal since the exposed members of objects must be methods in OCaml. Certainly, we can declare values and mutable values, but these are not exposed in the objects type. Specifically,
# class foo = object val info = 1 end;;
class foo : object val info : int end
# let bar = new foo;;
val bar : foo = <obj>
# bar#info;;
Error: This expression has type foo
It has no method info
In order to get around this, we can change these values to methods (or add getter methods that return these values). However, the problem with doing that is then we can't make a new object with all of the same values, but with the specified valued changed:
# class foo = object method info = 1 end;;
class foo : object method info : int end
# let bar = new foo;;
val bar : foo = <obj>
# bar # info;;
- : int = 1
# let buz = bar with info = 2;;
Error: Parse error: [str_item] or ";;" expected (in [top_phrase])
In other words, we don't have a good mechanism for creating a bunch of immutable objects.
In other words, OCaml records allow immutable records, but they lack row polymorphsim. OCaml classes allow row polymorphism, but disallow immutable updates (immutable updates meaning create a new object with everything the same, but one element different.)
Does anyone know why this is the case?
- rwmj 12y agoI believe it's because #info (ie. method access) is kind of dynamic. The string "info" is hashed and then the hash is looked up at runtime against the class vtable. (For precise details, see 19.3.5 here: http://caml.inria.fr/pub/docs/manual-ocaml/intfc.html http://caml.inria.fr/pub/docs/manual-ocaml/intfc.html) I'm not clear under what circumstances this can be optimized to a direct call, and there is also some kind of cache, so repeated method calls to the same method are fast. This is the reason people who value performance over everything else steer clear of using objects in OCaml. Record fields are simply offsets in a struct, so the type must be fully known when compiling the code. It would not be possible to compile a function that could work on two different types of struct that both happened to have a .info field.
- kxyvr 12y agoActually, that makes a lot of sense. Thanks for the insight.
- tel 12y agoThis would be a great place to include lenses if OCaml allowed for higher rank polymorphism. I don't think you can jerry-rig a Functor-based lens solution into an object like this...
- kxyvr 12y agoIn fact you can, but it becomes a little verbose. Thankfully, much of the verbosity can be eliminated by macros. In any case, here's an example with lenses that shows how to do this: (* Create a basic lens type *) type ('a, 'b) t = { get : 'a -> 'b; set : 'b -> 'a -> 'a } (* A base lens, kind of like a base class *) type 'a myrecord_lens = { foo : ('a,int) t; label : ('a,string) t } (* Record 1 *) type myrecord1 = { foo : int; bar : float; label : string } let myrecord1 = { foo = 1; bar = 2.3; label = "myrecord1" } let foo_lens = { get = (fun x -> x.foo); set = (fun v x -> { x with foo = v }) } let bar_lens = { get = (fun x -> x.bar); set = (fun v x -> { x with bar = v }) } let label_lens = { get = (fun x -> x.label); set = (fun v x -> { x with label = v }) } let myrecord1_lens = { foo = foo_lens; label = label_lens } (* Record 2 *) type myrecord2 = { foo : int; buz : string; label : string } let myrecord2 = { foo = 4; buz = "five"; label = "myrecord2" } let foo_lens = { get = (fun x -> x.foo); set = (fun v x -> { x with foo = v }) } let buz_lens = { get = (fun x -> x.buz); set = (fun v x -> { x with buz = v }) } let label_lens = { get = (fun x -> x.label); set = (fun v x -> { x with label = v }) } let myrecord2_lens = { foo = foo_lens; label = label_lens } (* Write a generic function to work with these objects*) let myfunc (myrecord:'a) (lens:'a myrecord_lens) = let _ = Printf.printf "Working with: %s\n" (lens.label.get myrecord) in (lens.foo.get myrecord) + 1 (* Get some results *) let _ = myfunc myrecord1 myrecord1_lens let _ = myfunc myrecord2 myrecord2_lens For the important pieces of the output, we have: val myfunc : 'a -> 'a myrecord_lens -> int = <fun> Working with: myrecord1 - : int = 2 Working with: myrecord2 - : int = 5 Hence, by using lenses, we can basically get the polymorphic behavior that we need out of immutable records. The syntax is pretty verbose, but it can be lessened with macros. As a final note, there is an OCaml lens library to help with this, but you're still mostly stuck writing lenses by hand: https://github.com/avsm/ocaml-lens https://github.com/avsm/ocaml-lens Also, Jane Street has fieldslib, which does some interesting things with record types. It doesn't have exactly what I described above, but it does help in many cases and they have some nice macros to help generate getters and setters: https://github.com/janestreet/fieldslib https://github.com/janestreet/fieldslib
- kopter 12y agoRoughly, the intuition is that in OCaml, objects are to records as polymorphic variants are to regular variants
- emillon 12y ago> OCaml classes allow row polymorphism, but disallow immutable updates (immutable updates meaning create a new object with everything the same, but one element different.) There's a functional update construct for objects: http://caml.inria.fr/pub/docs/manual-ocaml/objectexamples.html#sec36 http://caml.inria.fr/pub/docs/manual-ocaml/objectexamples.ht... It works like { r with field = value }.
- tel 12y agoThat syntax works for records not objects. The {< _ = _ >} syntax is sort-of equivalent except it can only be used within method definitions (!) and is therefore too restricted. Instead, try taking a look at the store-comonad mechanism I've got below: https://news.ycombinator.com/item?id=9213482 https://news.ycombinator.com/item?id=9213482