Syntax proposal: optional record fields

ReScript is implementing[1] what I think is a cool feature that could be useful in upstream OCaml too: optional record fields.

E.g., say we have the following record type:

type t = { x : int; y : int option }

Now, if we want to create a value with y = None, we have to write:

{ x = 1; y = None }

But what if we could just write:

{ x = 1 }

This would also mirror current destructuring syntax, and be conceptually similar to current syntax for optional arguments.

It would make it a lot easier to instantiate records with lots of missing fields e.g. in JSON objects or other encoding systems.

[1] RFC: More general type checking for structural typings - #51 by Hongbo - ReScript Forum

2 Likes

Personally Iā€™m against this feature, in the same way Iā€™m against { a; _ } if you add a parameter that is optional, you want the typer to notify you across the codebase so that you can verify it.

1 Like

Good point. Then it should probably be opt-in, the same way destructuring is, so e.g. we can construct the record value with {x = 1; _ } to indicate 'I donā€™t care about the optional fields, just set them to None'.

2 Likes

Edit: The objection below is erroneous, it can be ignored.

This feature is not decidable: consider a type defined by

type 'a t = { key:('a,'b) gadt option; value:'b }
let x = {  }

Then the second line is only valid if the type scheme (_,_ option) gadt is inhabited which is undecidable in the general case.

I would find that syntax a bit confusing for reading, you would never know the exact structure of the value without going to the definition.

Also somehow you have that one function away.

module M = struct 
  type t = { x : int; y : int option }

  let v ?y ~x () = { x; y };;
end
6 Likes

+1. And if you want to prevent people from constructing t directly, make it private. This way you can safely add new optional fields (well pattern matching can still break but thatā€™s expected).

1 Like

Good point but in certain scenarios defining this function would be cumbersome e.g. modelling complex schemas with lots of record types.

PPX can help here, e.g., this ppx_deriving plugin or this standalone.

1 Like

On the other hand, PPX adds complexity, build time costs, maintenance costs. A small bit of syntax sugar (I see it very similar to letop-punning) would remove that burden from the community.

I never really understood the attraction of letop punning (in the let* x in ... sense). That seemed to me to represent more cognitive load when trying to understand code, shortening the code by a few letters with little practical benefit to be measured against that. On the latest proposal, what is the practical gain in being able to write { x = 1 } instead of { x = 1; y = None }? It seems like another special rule to remember (and check for) when reading and modifying code.

2 Likes

All valid points. Thatā€™s why I think some kind of built-in deriving mechanism is the best answer to this particular problem (granted, it will not be a ā€œsmallā€ extension, but itā€™s in line with some of the things the dev team are considering).

1 Like

I agree - I think in the issue/pull request in which it was introduced there was a reference to a specific style of coding/codebase where this kind of simplification would be particularly useful, but in the general case, this seems like unecassary overhead when reading OCaml code.

To me, it feels like the punning, and this syntax proposal are both symptoms of an underlying problem, which is the lack of an easy to use macro system[1], and as such, domain-specific niceties are being pushed into the language (which I guess will probably lead to a C++ - style kitchen-sink scenario in the long term).

[1] - PPXs are useful, but unwieldy to write, even with the helpful ppxlib libraries.

2 Likes

Not much if the record type has just two fields, but potentially a lot more if there are many optional fields, e.g. when dealing with a backward-compatible schema that added many optional fields over time, that should default to None. It would save a ton of keyboarding to say just { x = 1; _ }. Itā€™s a pragmatic consideration.

Punning exists in many parts of OCaml syntax already, and they are super convenient for day-to-day work, so I donā€™t think my proposal is such a huge deal as to warrant an analysis of underlying problems :slight_smile:

1 Like

To be clear, the issue was not with punning in general, but let-punning as recently introduced, which feels like a more domain specific syntactic sugar.

Maybe it just feels a bit ad-hoc to me because it breaks a lot of mental parsing to have let-bindings without an an associated expression, wheras all the other cases of punning are more coherent with the OCaml syntax.

Maybe so. I guess I just wanted to point out a trend Iā€™ve been seeing; death by a thousand cuts and all that~

1 Like

I think ppx deriving (make) GitHub - ocaml-ppx/ppx_deriving: Type-driven code generation for OCaml

Is very close to this when you consider default value annotations.

This also extends beyond the option type. I suspect it will quickly be a friction/annoyance that option types have short hand syntax support and other types do not.

2 Likes

An existing syntax is very close from what you want by copying an record and overloading only the required fields:

type t = { x : int; y : int option }
let default = { x = 0; y = None }

let record = { default with x = 1 }
8 Likes

Thanks. ppx_make was suggested earlier, however as I said, adding any PPX must be weighed against its maintenance and build time burden.

I suspect it will quickly be a friction/annoyance that option types have short hand syntax support and other types do not.

Itā€™s already well established in OCaml that option types have shorthand syntax, e.g. optional parameters.

Thanks for the suggestion. This is a nice approach but I think it suffers from two issues, first, as I said earlier it means manual keyboarding to set up the default values, and second, it looks like it would be too easy to forget to override the default non-optional fields. A syntax like a ā€˜makeā€™ function or my suggestion would not have that issue.

This all sounds good until you have a very large code base re-using the same record all across the board. At this point, you learn to appreciate the implicit extensivity that records can bring, for instance adding an extra data point used only in very specific part of your code base without having to needlessly touch millions of other lines of code which, even if it could be done automatically, eventually conflicts with multiple other on-going changes and PRs for no good reason.

2 Likes

This is a thing that I really think should be avoided, in general I really avoid having types crossing the library barrier even in internal libraries, unless those records ā€œareā€ the API, which means they should not be used internally.

for instance adding an extra data point used only in very specific part of your code base without having to needlessly touch millions of other lines of code which

If the record is being routed across the entirety of the codebase which again it really smells to me, if performance is not needed this should just not happen and if performance is needed then this is gonna be bad anyway as your runtime will need to carry a lot more information.

And if itā€™s not I would say, just duplicate the type and write a mapping function on it on the entry of the library and on the output of it.