Should I use objects in this case?

In my program, I have to deal with three kinds of things: orders, which can be carrot orders or milk orders. All three types will appear as arguments of some later functions. My current setup is:

utop # type order = C of carrot_order | M of milk_order
and carrot_order = {client: string; number: int}
and milk_order = {client: string; volume: float};;

utop # let order_client = function
  C {client; _} | M {client; _} -> client;;
val order_client : order -> string = <fun>

This is obviously a toy example, and there are other fields for each type. This permits me to write the later functions that I need. (A more or less equivalent way would be to “factor out” the client field, see below.)

However, there is a feel of redundancy in the code above, and there is a collision for the field name client (the compiler warns: (warning 30 [duplicate-definitions])). Not sure if this is bad. Also, I do not know if the “double pun” I use in the function order_client is idiomatic to OCaml or on the contrary is bad practice. My first idea, which seemed natural coming from Python, was to use objects and subtyping or row polymorphism instead. But I seem to understand that objects are not much used in OCaml, and should be used mostly when one needs open recursion (Objects - Real World OCaml), which is not the case here.

So, should I be content with my current solution (or the solution below), or should I use objects, or yet another paradigm ?


Another possibility: factor out the client field, and define a function which constructs an order from a client and a bare_order:

utop # type order = {client: string; bare_order: bare_order}
and bare_order = Cb of bare_carrot_order | Mb of bare_milk_order
and bare_carrot_order = {number: int}
and bare_milk_order = {volume: float};;

utop # let order_from_bare_order client bare_order = {client; bare_order};;
val order_from_bare_order : string -> bare_order -> order = <fun>

This also looks a bit verbose.

The factored approach looks good to me. The functions which later handle the orders will ultimately tell you if it’s good or bad. If they are simple to implement, it’s good. A little verbosity now will pay off later.

2 Likes

Two thoughts:

  1. OCaml’s objects don’t admit downcast (from order down to milk and carrot orders). So if you need to do lots of those “match …with” on your orders, then objects aren’t going to be good for that.
  2. But OTOH, if you only have a few such matches, then sure, you can turn each match into a method on order.

But really, objects are designed for when you have a large and ad-hoc hierarchy of types, maybe even extensible by consumers of your library. If you literally have only these two examples, why bother with objects? And also, your example code does seem verbose. I mean …

type order = string * bare_order
and bare_order = Cb of int | Mb of float
;;
let mk c b : order = (c,b);;
let carrot c n : order = (c,Cb n);;
let milk c n : order = (c,Mb n);;

It isn’t necessary to wrap things up in records.

1 Like

Having records is nice if you read the fields or do an update. More concise, and less problem with typing, in my opinion.

3 Likes