silverchain
silverchain copied to clipboard
Feature request: multiplication syntax
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.
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 :)
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.)
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 withnew String[] {"a", "b", "c"}
❸ foo(String name) multiply 3
.foo("a").foo("b").foo("c")
actionImpl.foo(String... names);
called once withnew 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 withkeys = new String[] {"k1", "k2", "k3"}
andvalues = 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 becausefoo(String string[5])
feels way too similar tofoo(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.
- Briefly, I also considered mimicking Java's classic for loop:
- 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); }
- Example:
- 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 morefiz()
up o the number ofbaz()
" could be expressed as
(edit: fixed the variable names in the template)for ($B : 1...3) { for ($F : 1...$B) { baz() multiply $B fiz() multiply $F; } }
- 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
- Example: the pattern "1 to 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.
- Example: