fmease / lushui

The reference compiler of the Lushui programming language
Apache License 2.0
7 stars 0 forks source link

More ergonomic record fields #42

Open fmease opened 3 years ago

fmease commented 3 years ago

Depends on/relates to #18.

Instead of the very verbose field projections which are namespaced under record constructors which in turn are namespaced under data types, utilize type-directed namespaces in the language.

This proposal adds a new syntactic and semantic form called a (field) projection or field access. Its grammar rule is Naked-Lower-Expression "::" Identifier (or # instead of ::) and it is a lower expression. Examples: some.path.to::field_, 0::field_ (ill-typed), @A(@B hello)::field_.

Marking parameters of constructors as fields with the reserved symbol :: makes this projection available for values created with this constructor. Example:

data Person: Type of
    person
        (::name: Text)
        (::age: Nat): Person

jane: Person = Person.person (name = "Jane") (age = 52)
jane-name: Text = jane::name

Counter-example:

;; `jane` from above
x: Nat = jane::afe ;; error: ~ `Person`/`person` does not have such a field
y: Nat = 0::identity ;; error: ~ `Nat` is not even a record, duh!

Initially, I did not want to add this feature (because of "m i n i m a l i s m") but I recognized that the current way of doing this is horrendous! I really like that we use a different symbol for this instead of "re-using" . for "static" name resolution. This allows us to provide better error messages for both invalid uses of . and ::.

The question remains if we should remove the old way of accessing fields or keep it as an alternative. I think we should throw it away as we cannot lower either one to the other, meaning more unnecessary complexity.

For reference, the current design dictates generating functions namespaced under the constructor. Taking the example from above, there would exist Person.person.name of type Person -> Text and Person.person.age of type Person -> Nat. So you'd have to write (or use) Person.person.name jane for jane::name.

The advantage of the previous field projection design was that it enabled point-free style. Now, we need to write an explicit lambda to turn the projection into a first-class function: (\p => p::name) of type Person -> Text. That's not a big problem however: Just provide more syntactic sugar: (::field_) to (\x => x::field_).

Example:

people: List Person = [(Person "Jane" 43) (Person "Jack" 33) (Person "Jim" 80)]
names: List Text = core.list.map (::name) people ;; ["Jane" "Jack" "Jim"]
fmease commented 3 years ago

One thing to re-consider is the grammar: It might be worthwhile to parse field access as something "higher" than lower expressions because requiring a Naked-Lower-Expression instead of Lower-Expression is odd and we'd need to parse it separately anyways (the parser for lower expressions automatically parses attributes i.e. no naked lower expressions).

The given example @A(@B hello)::field_ attaches @B to the target and @A to the lower expression field access. Weird. With a different grammar, it might look like this: @A (@B hello::field_) which is also kinda strange but at least consistent and not an edge case anymore. But thinking about this, that one is even less intuitive. Hmm :/

fmease commented 3 years ago

Proposal accepted. Parsing implemented in 57fec137519eb98a443b75ba8c2c738f56141cc3.