扩展插件体系结构中的类型

时间:2012-02-22 13:41:19

标签: types functional-programming ocaml

现在,我有一个用OCaml编写的工作HTML模板系统。一般设计是单个模板是应用于以下模块类型的仿函数返回的模块:

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

然而,由于两个原因,这种方法仍然不能令人满意,我正在寻找一种更好的模式来解决它们。

第一个问题是这种方法仍然需要我更改模板定义代码(我仍然需要应用WithPermalink仿函数)。我想要一个解决方案,在Post模块中非侵入式地执行向模板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 "" 

getter,setter和初始化的转换相当简单,并使用字段实际上是镜头的事实。

您是否看到任何潜在的设计问题或此方法的可能扩展?

1 个答案:

答案 0 :(得分:3)

您要避免的是在您定义的标签周围编写样板。也许您可以使用camlp4自动生成一组标签的模块代码?

修改

您希望能够将方法添加到对象类型。我认为目前不可能。

我所知道的唯一可行方法是使用可识别类型的预处理器。在Haskell中,他们有HaskellTemplate,一种在打字过程中扩展宏的预处理器,具有打字环境的知识。

两年前我为OCaml编写了一个等效的原型,它工作得很好,对于ocaml-3.12.0它是可达的here,有一些基本的例子。但要做你想做的事,你需要了解OCaml AST,并能够从前一个生成一个新的AST(目前没有引用可以轻松生成AST)。