Расширение типов в архитектурах плагинов

Прямо сейчас у меня есть рабочая система шаблонов HTML, написанная на OCaml.Общий дизайн состоит в том, что отдельный шаблон - это модуль, возвращаемый функтором, применяемым к следующему типу модуля:

module type TEMPLATE_DEF = sig
  type t (* The type of the data rendered by the template. *)
  val source : string (* Where the HTML template itself resides. *)
  val mapping : (string * (t -> string)) list 
end

Например, рендеринг сообщения в блоге будет основываться на этом:

module Post = Loader.Html(struct
  type t = < body : string ; title : string >
  let source  = ...
  let mapping = [ "body", (fun x -> x#body) ; "title", (fun x -> x#title) ]
end)

Это более сложно, чем просто наличие t -> (строка * строка) list функция, которая извлекает все возможные значения, но при инициализации обеспечивает предоставление всех необходимых переменных шаблона.

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

Изначально это привело меня к шаблону декоратора следующего вида:

module WithPermalink = functor(Def:TEMPLATE_DEF) -> struct
  type t = < permalink : string ; inner : Def.t >
  let source = Def.source 
  let mapping =
    ( "permalink", (fun x -> x # permalink) ) 
    :: List.map (fun (k,f) -> (k, (fun x -> f (x#inner)))) Def.mapping 
end

Однако этот подход все еще неудовлетворителен по двум причинам, и я ищу лучший шаблон для решения обеих проблем.

Первая проблема заключается в том, что этот подход по-прежнему требует от меня изменения кода определения шаблона (мне все еще нужно применить функтор WithPermalink ). Мне нужно решение, в котором добавление постоянной ссылки в шаблон Сообщение будет выполняться ненавязчиво модулем Permalink (это, вероятно, потребует реализации какой-то общей расширяемости системы шаблонов) .

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

Как я могу этого добиться?

РЕДАКТИРОВАТЬ

Поразмыслив над этим предметом, я остановился на дизайне расширяемых объектов. Вот как я ожидаю, что это будет выглядеть после некоторого улучшения препроцессора:

(* Module Post *)
type post = {%
  title : string = "" ;
  body  : string = "" 
%}   

let mapping : (string * (post -> string)) list ref = 
  [ "title", (%title) ;
    "body",  (%body) ]

(* Module Permalink *)
type %extend Post.post = {% 
  link : string = "" 
%}

Post.mapping := ("permalink", (%link)) :: !Post.mapping

(* Defining the template *)
module BlogPost = Loader.Html(struct
  type t = Post.post
  let source = ...
  let mapping _ = !Post.mapping
end)

(* Creating and editing a post *)
let post = {% new Post.post with 
  Post.title     = get_title () ;
  Post.body      = get_body () ;
  Permalink.link = get_permalink () ; 
%}

let post' = {% post with title = BatString.strip (post % Post.title) %}

Реализация будет довольно стандартной: когда определен расширяемый тип post , создайте модуль ExtenderImplementation_post в этом месте с помощью такого рода кода:

module ExtenderImplementation_post : sig
  type t 
  val field : 'a -> (t,'a) lens
  val create : unit -> t
end = struct
  type t = (unit -> unit) array
  let fields : t ref = ref [| |]
  let field default =
    let store = ref None in
    let ctor () = store := Some default in
    let n = Array.length !fields in
    fields := Array.init (n+1) (fun i -> if i = n then ctor else (!fields).(i)) ;
    { lens_get = (fun (t:t) -> t.(n) () ; match !store with
      | None   -> assert false
      | Some s -> store := None ; s) ;
      lens_set = (fun x (t:t) -> let t' = Array.copy t in
                            t'.(n) <- (fun () -> store := Some x) ; t') }
  let create () = !fields
end
type post = ExtenderImplementation_post.t

Затем определение поля link: string = "" преобразуется в:

let link : (Post.post,string) lens = Post.ExtenderImplementation_post.extend "" 

Перевод методов получения, установки и инициализации довольно прост и используется тот факт, что поля на самом деле линзы.

Видите ли вы какие-либо потенциальные проблемы при проектировании или возможные расширения этого подхода?

5
задан Fabrice Le Fessant 24 February 2012 в 12:31
поделиться