Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature request: multiplication syntax #73

Open
bannmann opened this issue Nov 1, 2021 · 3 comments
Open

Feature request: multiplication syntax #73

bannmann opened this issue Nov 1, 2021 · 3 comments

Comments

@bannmann
Copy link
Contributor

bannmann commented Nov 1, 2021

Part 1 of 4

Sometimes, a fluent API has methods where the number of parameters of a method matches those of a another method called earlier in the chain:

AggregateBuilder
{
    Aggregate
    properties(
        String nameA,
        String nameB
    )
    withValues(
        Object valueA,
        Object valueB
    )
    ;

    Aggregate
    properties(
        String nameA,
        String nameB,
        String nameC
    )
    withValues(
        Object valueA,
        Object valueB,
        Object valueC
    )
    ;
}

In the action class, one can then direct the overloads of each method to the same private varargs method:

@Override
public void properties(String nameA, String nameB)
{
    setProperties(nameA, nameB);
}

@Override
public void properties(String nameA, String nameB, String nameC)
{
    setProperties(nameA, nameB, nameC);
}

private void setProperties(String... names)
{
    // implementation
}

@Override
public Aggregate withValues(Object valueA, Object valueB)
{
    return build(valueA, valueB);
}

@Override
public Aggregate withValues(Object valueA, Object valueB, Object valueC)
{
    return build(valueA, valueB, valueC);
}

private Aggregate build(Object... values)
{
    // implementation
}

Note: with its 2 to 3 parameters, this example is a shortened version of my original use case, an API that offers overloads with 2 to 8 parameters.

While the pattern works fine, it is quite repetitious both inside the AG and in Java, and the average Silverchain user will probably not come up with it on their own.

Obviously, Silverchain could offer a more compact way to achieve the same result. I imagine a parameter multiplication syntax which may look roughly as follows:

AggregateBuilder
{
    Aggregate
    $N=[2,3]
    properties($N × String name)
    withValues($N × Object value);
}

(Note the use of Unicode × instead of ASCII *. Although the parser could probably use * as well and distinguish the two meanings, I fear that humans might have trouble if it did.)

Silverchain would create the same chain interfaces/classes as in the manual version above, but all overloads of each method (properties() or withValues()) would use the same varargs method in the action class:

@Override
public void properties(String... names)
{
    // ...
}

@Override
public Aggregate withValues(Object... values)
{
    // ...
}

The beauty of this is obviously that I can change the $N=[2,3] to $N=[2,8] or even $N=[2,20] without having to add any boilerplate AG or Java code - it all stays the same.

Part 2 of 4

While writing this, I suddenly thought "why limit this feature to method calls that each have N parameters? Why don't we also allow N successive method calls?" Granted, I don't have a real-world use case for this (my AggregateBuilder quite intentionally always works via a two-method chain) - but maybe it is something that's worth pursuing.

So we could generalize the idea of parameter multiplication syntax to expression multiplication syntax, and also allow using it for method calls as follows:

BazBuilder
{
    Baz
    $N=[1,10]
    $N × initColumn(Object columnName)
    (
        addRow()
        $N × setCell(Object value, Color background);
    )+
    build()
}
@Override
public void initColumn(String... columnName)
{
    // ...
}

@Override
public void setCell(Object... value, Color... background)
{
    // ...
}

Part 3 of 4

If you didn't guess it yet, I see no reason why Silverchain should restrict each chain to have exactly one multiplier. Maybe there are use cases for having a chain with $I=[1,6] $J=[2,3].

Part 4 of 4

The variants from Part 1 and 2 (multiplying parameters & method calls) could even be mixed in the same chain: when setColumns() was called with N parameters, one has to call setCell() N times.

BazBuilder2
{
    Baz
    $N=[1,10]
    setColumns($N × Object columnName)
    (
        addRow()
        $N × setCell(Object value, Color background);
    )+
    build()
}

Finals

So, what do you think about this, do you like it? Does it look useful to you, as well? And maybe the most important question: can all this be implemented (and with reasonable effort)?

I'm the first to admit that especially Parts 2 to 4 look a bit over the top. But then again, Silverchain offering powerful features like this may be exactly the thing that inspires developers to even attempt to create much richer fluent APIs than usual, with less effort.

@tomokinakamaru
Copy link
Owner

This proposal is really interesting! I believe this feature will inspire library developers as you say. I have never thought of such a feature, but it is in line with the original purpose of Silverchain (= reducing developers' effort).

However, two problems need to be solved before starting its implementation:

Syntax: While I like the parameter/expression multiplication feature, it was a little hard for me to understand their notation. I don't like to disagree without an alternative, but I haven't come up with a concrete idea yet. (I am thinking of using for-loop-like syntax without Unicode characters.)

Current code quality: To be honest, the code quality is not good (not easy to safely extend 😱). I assume that more features will be proposed/implemented in the future so that developers can quickly create rich fluent APIs. To support those features smoothly, I want to refactor the code first.

So, I'd like to continue looking for a better syntax of the multiplication feature for a while (but not so long), while refactoring Silverchain. How about this plan? If the priority of the multiplication feature is high, I will add ad-hoc implementation quickly :)

@bannmann
Copy link
Contributor Author

bannmann commented Nov 7, 2021

Thanks a lot! I'm glad that you like the idea. 😃

I'd like to continue looking for a better syntax of the multiplication feature for a while (but not so long), while refactoring Silverchain. How about this plan?

Sounds great! Compared to the other features so far, multiplication is not a priority. It certainly doesn't require rushing things (maybe nothing ever does, TBH). Feel free to concentrate on refactoring and other improvements first!

Syntax: (...) it was a little hard for me to understand their notation. (...) I am thinking of using for-loop-like syntax without Unicode characters.

I think multiplication is the most complicated feature so far, so it's fitting that it is tough to get the syntax right: it needs to be flexible, but still very easy to understand. A for-loop-like approach (or relying on keywords in general) could really help with that.

I will think about this for a while and post my thoughts. (To avoid ambiguities, I'll probably include examples which consist of proposed AG syntax together with current AG code that achieves the same effect.)

@bannmann
Copy link
Contributor Author

bannmann commented Nov 27, 2021

I think I have come up with a syntax and specification that could work. To explain it, I have split it into two distinct features.

Feature 1: Multiplication

This feature does not "loop" through anything, it just duplicates elements in the AG and changes how the action class is invoked. Here is the pseudo grammar:

<parameter-or-method> "multiply" <number_literal-or-variable>
  • multiply 7 can be thought of as "pretend I wrote the previous thing 7 times, but call the action method only once with an array of 7 elements"
  • Examples:
    AG Code Required API Usage Action Invocation
    foo(String name)[3] .foo("a").foo("b").foo("c") actionImpl.foo(String name); called three times
    foo(String name multiply 3) .foo("a", "b", "c") actionImpl.foo(String... names); called once with new String[] {"a", "b", "c"}
    foo(String name) multiply 3 .foo("a").foo("b").foo("c") actionImpl.foo(String... names); called once with new String[] {"a", "b", "c"}
    put(Object key, Object value) multiply 3 .put("k1", "v1").put("k2", "v2").put("k3", "v3") actionImpl.put(Object... keys, Object... values); called once with keys = new String[] {"k1", "k2", "k3"} and values = new String[] {"v1", "v2", "v3"}
  • ❶ is the existing repetition syntax for comparison
  • ❷ is the primary motivation for implementing the multiplication feature: reducing boilerplate if I want several similar parameters. The API user calls the method once and the method of the action class is also called once.
  • ❸ is for the case where the fluent API should consist of several successive method calls, but the API developer wants only one invocation of the action class method (using the same signature as for ❷).
  • ❹ demonstrates how to combine several parameters when the method is multiplied.

Feature 2: Template Blocks

While the multiplication feature above already saves boilerplate on its own, it gets even better when used together with this feature. Template Blocks could be used on their own without multiplication, but I don't have an example for that, so I'll show the combination of both features.

However, it's important to note that Template Blocks do not change anything regarding the action method calls and can be thought of to operate purely as a preprocessor.

Here's the pseudo grammar:

"for" "(" <variable> ":" <range> ")" "{" <template> "}"
  • <variable> has the same syntax as a fragment name.
  • <range> specifies the values that <variable> takes using a starting and an ending integer, e.g. 2...8. For each number in that range, the <template> is "evaluated" once.
  • <template> consists of one or more expressions.
  • Example:
    for ($N : 2...8)
    {
        Aggregate properties(String name multiply $N) withValues(Object value multiply $N);
    }
    
  • This is strictly equivalent to the following AG:
    Aggregate properties(String name multiply 2) withValues(Object value multiply 2);
    Aggregate properties(String name multiply 3) withValues(Object value multiply 3);
    Aggregate properties(String name multiply 4) withValues(Object value multiply 4);
    Aggregate properties(String name multiply 5) withValues(Object value multiply 5);
    Aggregate properties(String name multiply 6) withValues(Object value multiply 6);
    Aggregate properties(String name multiply 7) withValues(Object value multiply 7);
    Aggregate properties(String name multiply 8) withValues(Object value multiply 8);
    

Design Notes

  • Instead of multiply I considered the [n] syntax from the method call repeat operator. However, I decided to not use that because foo(String string[5]) feels way too similar to foo(String string[]) which is a legal (albeit uncommon) Java syntax for a String array method parameter.
  • With the use of the colon (:), the template block syntax intentionally mimicks Java's extended for loop. To me, that is a good fit because both features are about "for each of these, do that".
    • Briefly, I also considered mimicking Java's classic for loop: for ($I = 2; $I <=8; $I++). However, that would imply the ability to have real Java logic in there. Also, the classic syntax would not work for one of the potential features shown below.
  • I'm a tiny bit concerned that the use of braces for template blocks may cause user confusion: would somebody mix up the braces with the existing feature for unordered rules?
    • Counterpoint: the AG language will likely make more use of keywords in the future. If unordered rules got an explicit keyword that goes in front of the braced block, users would be less likely to think of braces as the defining thing and therefore not memorize "braces = unordered rules".

Future Possibilities

An early indication that the syntax proposed above might be a good choice is that several other features (which I would consider outside the scope of a "minimum viable product" of this feature) seem to fit nicely.

Disclaimer: most examples below are totally made up, so don't reject a feature just because its example usage feels unrealistic or uncommon.

  • A <range> could use a fragment for one or both ends (e.g. if I want to reuse $MAX_PARAMETER_COUNT = 10; for several different template blocks).
  • A <range> could be non-numeric and simply list alternative text/tokens/expressions. Though not as impressive as use with numerical ranges, it could reduce boilerplate for APIs that have several overloads:
    • Example:
      for ($T : Path | File | String)
      {
          load($T file);
          load($T file, Charset charset);
      }
  • Template blocks could be nested, and one or both ends of a <range> could itself be a variable reference.
    • Example: the pattern "1 to 3 baz() followed by one or more fiz() up o the number of baz()" could be expressed as
      for ($B : 1...3) { for ($F : 1...$B) { baz() multiply $B fiz() multiply $F; } }
      
      (edit: fixed the variable names in the template)
    • This is strictly equivalent to this AG:
      baz() fiz();                            // B=1 & F=1
      baz() baz() fiz();                      // B=2 & F=1
      baz() baz() fiz() fiz();                // B=2 & F=2
      baz() baz() baz() fiz();                // B=3 & F=1
      baz() baz() baz() fiz() fiz();          // B=3 & F=2
      baz() baz() baz() fiz() fiz() fiz();    // B=3 & F=3
      
  • By nesting non-numeric ranges, one could create an API that has all permutations of two sets of types:
    • Example:
      for ($INPUT : char[] | String | Reader)
      {
          for ($OUTPUT : Path | File | OutputStream)
          {
              copy($INPUT input, Charset outputCharset, $OUTPUT output);
              copyAsUtf8($INPUT input, $OUTPUT output);
          }
      }
      
    • This is strictly equivalent to this AG (reordering & blank lines for readability):
      copy(char[] input, Charset outputCharset, Path output);
      copy(char[] input, Charset outputCharset, File output);
      copy(char[] input, Charset outputCharset, OutputStream output);
      copyAsUtf8(char[] input, Path output);
      copyAsUtf8(char[] input, File output);
      copyAsUtf8(char[] input, OutputStream output);
      
      copy(String input, Charset outputCharset, Path output);
      copy(String input, Charset outputCharset, File output);
      copy(String input, Charset outputCharset, OutputStream output);
      copyAsUtf8(String input, Path output);
      copyAsUtf8(String input, File output);
      copyAsUtf8(String input, OutputStream output);
      
      copy(Reader input, Charset outputCharset, Path output);
      copy(Reader input, Charset outputCharset, File output);
      copy(Reader input, Charset outputCharset, OutputStream output);
      copyAsUtf8(Reader input, Path output);
      copyAsUtf8(Reader input, File output);
      copyAsUtf8(Reader input, OutputStream output);
      
    • Note: As a separator between alternatives, I used the pipe (|) to mimick java's multi-catch blocks. Using commas (,) might work equally well. However, given the fact that in an AG file, a comma is passed as-is to Java and never influences Silverchain logic (unlike |), a pipe feels more "native" to AG.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants