- Generation of constructors: at the moment, piqic-ocaml only generates default constructors which take no arguments. Typically, when I have defined a type, I define a "create" function which takes all of the record fields as arguments. I've added a --gen-constructors option to piqi which does this for any record type, e.g. for the person type from the address book example, it generates the following code:
let create_person name id email phone =
{
Person.name = name;
Person.id = id;
Person.email = email;
Person.phone = phone;
Person.piqi_unknown_pb = [];
}
- Generation of separate modules for each record type with consistent naming: every type generated by piqi has more or less the same functionality: a generate/parse call (serialization) and a (default) constructor. However, the default function names include the type name, which makes using the generated code as the input module for a functor impractical, as once has to write a adaptor module that maps the function names chosen by piqi to those expected by the functor. I've added a --gen-module-per-record option that adds a module for every record type and has a uniform interface.
E.g. for that same person type, it generates:
module Person =
struct
type t = Person.t
let pickle = gen_person
let unpickle = parse_person
let create = create_person
end
- Generate setters / getters: adds a set and get function for every record field to the module:
module Person =
struct
type t = Person.t
let pickle = gen_person
let unpickle = parse_person
let create = create_person
let name t = t.Person.name
let set_name t value = { (t) with Person.name = value; }
let id t = t.Person.id
let set_id t value = { (t) with Person.id = value; }
let email t = t.Person.email
let set_email t value = { (t) with Person.email = value; }
let phone t = t.Person.phone
let set_phone t value = { (t) with Person.phone = value; }
end
I am aware that some of these changes overlap with the functionality provided by the syntax extension (Module#... style). One of the reasons that I prefer not using a syntax extension is that it doesn't seem to be playing well with the Merlin tool that I use extensively when writing Ocaml code.
In addition, I ran into a small bug in piqic-ocaml when using the --pp option, which uses camlp4o to pretty print the generated code. In some cases, it would output a binary AST instead of a ml file. Also, it depends on /bin/sh. I've tried to fix both of these issue on https://github.com/amplidata/piqi/tree/pretty_printing_fix and I'll send a pull request for this.
- Generation of constructors: at the moment, piqic-ocaml only generates default constructors which take no arguments. Typically, when I have defined a type, I define a "create" function which takes all of the record fields as arguments. I've added a --gen-constructors option to piqi which does this for any record type, e.g. for the person type from the address book example, it generates the following code:
let create_person name id email phone =
{
Person.name = name;
Person.id = id;
Person.email = email;
Person.phone = phone;
Person.piqi_unknown_pb = [];
}So far, I've been using default functions for that. For example,let x = Person.default_person () in{x withname = nameid = id...}The approach with defaults seems to be more flexible and robust. First, we can override only those fields that we care about. Second, we can refer them by name instead of relying on the order of fields in the record definition and the total number of fields. Both the order and the number of fields can change over time which will break the code in your case. Besides, it is easy to get confused if there are a lot of positional arguments for large records.I've seen some people using labeled arguments for that. For example, the above example could be expressed aslet x = Person.make_person ~name ~id ...This is a bit more concise compared to using default functions. We could do something like that.
- Generation of separate modules for each record type with consistent naming: every type generated by piqi has more or less the same functionality: a generate/parse call (serialization) and a (default) constructor. However, the default function names include the type name, which makes using the generated code as the input module for a functor impractical, as once has to write a adaptor module that maps the function names chosen by piqi to those expected by the functor. I've added a --gen-module-per-record option that adds a module for every record type and has a uniform interface.
E.g. for that same person type, it generates:
module Person =
struct
type t = Person.t
let pickle = gen_person
let unpickle = parse_person
let create = create_person
endI can see how this can be useful for functors. I'm wondering if it would be reasonable to also generate modules for other non-record user-defined types such as variants?Also, if we decide to do it, I think we should keep naming consistent and use "gen" and "parse" instead of "pickle" and "unpickle".
I've seen some people using labeled arguments for that. For example, the above example could be expressed aslet x = Person.make_person ~name ~id ...This is a bit more concise compared to using default functions. We could do something like that.
One of the main reasons why I prefer to use explicit constructors (i.e. the create or make function) and getter/setter functions is that it allows to hide the inner details of the records. In my use case, there are additional relations between record fields, or constraints on them, that cannot be expressed in the proto/piqi description and which would not be enforced if I let other layers of the application have direct access to the record. In practice, I end up needing a layer on top of the generated code that enforces these constraints and relations. 90% of this interface might just be pass-through to the generated code, so it is of benefit to me that I would not have to write manual forwarding code for this 90%. One can accomplish this by doing an include of the generated module-per-type and then narrowing down the signature, so that only a selected subset of the generated functionality is exposed, in addition to adding a small number of manually written getters/setters/constructors that implement these extra relations and constraints by override the auto-generated ones and by adding a couple of additional functions.
I can imagine that in some other use cases the fields of the record are totally independent though, so I can see the use of the "record field name" based options as well. The explicit constructor/getter/setter functions are probably most useful within the module-per-type approach, so maybe the create/make function should simply be defined within that type specific module and not in the main module?
I am also not a frequent user of the named/optional arguments in Ocaml, but that is more of a personal preference. I haven't run into a lot of cases of getting the arguments for these constructors wrong, but it might indeed be a good option to go this named argument route here.