I want to explore the construction of objects without depending on the “O” features of “Ocaml”. The code
let current_rand = ref 0;; let random () = current_rand := !current_rand * 25713 + 1345; !current_rand;;is provided as a too simple example of a random number generator here. It exposes the seed and cannot provide concurrent seeds and concomitant number streams as a good generator would. How about
let rngc () = let seed = ref 0 in (fun () -> seed := !seed * 25713 + 1345; !seed);;instead? This seems like a perfectly fine Caml object to me. It carries state and behavior and is multiply instantiatable.
There are two ways to go next from here:
It seemed clear that language support for user defined v-table layouts was needed. I suspect that Simula used v-tables. Can we express this type of a v-table in Caml? More exactly: can we express this pattern in Caml or must we extend Caml as Ocaml did?
How about a type that is a 2-tuple that combines a specific but unknown type, once covariantly and once contravariantly? This type will vary at run time but the types will always match for any tuple of this type.
# type ('a) vt = 'a * ('a -> int);; type 'a vt = 'a * ('a -> int)looks promising but it seems that we have defined a type constructor, rather than a type. I think an expression cannot have a type vt at compile time.
# type 'a vt = 'a * ('a -> int);; type 'a vt = 'a * ('a -> int) # let a = ref (3, fun x -> x+4);; val a : (int * (int -> int)) ref = {contents = (3,Seems to preclude the existence of a dynamic Caml type 'a * ('a -> unit). I don’t know how to type v-tables in light of these results. Notice that C++ compiles efficient code that uses v-tables where the type denoted by 'a is unknown at compile time. Tuncol admitted these types, but tuncol was not complete.)} # a := (false, fun b -> if b then 3 else 4);; This expression has type bool * (bool -> int) but is here used with type int * (int -> int)
The rules for records are different and the compiler pays more attention to record type definitions. The above declaration of vt produced only a constructor.
let rngcp () = let seed = ref (0, 42) in fun () -> match !seed with l, r -> seed := l*25713 + r*2633 + 1345, l*32654 + r*1327 + 4390; l;;which has the same type as rngc, but a different behavior and different state format. Both have type unit -> unit -> int. They can be used polymorphically. (Notice that the compiler can see that space for the new tuple need not be allocated for each evaluation of the tuple expression since access to the old tuple manifestly dies upon the assignment.)
Is this sort of polymorphism good enough?
let rngc () = let rec rf s = ( (fun () -> s := !s * 25713 + 1345; !s), (fun () -> rf (ref !s))) in rf (ref 71);;
let rngr () = let rec rf s = { rand = (fun () -> s := !s * 25713 + 1345; !s); clone = (fun () -> rf (ref !s))} in rf (ref 71);;
The tuple package has interface unit -> rt and can be invoked thus:
let p i = print_int i; print_char '\n' in match rngc() with rand, clone -> begin for i = 1 to 5 do p (rand()) done; match clone() with r2, c2 -> begin for i = 1 to 5 do p (r2()) done; for i = 1 to 5 do p (rand()) done end end;;The user of rngc gets to choose the name of the component functions.
Using interface unit->rr the record packaging might be used as follows:
let p i = print_int i; print_char '\n' in let r1 = rngr() in begin for i = 1 to 5 do p (r1.rand()) done; let r2 = r1.clone() in begin for i = 1 to 5 do p (r1.rand()) done; for i = 1 to 5 do p (r2.rand()) done; end end;;