Модули Friend в OCaml - PullRequest
       22

Модули Friend в OCaml

5 голосов
/ 23 февраля 2011

В настоящее время у меня есть два «слоя» модулей, которые представляют отношения идентификатор-данные в базе данных.

Первый уровень определяет типы идентификаторов, такие как IdUser.t или IdPost.t, тогда как второй уровень определяет типы данных, такие как User.t или Post.t. Мне нужно, чтобы все модули первого слоя были скомпилированы до модулей второго слоя, потому что Post.t должен содержать IdUser.t его автора, а User.t содержит IdPost.t из последних пяти постов, которые он посетил.

Прямо сейчас, IdUser.t предоставляет функциональность, которая должна использоваться только User.t, такую ​​как возможность преобразования IdUser.t в IdUser.current: по соображениям безопасности это преобразование должно выполняться только функция User.check_password. Поскольку IdUser и User являются независимыми модулями, мне нужно определить эти функции как публичные функции и опираться на соглашения, чтобы избежать вызова их где-либо за пределами User, что довольно грязно. Симметричная ситуация происходит в 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

Есть ли способ определения функции current_of_t : t -> current в IdUser, которая может быть вызвана только из модуля User?

РЕДАКТИРОВАТЬ : это был упрощенный пример одна пара модулей, но есть очевидное решение для одной пары, которую нельзя обобщить на несколько пар, и мне нужно решить эту проблему для нескольких пар & mdash; около 18 пар, на самом деле ... Итак, я расширил его, чтобы он был примером двух пар.

Ответы [ 4 ]

3 голосов
/ 23 февраля 2011

Таким образом, IdUser на самом деле является экзистенциальным типом: для User существует тип IdUser.current такой, что общественность IdUser.t может быть поднята к ней. Есть несколько способов закодировать это: либо functorize User, как показывает Гаше, если статического управления зависимостями достаточно, или используйте первоклассные модули или объекты, если вам нужно больше динамизма.

Я еще немного разберу пример Гаше, используя сокращения частного типа для удобства и покажу, как использовать полупрозрачность, чтобы избежать слишком частой приватизации типов реализации. Во-первых, и это может быть ограничением, я хочу объявить ADT постоянного 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

С помощью этого я могу определить тип Post s, используя конкретные типы для ID s, но с помощью ADT для обеспечения соблюдения бизнес-правил, касающихся персистентности:

(* 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

То же самое с User s:

(* 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

Обратите внимание, что в обоих случаях я использую конкретные, но частные ID типы. Связать все вместе - это просто определить ID ADT с их правилами постоянства:

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)

На этом этапе и для полного разделения зависимостей вам, вероятно, понадобятся подписи для USER и POST, которые можно экспортировать из этого модуля, но их просто добавить.

2 голосов
/ 23 февраля 2011

Я предлагаю вам параметризировать Post (и, возможно, User для согласованности) с помощью подписи для модуля IdUser: вы бы использовали подпись с current_of_t для User и одну без для Post .

Это гарантия того, что Post не использует IdUser частных функций, но общедоступный интерфейс IdUser все еще слишком разрешительный. Но с этой настройкой вы полностью изменили зависимости, и IdUser (чувствительная часть) может напрямую контролировать его использование, присвоить себе (с закрытой частью) значение IdUser и ограничить открытую подпись открытыми частями.

module type PrivateIdUser = sig
  val secret : unit
end

module type PublicIdUser = sig
end

module type UserSig = sig
  (* ... *)
end
module MakeUser (IdUser : PrivateIdUser) : UserSig = struct
  (* ... *)
end

module IdUser : sig
  include PublicIdUser
  module User : UserSig
end
 = struct
   module IdUser = struct
     let secret = ()
   end
   module User = MakeUser(IdUser)
   include IdUser
end

module Post = struct
  (* ... *)
end

Редактировать : параллельное, во временном смысле, решение Паскаля Куока - решение очень хорошее. На самом деле это проще и имеет меньше шаблонов. Мое решение добавляет абстракцию, которая позволяет немного больше модульности, так как вы можете определить User независимо от IdUser.

Я думаю, какое решение лучше, вероятно, зависит от конкретного применения. Если у вас много разных модулей, которые используют PrivateIdUser личную информацию, то использование функторов для написания их отдельно вместо объединения всех в один и тот же модуль может быть хорошей идеей. Если только «1024» должно быть в «частной зоне» и оно не очень большое, то решение Паскаля - лучший выбор.

Наконец, хотя принудительное указание интерфейсов Private и Public может рассматриваться как дополнительная нагрузка, это также способ сделать свойства доступа различных модулей более явными, чем использование позиции внутри иерархии модулей.

2 голосов
/ 23 февраля 2011

Один способ, который, по-видимому, работает, по крайней мере, на вашем упрощенном примере, - это сгруппировать IdUser и User в одном модуле:

module UserAndFriends : sig ... end = struct
 module IdUser : sig
  ...
 end = struct
  ...
 end

 module User = struct
   ...
 end
end

module Post : sig 
  val create : (* <--- Should not "see" IdUser.current_of_t *)
    author:IdUser.current -> title:string -> body:string -> IdPost.t
end

Сокрытие опасных функций в сигнатуре UserAndFriends дает желаемый результат. Если вы не хотите создавать большой файл, содержащий как IdUser, так и User, вы можете использовать опцию -pack ocamlc для создания UserAndFriends. Обратите внимание, что в этом случае вы должны тщательно создать свой Makefile, чтобы файлы .cmi IdUser и User не были видны при компиляции Post. Я не специалист по Makefile для Frama-C, но я думаю, что мы используем отдельные каталоги и тщательно позиционируем опцию компилятора -I.

0 голосов
/ 03 марта 2016

Можно добиться детального контроля над сигнатурами с помощью комбинации рекурсивных модулей, модулей первого класса и GADT, но ограничением будет то, что все модули должны тогда находиться внутри одного модуля верхнего уровня и распаковывать Модули классов внутри рекурсивных модулей должны выполняться в каждой функции отдельно (не на уровне модулей, поскольку это приведет к исключению времени выполнения Undefined_recursive_module):

module rec M1 : sig
  module type M2's_sig = sig
    val a : int
    val c : float
  end

  module type M3's_sig = sig
    val b : string
    val c : float
  end

  type _ accessor =
    | I'm_M2 : M2.wit -> (module M2's_sig) accessor
    | I'm_M3 : M3.wit -> (module M3's_sig) accessor

  val access : 'a accessor -> 'a

  type wit

  val do_it : unit -> unit
end = struct
  module type M2's_sig = sig
    val a : int
    val c : float
  end

  module type M3's_sig = sig
    val b : string
    val c : float
  end

  type _ accessor =
    | I'm_M2 : M2.wit -> (module M2's_sig) accessor
    | I'm_M3 : M3.wit -> (module M3's_sig) accessor

  module M1 = struct
    let a = 1
    let b = "1"
    let c = 1.
  end

  let access : type a. a accessor -> a =
    function
    | I'm_M2 _ -> (module M1)
    | I'm_M3 _ -> (module M1)

  type wit = W

  let do_it () =
    let (module M2) = M2.(access @@ I'm_M1 W) in
    let (module M3) = M3.(access @@ I'm_M1 W) in
      Printf.printf "M1: M2: %d %s M3: %d %s\n" M2.a M2.b M3.a M3.b
end
and M2 : sig
  module type M1's_sig = sig
    val a : int
    val b : string
  end

  module type M3's_sig = sig
    val b : string
    val c : float
  end

  type _ accessor =
    | I'm_M1 : M1.wit -> (module M1's_sig) accessor
    | I'm_M3 : M3.wit -> (module M3's_sig) accessor

  val access : 'a accessor -> 'a

  type wit

  val do_it : unit -> unit
end = struct
  module type M1's_sig = sig
    val a : int
    val b : string
  end

  module type M3's_sig = sig
    val b : string
    val c : float
  end

  type _ accessor =
    | I'm_M1 : M1.wit -> (module M1's_sig) accessor
    | I'm_M3 : M3.wit -> (module M3's_sig) accessor

  module M2 = struct
    let a = 2
    let b = "2"
    let c = 2.
  end

  let access : type a. a accessor -> a =
    function
    | I'm_M1 _ -> (module M2)
    | I'm_M3 _ -> (module M2)

  type wit = W

  let do_it () =
   let (module M1) = M1.(access @@ I'm_M2 W) in
   let (module M3) = M3.(access @@ I'm_M2 W) in
   Printf.printf "M2: M1: %d %f M3: %d %f\n" M1.a M1.c M3.a M3.c
end
and M3 : sig
  module type M1's_sig = sig
    val a : int
    val b : string
  end

  module type M2's_sig = sig
    val a : int
    val c : float
  end

  type _ accessor =
    | I'm_M1 : M1.wit -> (module M1's_sig) accessor
    | I'm_M2 : M2.wit -> (module M2's_sig) accessor

  val access : 'a accessor -> 'a

  type wit

  val do_it : unit -> unit
end = struct
  module type M1's_sig = sig
    val a : int
    val b : string
  end

  module type M2's_sig = sig
    val a : int
    val c : float
  end

  type _ accessor =
    | I'm_M1 : M1.wit -> (module M1's_sig) accessor
    | I'm_M2 : M2.wit -> (module M2's_sig) accessor

  module M3 = struct
    let a = 3
    let b = "3"
    let c = 3.
  end

  let access : type a. a accessor -> a =
    function
    | I'm_M1 _ -> (module M3)
    | I'm_M2 _ -> (module M3)

  type wit = W

  let do_it () =
    let (module M1) = M1.(access @@ I'm_M3 W) in
    let (module M2) = M2.(access @@ I'm_M3 W) in
    Printf.printf "M3: M1: %s %f M2: %s %f\n" M1.b M1.c M2.b M2.c
end

let () =
  M1.do_it ();
  M2.do_it ();
  M3.do_it ()
...