A thought on F# object model

Here I expand on my thought about the possible semantic model for F# objects.

The initialization semantics of objects may be formalized via nestable lazy records, where a record can contain at most one sub-record; the sub-record represents an inheriting object.

(* x and y are instance fields, while z represents an inheriting object *)
# type t = { x: int; y: int; z: (t Lazy.t) option };;
# let rec initialize x = match x with 
  | None -> ()
  | Some x -> initialize ((Lazy.force x).z);;

# let o1 = lazy { x = 1; y = 2; z = Some (lazy {x = 3; y = 4; z = None}) };;
# let _ = initialize (Some o1);;

# let rec o3 = lazy { x = 1; y = 2; z = Some (lazy {x = 3; y = (Lazy.force o3).y; z = None}) };;
# let _ = initialize (Some o3);;

# let rec o2 = lazy { x = 1; y = (Lazy.force o2).x; z = None };;
# let _ = initialize (Some o2);;
Exception: Lazy.Undefined.
Object initialization recursively forces all sub-records (initialize). As the above program illustrates, enclosing records cannot access sub-records, but sub-records can access enclosing ones. This looks close to the behavior that I observed about object initialization.

I have been questing for a uniform and disciplined programming idiom of object initialization along the F#'s design choice (hopefully) without losing the original expressive power.

My idea is, in short, to restrict the trailing expression to a call of a dedicated method of the class, where the self variable is passed as argument.

To illustrate, let me consider the following code.

> type c1 = class
 val i1:int
 new (init) as this = { i1 = init } then this.initialize_c1 this
 abstract initialize_c1 : c1 -> unit
 default self.initialize_c1 x = print_endline ("hello("^(string_of_int x.i1)^")")
end;;
> type c2 = class
 inherit c1
 val i2:int
 new (init) as this = { inherit c1 (init); i2 = (init*2)} 
                        then this.initialize_c2 this
 override self.initialize_c1 x = print_endline ("hi("^(string_of_int x.i1)^")")
 abstract initialize_c2 : c2 -> unit
 default self.initialize_c2 x = print_endline ("bye("^(string_of_int x.i2)^")")
end;;
> let x1 = new c1 1;;
val x1 : c1
hello(1)
> let x2 = new c2 2;;
val x2 : c2
hi(2)
bye(4)
Above the trailing expressions are exactly calls to dedicated methods, that is, initialize_c1 for c1 initialize_c2 for c2. Besides since initialize_c1 is abstract, it is overridable as I did in c2.

Why can it be interesting? Because access to a field of the currently constructing object becomes more explicit. I also guess that in this scenario it could be easier to signal warnings of potential null exceptions and that non-null guaranteeing type systems proposed by M. Fahndrich can be even more beneficial. (If I have time and opportunities, I want to work on it!)

A variant of the above idiom may be useful as well.

> type d1 = class
    val mutable i1:int
    new (init) = let f this = () in let g this = () in new d1(f, g, init)
    new (f: d1 -> unit, g: d1 -> unit, init) as this = { i1 = init } then f this; g this
  end;;
> type d2 = class
    inherit d1 
    val i2:int
    new (init) as this = 
      let f (x:d1) = print_endline ("hello("^(string_of_int x.i1)^")") in 
      let g (x:d1) = x.i1 <- x.i1 * 2 in
      { inherit d1(f, g, init); i2 = init } then this.initialize_d2 this
    abstract initialize_d2 : d2 -> unit
    default self.initialize_d2 x = print_endline ("bye("^(string_of_int x.i1)^")")
  end;;
> let x = new d2 2;;
hello(2)
bye(4)

Jan/2008