Version française
Home     About     Download     Resources     Contact us    
Browse thread
[Caml-list] record labels of record scope using camlp4
[ Home ] [ Index: by date | by threads ]
[ Search: ]

[ Message by date: previous | next ] [ Message in thread: previous | next ] [ Thread: previous | next ]
Date: -- (:)
From: Jeff Henrikson <jehenrik@y...>
Subject: [Caml-list] record labels of record scope using camlp4
It seems like a lot of people dislike the fact that record labels have module scope instead of record scope.  Looking at the
identifier style in the ocaml source we see the symmetry behind this design choice:

<from parsing/parsetree.mli>
(* Type expressions for the core language *)

type core_type =
  { ptyp_desc: core_type_desc;
    ptyp_loc: Location.t }

and core_type_desc =
    Ptyp_any
  | Ptyp_var of string
  | Ptyp_arrow of label * core_type * core_type
  | Ptyp_tuple of core_type list
  | Ptyp_constr of Longident.t * core_type list
  | Ptyp_object of core_field_type list
  | Ptyp_class of Longident.t * core_type list * label list
  | Ptyp_alias of core_type * string
  | Ptyp_variant of row_field list * bool * label list option

and core_field_type =
  { pfield_desc: core_field_desc;
    pfield_loc: Location.t }


I.e. an abbreviated type always prefixes constructors or labels.  But I find this annoying when records get long, since to type a
pattern for a record with n entries requires specifying the type n times!  It is even tempting to accept the incentive of creating
short type abbreviations to resist typing a long identifier n times.  And eliminating the type from the labels entirely doesn't
work if you have lots of fields called generic things like "name" and "address".

On the other hand, I wouldn't have a huge problem with this if it were all I had to deal with was:

 var.Type_label

*So* I took the chance to learn some camlp4 and wrote a macro that implements declarations and patterns having record-local scope
like this:

  type person = LOCAL {name:string; addr: string};;
  let p = Person{name="Joe";addr="1 Broadway Ave"};;
  match p with
    Person{name=n;addr=a} -> (n,a);;

It does this by prepending the name of the type onto the front of the identifiers and then automatically adding them back on the
patterns, that is:

  type person = { person_name : string; person_addr : string; }

so of course you are still stuck with:

  p.person_name

but it seems a small price to pay.  The extension properly handles recursive declarations, eg:

type person = LOCAL {name:string; addr:string; friends:plist}
and plist = Plist_person of (person * plist) | List_End;;


_The bad news_

Okay, so there are some problems.

First bad news: the code runs great in the toplevel, but that's because camlp4 extends grammars interactively there.  Apparently in
batch compilation, any EXTEND construct only takes effect at the end of the file, not at the end of a statement.  I remember
reading this in the manual somewhere, though I don't seem to be able to find it now.  And every time you use the LOCAL syntax
above, it defines a new macro for the constructor.  So the macro won't be visible until the file is done parsing.

A workaround might be to stuff all typedefs in one file and then #load them into another.  I'd have to think about the
consequences.  It smells funny.

Second bad news: the scoping isn't quite right.  The types will be in module scope, but their constructor macros will be global.
The good news is that the macros themselves don't know about the members of the types at all.  They just syntactically go pasting
stuff onto them.  So concievably if you had two records with the same names in different modules, nothing bad would happen.  Again,
it smells funny.


_To Do_

Besides working out the practical problems above, I'd like to see more symmetry between function labels and record labels, eg
default values.  Labels are great if you are creating an API to be called, but they do nothing to help if you are creating a
callback API.  As in "Here are a bunch of parameters, you don't have to use/know them all if you don't want."  Adding default
values seems feasible, but it would exacerbate the macro scoping problem.

Here's the source below.  You can evaluate it in the toplevel.  Have fun.


Jeff Henrikson


PS-

> PS> I guess I can understand Xavier, sometimes some people complain
> about so-called bugs which are in facts features, and for features
> such as the ``cannot use record labels as I would have wished to'' it
> is just too often.

Apparently camlp4 has, or nearly has, the power to supersede such quibbling.  :-)


----------------------------------------------------------------


#load "camlp4o.cma";;    (* toploop only *)

#load "q_MLast.cmo";;
#load "pa_extend.cmo";;

open Pcaml;;

let loc = (0,0);;        (* toploop only *)

let prepend_id name labels =
  let helper2 v =
    match v with
    <:patt<$lid:x$>> ->
      let y = name ^ "_" ^ x in
      <:patt<$lid:y$>>
  | _ -> v in
  let helper1 = fun
    (patt,expr) -> (helper2 patt,expr) in
  List.map helper1 labels;;



let make_record_constructor name =
  let lname = String.uncapitalize name in
  let uname = String.capitalize name in
  let lbl_expr_list =
    (Obj.magic (Grammar.Entry.find expr "lbl_expr_list") :
       (MLast.patt * MLast.expr) list Grammar.Entry.e) in
  let lbl_patt_list =
    (Obj.magic (Grammar.Entry.find patt "lbl_patt_list") :
       (MLast.patt * MLast.patt) list Grammar.Entry.e) in
  EXTEND
    expr: LEVEL "simple"
    [ [ $uname$; "{"; memb = LIST1 lbl_expr_list SEP ";" ; "}" ->
          (let memb0= prepend_id lname (List.hd memb) in
          <:expr<{$list:memb0$}>>) ] ];
    patt: LEVEL "simple"
    [ [ $uname$; "{"; memb = LIST1 lbl_patt_list SEP ";" ; "}" ->
          (let memb0= prepend_id lname (List.hd memb) in
          <:patt<{$list:memb0$}>>) ] ];
  END;
;;

(* test:
make_record_constructor "bogus";;

type bogus = {bogus_foo:string;bogus_bar:string};;

Bogus{foo="happy";bar="sad"};;
*)

let type_declaration =
    (Obj.magic (Grammar.Entry.find str_item "type_declaration") :
       MLast.type_decl Grammar.Entry.e);;
let type_parameters =
    (Obj.magic (Grammar.Entry.find type_declaration "type_parameters") :
       ((string * (bool * bool)) list) Grammar.Entry.e);;
let type_patt =
    (Obj.magic (Grammar.Entry.find type_declaration "type_patt") :
       (MLast.loc * string) Grammar.Entry.e);;
let type_kind =
    (Obj.magic (Grammar.Entry.find type_declaration "type_kind") :
        MLast.ctyp Grammar.Entry.e);;
let constrain =
    (Obj.magic (Grammar.Entry.find type_declaration "constrain") :
        ((MLast.ctyp * MLast.ctyp)) Grammar.Entry.e);;


let prepend_id_t name tk =
  let helper = fun
    (loc,s,b,t) -> (loc,name ^ "_" ^ s,b,t) in
  match tk with
    <:ctyp< { $list:ldl$ }>> ->
      let ldl2 = List.map helper ldl in <:ctyp< { $list:ldl2$ }>>
  | _ -> tk;;



EXTEND
  type_declaration:
    [ [ tpl = type_parameters; n = type_patt; "="; "LOCAL"; tk = type_kind;
        cl = LIST0 constrain ->
	  match n with (loc,s) ->
	    make_record_constructor s;
	    (n, tpl, prepend_id_t s tk,  cl) ] ]
  ;
END;;


(* example: *)
type person = LOCAL {name:string; addr: string};;
let p = Person{name="Joe";addr="1 Broadway Ave"};;
match p with
  Person{name=n;addr=a} -> (n,a);;


(* this sort of thing works too: *)
type person = LOCAL {name:string; addr:string; friends:plist}
and plist = Plist_person of (person * plist) | List_End;;



-------------------
Bug reports: http://caml.inria.fr/bin/caml-bugs  FAQ: http://caml.inria.fr/FAQ/
To unsubscribe, mail caml-list-request@inria.fr  Archives: http://caml.inria.fr