Hello everyone,
I am trying to mimic a Java class using a very simple example.
Could you please tell me if it is a correct (OCaml) way?
module Lamp = struct
type state = On | Off
type lamp_state = {mutable c: state}
type lamp_color = {mutable cl: string}
let set_color clr = {cl = clr}
let create_lamp () = {c = Off}
let turn_on s = s.c <- On
let get_state s = match s.c with
| On -> "On"
| Off -> "Off"
end
let lmp = Lamp.create_lamp ();;
Lamp.turn_on lmp;;
Lamp.get_state lmp;;
Mostly, but there is some mistakes. I would write it this way:
module Lamp = struct
type state = On | Off
(* this is a convention in OCaml to name `t' the principal type defined by your
module and in your case you have two attributes, namely state and color *)
type t = {mutable state : state; mutable color : string}
let create c = {state = Off; color = c}
let set_color lamp c = lamp.color <- c
let turn_on lamp = lamp.state <- On
let get_state lamp = lamp.state
end
And if you prefer pure functional data structure without mutable attributes, you can do:
module Lamp = struct
type state = On | Off
type t = {state : state; color : string}
let create c = {state = Off; color = c}
let set_color lamp c = {lamp with color = c}
let turn_on lamp = {lamp with state = On}
let get_state lamp = lamp.state
end
To be honest your are not trying to mimic Java/Python class but the converse is true. This OCaml code is the right way to formalize your thought (this is really what you have in mind), and most of object oriented languages try to mimic this code, but do it stupidly because they don’t have a decent module system.
If you want, you can even add some additional sugar to @kantian’s solution:
module Lamp = struct
type state = On | Off
type t = {state : state; color : string}
let create color = {state = Off; color} (* Added sugar here ... *)
let set_color lamp color = {lamp with color} (* ... and here. *)
let turn_on lamp = {lamp with state = On}
let get_state lamp = lamp.state
end
(Although it’s not clear the additional sugar makes the code any sweeter.)
module Lamp2 = struct
type state = On | Off
type t = {mutable state : state}
let create = {state = Off}
let turn_on lamp = lamp.state <- On
let get_state lamp = lamp.state
end
And tested it by
utop # let la = Lamp2.create;;
val la : Lamp2.t = {Lamp2.state = Lamp2.Off}
# Lamp2.turn_on la;;
- : unit = ()
# let lb = Lamp2.create;;
val lb : Lamp2.t = {Lamp2.state = Lamp2.On}
Well, the state of lb is On
I made another example with a slight difference on adding () parameter for create
module Lamp3 = struct
type state = On | Off
type t = {mutable state : state}
let create () = {state = Off}
let turn_on lamp = lamp.state <- On
let get_state lamp = lamp.state
end
And it works as expected:
# let l3a = Lamp3.create ();;
val l3a : Lamp3.t = {Lamp3.state = Lamp3.Off}
# Lamp3.turn_on l3a;;
- : unit = ()
# let l3b = Lamp3.create ();;
val l3b : Lamp3.t = {Lamp3.state = Lamp3.Off}
I was curious why the () would make it so different?
Because without it you do not define a function (which generates lamp) but a t value. Your Lamp2 module defines only a unique lamp so la, lb and Lamp2.create are only different aliases to the same value.
If you go that way, we can further improve the API using optional and named arguments.
module Lamp = struct
type state = On | Off
type t = {state : state; color : string}
let create ?(state=Off) color = {state; color}
let set_color lamp ~color =
if lamp.color = color then lamp else {lamp with color}
let get_color lamp = lamp.color
let turn_on lamp = match lamp.state with
| On -> lamp
| Off -> {lamp with state = On}
let turn_off lamp = match lamp.state with
| On -> {lamp with state = Off}
| Off -> lamp
let get_state lamp = lamp.state
end
Here the functions only allocate new values when needed. And from the module user point of view:
(* import Lamp.state to use On and Off without need to prefix by path name Lamp *)
type state = Lamp.state = On | Off
(* by default the lamp is Off *)
let lmp = Lamp.create "red";;
val lmp : Lamp.t = {Lamp.state = Lamp.Off; color = u"red"}
(* we can define a create_on function *)
let create_on = Lamp.create ~state:On;;
val create_on : string -> Lamp.t = <fun>
(* with it the lamp is On at creation *)
let lmp = create_on "yellow";;
val lmp : Lamp.t = {Lamp.state = Lamp.On; color = u"yellow"}
(* thanks to named arguments we can easily partially apply `set_color` with color parameter *)
let paint_blue = Lamp.set_color ~color:"blue";;
val paint_blue : Lamp.t -> Lamp.t = <fun>
paint_blue lmp;;
- : Lamp.t = {Lamp.state = Lamp.On; color = u"blue"}
(* or use the pipe operator conveniently *)
lmp |> Lamp.turn_off |> Lamp.set_color ~color:"orange";;
- : Lamp.t = {Lamp.state = Lamp.Off; color = u"orange"}
And of course, to have encapsulation we should add a signature coercion on the module Lamp to leave the type t abstract.
module Lamp : sig
type t
type state = On | Off
val create : ?state:state -> string -> t
val set_color : t -> color:string -> t
val get_color : t -> string
val turn_on : t -> t
val turn_off : t -> t
val get_state : t -> state
end = struct
type state = On | Off
type t = {state : state; color : string}
let create ?(state=Off) color = {state; color}
let set_color lamp ~color =
if lamp.color = color then lamp else {lamp with color}
let get_color lamp = lamp.color
let turn_on lamp = match lamp.state with
| On -> lamp
| Off -> {lamp with state = On}
let turn_off lamp = match lamp.state with
| On -> {lamp with state = Off}
| Off -> lamp
let get_state lamp = lamp.state
end
You should have a look at the zipper data structure (here a possible implementation in OCaml). The data structure is very well named: when you close your coat with a zipper your are using a double linked list.
module Lamp = struct
type 'a t = ...
end;;
module LampsContainer = struct
type 'a t = {container : 'a Lamp.t list}
(* or type t = {container : 'a. 'a Lamp.t list}, depending on what you want *)
end;;