Open vs include?

Code: (from OCaml - The module system )

  module PrioQueue = struct
    type priority = int
    type 'a queue = Empty | Node of priority * 'a * 'a queue * 'a queue

    let empty = Empty

    let rec insert queue prio elt =
      match queue with
      | Empty -> Node (prio, elt, Empty, Empty)
      | Node (p, e, left, right) ->
          if prio <= p then Node (prio, elt, insert right p e, left) else Node (p, e, insert right prio elt, left)

    exception Queue_is_empty

    let rec remove_top = function
      | Empty -> raise Queue_is_empty
      | Node (_prio, _elt, left, Empty) -> left
      | Node (_prio, _elt, Empty, right) -> right
      | Node (_prio, _elt, (Node (lprio, lelt, _, _) as left), (Node (rprio, relt, _, _) as right)) ->
          if lprio <= rprio then Node (lprio, lelt, remove_top left, right)
          else Node (rprio, relt, left, remove_top right)

    let extract = function
      | Empty -> raise Queue_is_empty
      | Node (prio, elt, _, _) as queue -> (prio, elt, remove_top queue)
  end



  module PrioQueueOpt = struct
    include PrioQueue

    let remove_top_ot x = try Some (remove_top x) with Queue_is_empty -> None
    let extract_opt x = try Some (extract x) with Queue_is_empty -> None
  end

This code compiles regardless of whether I use include PrioQueue or open PrioQueue.

My current (possibly incorrect) understanding is:

  1. open adds bindings to local env referencing openeed module
  2. include literally copies over the code

I regularly use open. I have seen code with include, but I don’t think
I have ever used it myself.

What are situations where open is not sufficient and we have to use include ?

One important observable difference is that include re-exports what’s included, not open.

For example:

  ...

  module PrioQueueOpt = struct
    include PrioQueue

    let remove_top_ot x = try Some (remove_top x) with Queue_is_empty -> None
  end

  let new_queue = PrioQueueOpt.empty  (* works ok *)
  ...

  module PrioQueueOpt = struct
    open PrioQueue

    let remove_top_ot x = try Some (remove_top x) with Queue_is_empty -> None
  end

  let new_queue = PrioQueueOpt.empty (* fail because [empty] is not re-exported by PrioQueue *)
4 Likes

It is also explained here.

@AsimovIV @vds :

Thanks! Is this (re-exporting of defs) the only difference? I thought there was something deeper / more subtle going on.

From the OCaml manual: Including the components of another structure

The difference between open and include is that open simply provides short names for the components of the opened structure, without defining any components of the current structure, while include also adds definitions for the components of the included structure.

So yes, that seems to be the only difference between the two. I haven’t seen any in practice either.

2 Likes

To answer this part of your question, though:

What are situations where open is not sufficient and we have to use include ?

In my experience, include is usually used when some kind of subtyping/inheritance is desired. For example, if I have some module signatures:

module type TO_STRING = sig
  type t
  val to_string : t -> string
end

module type OF_STRING = sig
  type t
  val of_string : string -> t option
end

module type SERIALIZABLE = sig
  include TO_STRING
  include OF_STRING with type t := t
end

In an OOP context, the closest analogue might be a class that implements two interfaces. But here you might use it like this:TryOCaml

module Int_serializable = struct
  type t = int
  let to_string = string_of_int
  let of_string = int_of_string_opt
end

module _ : TO_STRING = Int_serializable (* Won't compile if [Int_serializable] doesn't conform to [TO_STRING] *)
module _ : OF_STRING = Int_serializable
module _ : SERIALIZABLE = Int_serializable

Obviously this is in the context of module signatures and not structures, but you can play around with this using structure-based includes and get the same result.