Расширение типов в архитектурах плагинов - PullRequest
5 голосов
/ 22 февраля 2012

Сейчас у меня есть работающая система шаблонов 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 -> (string * string) list, которая извлекает все возможные значения, но при инициализации обеспечивает предоставление всех необходимых переменных шаблона.

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

Это привело меня первоначально к шаблону декоратора в соответствии с:

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

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

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

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

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

EDIT

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

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

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

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

1 Ответ

3 голосов
/ 22 февраля 2012

Чего вы хотите избежать, так это написания шаблона вокруг ярлыков, которые вы определяете. Может быть, вы могли бы просто использовать camlp4 для автоматической генерации кода модуля для набора меток?

Редактировать

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

Единственный возможный способ, которым я знаю, - это использовать препроцессор, который бы знал тип. В Haskell у них есть HaskellTemplate, препроцессор, который расширяет макросы во время набора с учетом среды ввода.

Я написал прототип аналога для OCaml два года назад, он работал хорошо, он доступен здесь для ocaml-3.12.0, с некоторыми базовыми примерами. Но чтобы делать то, что вы хотите, вы должны понимать OCaml AST и иметь возможность генерировать новый AST из предыдущего (в настоящее время нет цитат, которые бы легко генерировали AST).

...