Victor Nicollet
Victor Nicollet

Reputation: 24577

Extending types in plugin architectures

Right now, I have a working HTML template system written in OCaml. The general design is that an individual template is a module returned by a functor applied to the following module type:

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

For example, rendering a blog post would be based on this:

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)

It is more complex that just having a t -> (string * string) list function that extracts all possible values, but it ensures during initialization that all the required template variables are provided.

Adding a new field, such as permalink, is trivial but involves manually editing the code. I am trying to move away from this process and towards a situation where anything permalink-related throughout the application is cleanly compartimented away in a module and merely applied wherever it should be used.

This led me initially to a decorator pattern along the lines of:

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

However, this approach is still unsatisfying for two reasons, and I am looking for a better pattern to solve them both.

The first problem is that this approach still requires me to change the template definition code (I still have to apply the WithPermalink functor). I would like a solution where adding the permalink to template Post would be performed non-intrusively by the Permalink module (this will probably involve implementing some sort of generic extensibility of the template system).

The second problem is that if I need to apply several such functors (having a date, tags, comments...) then the order in which they are applied becomes relevant to the data type and thus to any code using it. This does not prevent code from working, but it's frustrating to have a by definition commutative operation be non-commutative in its implementation.

How can I achieve this ?

EDIT

After giving the subject more thought, I've settled on an extensible-object design. This is how I expect it would look after some preprocessor beautification :

(* 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) %}

The implementation would be fairly standard : when an extensible type post is defined, create a ExtenderImplementation_post module at that spot with this kind of code :

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

Then, defining a field link : string = "" is translated to :

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

The translation of getters, setters and initialization are fairly straightforward, and use the fact that fields are actually lenses.

Do you see any potential design issues or possible extensions to this approach ?

Upvotes: 5

Views: 160

Answers (1)

Fabrice Le Fessant
Fabrice Le Fessant

Reputation: 4274

What you want to avoid is writing the boilerplate around the labels you are defining. Maybe you could just use camlp4 to automatically generate the module code for a set of labels ?

Edit

You want to be able to add a method to an object type. I don't think it is currently possible.

The only possible way I am aware of is to use a pre-processor that would be type-aware. In Haskell, they have HaskellTemplate, a pre-processor that expand macros during typing, with the knowledge of the typing environment.

I wrote a prototype of the equivalent for OCaml two years ago, it was working well, it is reachable here for ocaml-3.12.0, with some basic examples. But to do what you want, you need to understand the OCaml AST and be able to generate a new AST from the previous one (there are currently no quotations to easily generate ASTs).

Upvotes: 3

Related Questions