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