Search code examples
moduledependenciesocamlfriend

Friend Modules in OCaml


I currently have two "layers" of modules that represent identifier-data relationships in a database.

The first layer defines identifier types, such as IdUser.t or IdPost.t while the second layer defines data types such as User.t or Post.t. I need all the modules of the first layer to be compiled before the modules of the second layer, because a Post.t must hold the IdUser.t of its author and the User.t holds the IdPost.t of the last five posts he visited.

Right now, IdUser.t provides functionality that should only ever be used by User.t, such as the ability to transform an IdUser.t into an IdUser.current: for security reasons, this transform must only ever be performed by the function User.check_password. Since IdUser and User are independent modules, I need to define those features as public functions and rely on conventions to avoid calling them anywhere outside of User, which is rather dirty. A symmetrical situation happens in IdPost.mine:

module IdUser : sig
  type t
  type current
  val current_of_t : t -> current (* <--- Should not be public! *)
end = struct 
  type t = string
  type current = string
  let current_of_t x = x
end

module IdPost : sig
  type t
  type mine
  val mine_of_t   : t -> mine (* <--- Should not be public! *)
end = struct 
  type t = string
  type mine = string
  let mine_of_t   x = x
end

module Post : sig 
 (* Should not "see" IdUser.current_of_t but needs IdPost.mine_of_t *)
  val is_mine : IdUser.current -> IdPost.t -> IdPost.mine
end 

module User : sig
  (* Should not "see" IdPost.mine_of_t but needs IdUser.current_of_t *)
  val check_password : IdUser.t -> password:string -> IdUser.current
end

Is there a way of defining an current_of_t : t -> current function in IdUser that can only be called from within module User ?

EDIT: this was a simplified example of one pair of modules, but there's an obvious solution for a single pair that cannot be generalized to multiple pairs and I need to solve this for multiple pairs — about 18 pairs, actually... So, I've extended it to be an example of two pairs.


Solution

  • So IdUser is in reality an existential type: For User there exists a type IdUser.current such that the public IdUser.t can be lifted to it. There are a couple of ways to encode this: either functorize User as Gasche shows if statically managing the dependence is sufficient, or use first-class modules or objects if you need more dynamism.

    I'll work out Gasche's example a bit more, using private type abbreviations for convenience and to show how to leverage translucency to avoid privatizing implementation types too much. First, and this might be a limitation, I want to declare an ADT of persistent IDs:

    (* File id.ml *)
    module type ID = sig
      type t
      type current = private t
    end
    
    module type PERSISTENT_ID = sig
      include ID
      val persist : t -> current
    end
    

    With this I can define the type of Posts using concrete types for the IDs but with ADTs to enforce the business rules relating to persistence:

    (* File post.ml *)
    module Post
      (UID : ID with type t = string)
      (PID : PERSISTENT_ID with type t = int)
    : sig 
      val is_mine : UID.current -> PID.t -> PID.current
    end = struct
      let is_mine uid pid =
        if (uid : UID.current :> UID.t) = "me" && pid = 0
          then PID.persist pid
          else failwith "is_mine"
    end
    

    The same thing with Users:

    (* File user.ml *)
    module User
      (UID : PERSISTENT_ID with type t = string)
    : sig
      val check_password : UID.t -> password:string -> UID.current
    end = struct
      let check_password uid ~password =
        if uid = "scott" && password = "tiger"
          then UID.persist uid
          else failwith "check_password"
    end
    

    Note that in both cases I make use of the concrete but private ID types. Tying all together is a simple matter of actually defining the ID ADTs with their persistence rules:

    module IdUser = struct 
      type t = string
      type current = string
      let persist x = x
    end
    
    module IdPost = struct 
      type t = int
      type current = int
      let persist x = x
    end
    
    module MyUser = User (IdUser)
    module MyPost = Post (IdUser) (IdPost)
    

    At this point and to fully decouple the dependencies you will probably need signatures for USER and POST that can be exported from this module, but it's a simple matter of adding them in.