AdaCore / uwrap

1 stars 0 forks source link

[RFC] change prefix syntax for fold () and filter () #18

Open QuentinOchem opened 3 years ago

QuentinOchem commented 3 years ago

The current implementation of generator suffixes such as fold () and filter () is currently confusing and has annoying limitations. E.g. in an expression like

   a.b.c ().filter (expr)

a.b denotes an initial it value, c () is the generator to test in the filter. From live discussion, this experimental syntax proves unsuccessful. This RFC proposes two potential reorganization while retaining current semantics.

We need to consider two pieces: (1) the function to call in the filter (2) the initial value of it

For (1), we move that to a parameter. This is still a "magic" lambda in this case, I suggest to discuss lambda in general separately.

For (2) we either keep this (a) as the prefix (which provides more compact code by default) or (b) add another parameter.

The above would be written either (a):

   a.b.filter (c(), expr)

or (b):

   filter (a.b, c(), expr)

The p_base_types test can now be written without the use of an intermediate function, showing below the version with and without many, either (a):

match filter (p_base_types ().foreach (), many (AdaNode (), 2))
weave standard.out (@ & it & " has an ascendance depth of at least two\n");

match filter (p_base_types ().foreach (), AdaNode () \ AdaNode ())
weave standard.out (@ & it & " has an ascendance depth of at least two\n");

Or (b):

match filter (it, p_base_types ().foreach (), many (AdaNode (), 2))
weave standard.out (@ & it & " has an ascendance depth of at least two\n");

match filter (it, p_base_types ().foreach (), AdaNode () \ AdaNode ())
weave standard.out (@ & it & " has an ascendance depth of at least two\n");

Note that composing filters is still possible. Something that would have been written:

   a.b.c ().filter (expr1).filter (expr2)

Can now be written (a):

   a.b.filter (filter (c (), expr1), expr2)

or (b):

   filter (a.b, filter (it, c (), expr1), expr2)

fold will follow the exact same changes.

In terms of implementation, either change is trivial, they're just about modifying where to find the various pieces in the code handling fold and filter.

raph-amiard commented 3 years ago
match filter (it, p_base_types ().foreach (), many (AdaNode (), 2))
weave standard.out (@ & it & " has an ascendance depth of at least two\n");

For me there are still at least two really strange bits here:

  1. p_base_types ().foreach () is an expression, with a property call and a foreach. You expect that to be called exactly once when the filter is evaluated. Here it's not the case. In some way you could say that it's much more akin to a lambda. I would like to read either of the following:
# Property ref version
match filter (it, p_base_types, ...)

# Lambda version
match filter (it, node => node.p_base_types().foreach(), ...)

Also ideally (as discussed before) the foreach would not be needed, and node => node.p_base_types() and p_base_type would be equivalent.

  1. Less important but still a bit concerning for me (and that's very pervasive in uwrap right now): The use of parens for built-ins that looks like functions. For me that'd be OK if they were built-in that could eventually be implemented by functions. But here I'm not sure it's the case. Instead it looks like a fundamental building block of the language, and indeed it looks like uwrap's top level loop is almost a big filter with child() as the traversal function and every match accumulated in an or. So let's imagine instead:
# many version
match
    query it 
    through p_base_types
    match many (AdaNode (), 2)

# / version
match
    query it 
    through p_base_types
    match AdaNode() / AdaNode()

You could imagine that in a regular child based filter, you can omit the through, so it could look like

query it
match many (AdaNode(), 2)

I decided to name filter query above. Not sure either is perfect. traverse could be a better choice, and we could still have fold with a similar syntax I guess.

Aside: I had a big discussion with Romain yesterday about DSLs and how to make them intuitive. For me, after a big chunk of experience, a key principle is to start with a minimal but complete general purpose language, with functions, expressions, etc (you can often omit side effects in a DSL), and then, make the DSLish bit easily understandable in the context of that general purpose language. Else you very quickly end up with something that is very hard to understand for outsiders.