Named parameters allows calling a function using parameter names with their corresponding values instead of using values at given position. This feature is slightly different from language to language, and some of them lack this feature at all. Among additional behaviors we emphasize the possibility of specifying default parameter values and the possibility to specify named parameters in any order.
Java language lacks named parameters feature, for complicated reasons I will not describe here. I have to say that I do not support this feature in general, or as a default function call behavior. The main reason why I don’t like is that I consider the code legibility will suffer if an entire core base is full of named parameters. A secondary reason is that the freedom and flexibility provided is easy to be abused, leading in long term to methods which are longer, overly complicated and harder to maintain. I might be wrong and my perspective could be shaped by the so long used tools and languages. I can accept that.
Still, there are legitimate cases when the presence of named parameters can help a lot and can shorted the verbosity. Some cases I identified are utility methods for building graphical or textual outputs which allows plenty of customization. This story describes how I implemented named parameters for my pet library for building scientific graphs or textual output.
Named parameter features
Since Java does not provide this feature, there only way to have it is to implement it myself. Since I had to implement it, why not devise some nice to have features for my named parameters?
- A named parameter has to have an expressive name with Java documentation attached
- The method which allows named parameters to allow only parameter types from a given set verified at compile time
- The same parameter to be allowed to receive multiple types of inputs, with different logic (for example a color could be expressed as a specific java.awt.Color instance, as a short color String name, or as an index from a color palette)
- The logic behind a named parameter from a set to have access to other parameter values from the same set (a color parameter can be specified as a color index from a palette and the palette can be also be specified as parameter)
- A named parameter should have a default value and to allow null as a default value
- To be easy to create hierarchies of named parameter sets having at the top the default values and to allow named parameter values overrides at any level (Suppose we have a plot with two figures, and in the left figure a histogram, than to allow specify alpha transparency at plot level, that value to be the value for both subplots, but if the histogram wants to override the value to be allowed)
As an implementation fact, to have some base classes which implements as much as possible of the previous features, letting the programmer to gain his attention entirely on the named parameter logic.
As a spoiler code, just in case you are bored, looks like the following:
Frame df = Datasets.loadLifeScience().mapRows(Mapping.range(2000));
// just two random variables from a random data set, for illustration
Var x = df.rvar(0).name("x");
Var y = df.rvar(1).name("y");
// 3 x 3 grid cells
// 4 top-left cells will contain a scatter
// last row, 2 cells on left side filled with a histogram
// the others are filled as they come, from up to down, then from left to right
var fig = gridLayer(3, 3)
.add(0, 0, 2, 2, points(x, y, sz(3), palette(Palette.tableau21()), pch(2), fill(2)))
.add(2, 1, 2, 1, hist(y, bins(20)))
.add(lines(x))
.add(hist(x, bins(20), fill(9)))
.add(hist2d(x, y, bins(30), fill(Color.BLUE)));
WS.draw(fig);
Please don’t judge my artistic skills, there is nothing to be found there. However, notice in the code sample some highlights from the features I wanted for named parameters. Among them we have:
- color can be specified directly or through a palette index, palette which can be chosen, also
- named parameters are specified in any order, this is maintained also for the order of subplots, unless index is unspecified
- default values exists, those can be overridden at any level: line width specified for all subplots, but customized for one pink histogram
Java implementation details
The main objective is to allow the programmer (me in this case, but you can be also that one) as little effort as possible in a safe way. Regarding safety, I have to admit, I did not achieved everything I wanted. I would wished to avoid a specific cast and force the check at compilation time, but I did not know how to do it.
Anyway, in order to achieve less effort we need some building blocks to contain as much logic as possible. Abstract classes helps with that. We need two of them: one to model the set of named parameters which are part of the same family and another one to model the definition, default values and identity of named parameters. Thus we have two classes: NamedParam and NamedParamSet.
NamedParam type
public abstract class NamedParam<S extends NamedParamSet<S, ?>, V> {
private final String name;
private final SFunction<S, V> fun;
public NamedParam(String name, SFunction<S, V> fun) {
this.name = name;
this.fun = fun;
}
public String getName() {
return name;
}
public V getValue(S s) {
return fun.apply(s);
}
}
First of all we have an abstract class, since anyone in need has to derive from this class to create named parameters for some set. The generic type S describes the named parameter set to which belongs the defined named parameters.
The named parameter contains two pieces. The name is used to identify the parameter. As you can notice later, you cannot create in the same set two named parameters with the same name. The interesting part is the value, which in this case is a function. The value is obtained when needed by calling a function which produces the parameter value using also information from the whole set of parameters. This is needed if we want to produce conditional parameter values.
The generic type V describes the type of the value hold by this parameter.
NamedParamSet type
public abstract class NamedParamSet<S extends NamedParamSet<S, P>, P extends NamedParam<S, ?>> {
protected final Map<String, P> defaults;
protected final Map<String, P> params = new HashMap<>();
protected S parent;
protected NamedParamSet(S parent) {
this.parent = parent;
if (parent != null) {
defaults = null;
} else {
defaults = new HashMap<>();
register(this.getClass());
}
}
public void setParent(S parent) {
this.parent = parent;
}
public final S apply(P... parameters) {
for (var p : parameters) {
params.put(p.getName(), p);
}
return (S) this;
}
private void register(Class<?> clazz) {
Field[] fields = clazz.getDeclaredFields();
for (var field : fields) {
if (!Modifier.isStatic(field.getModifiers())) {
continue;
}
try {
field.setAccessible(true);
Object fieldValue = field.get(null);
if(!(fieldValue instanceof NamedParam)) {
continue;
}
P np = (P) fieldValue;
if (defaults.containsKey(np.getName())) {
throw new IllegalArgumentException("Duplicate keys in named values.");
}
defaults.put(np.getName(), np);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
protected Object getParamValue(P p) {
S root = (S) this;
S ref = (S) this;
while(true) {
if (ref.params.containsKey(p.getName())) {
return ref.params.get(p.getName()).getValue(root);
}
if (ref.parent != null) {
ref = ref.parent;
continue;
}
return ref.defaults.get(p.getName()).getValue(root);
}
}
private S getRoot() {
S root = (S) this;
while (root.parent != null) {
root = root.parent;
}
return root;
}
public abstract S bind(P... parameters);
@Override
public String toString() {
return getClass().getSimpleName()
+ getRoot().defaults.values().stream().map(p -> {
Object value = getParamValue(p);
return p.getName() + ":" + (value == null ? "null" : value);
}).collect(Collectors.joining(",", "{", "}"));
}
}
This is much more consistent. The class encodes a set of named parameters. It does not make sense to mix parameters from various named parameter sets. As an example where is no reason to mix graphical plot parameters with printing parameters or with anything else. As such, we need a set to manage all the named parameters from the same family. This is the class.
We have two generic types in the class definition. The first type S is a recursive generic and describes actually the subclass implementation of the base abstract set class. A construct similar with the Self type from other languages. The second generic type P describes the named parameter class implementation managed by S set class.
The named parameter set has three properties, the default parameter value’s map, the current parameter value’s map and a parent set. If an S instance does not have a parent (the parent reference is null) it’s values are the default values. If it has a parent, the current parameter values are stored in params map.
An interesting fact is what is going on in the constructor for the case when an S instance does not have an instance. As we said, if it does not have a parent the values are the default values. But the default values are not present, so how do we know the default values? We inspect the S instance (this newly constructed instance, actually) and we search through reflection all the static members which extends P class, and we register them as default values. All this is done in method register. In that place we check at runtime if the parameter values identity condition is met.
The next method is setParent which is used to set a parent to the current set. This exists to allow some flexibility when building hierarchies (for example we might not know at creation time the hierarchy, this allows one to bind parents later).
It follows method apply. This is used to set parameter values from an array of values. This is usually received through a var args method.
The getParameterValue employs the hierarchy to compute parameter values. What it does is to find in the hierarchy, starting with the current set and goings further through parent’s references, the first set which contains a value for the requested parameter. Once this parameter instance is found, its building value function is called with the original base parameter values set.
The other methods are self explanatory. I will not waste your time with their description.
A small example
We will not implement a first set of named parameters.
final class GOpt<V> extends NamedParam<GOpts, V> {
public GOpt(String name, SFunction<GOpts, V> fun) {
super(name, fun);
}
public GOpt(GOpt<V> p, SFunction<GOpts, V> fun) {
super(p.getName(), fun);
}
}
Nothing fancy. We have two constructors, one of them creating only to reduce some typing. A legitimate question would be if we need a separate class at all. Well, we need, because we will use this class to receive parameters for this set and only those kind of named parameters, and we do that using this type.
final class GOpts extends NamedParamSet<GOpts, GOpt<?>> {
// named parameters definitions with default values
private static final GOpt<Boolean> _verbose = new GOpt<>("verbose", __ -> true);
private static final GOpt<Color> _color = new GOpt<>("colour", __ -> Color.GREEN);
private static final GOpt<DecimalFormat> _floatFormat = new GOpt<>("floatFormat",
s -> s.getVerbose() ? new DecimalFormat("0.00000000") : new DecimalFormat("0.00"));
private static final GOpt<Color[]> _repeatColors = new GOpt<>("repeatColors", __ -> new Color[] {Color.GREEN});
// named parameter static builders
public static GOpt<Boolean> verbose(boolean verbose) {
return new GOpt<>(_verbose, __ -> verbose);
}
public static GOpt<Color> color(Color c) {
return new GOpt<>(_color, __ -> c);
}
public static GOpt<Color[]> repeatColors(int len) {
return new GOpt<>(_repeatColors, s -> {
Color[] array = new Color[len];
Arrays.fill(array, s.getColor());
return array;
});
}
public GOpts(GOpts parent) {
super(parent);
}
public GOpts bind(GOpt<?>... parameters) {
return new GOpts(this).apply(parameters);
}
// getters for parameter values
public Boolean getVerbose() {
return (Boolean) getParamValue(_verbose);
}
public Color getColor() {
return (Color) getParamValue(_color);
}
public DecimalFormat getFloatFormat() {
return (DecimalFormat) getParamValue(_floatFormat);
}
public Color[] getRepeatColors() {
return (Color[]) getParamValue(_repeatColors);
}
}
Above we found the named parameter set implementation. The GOpts type is the set and GOpt type is the named parameter. There are three sections in this implementation: the definition of the named parameters, the builders for named parameters and the getters for named parameters. Additionally we have a constructor and some utility optional method.
The definition of the named parameters contains as one private static variable for named parameters. The definition of the named parameters contains its identity and default values. We have defined four named parameters. The first two named parameters are as simple as possible: a boolean and a color. The third named parameter is interesting in that it will not have a getter. It’s value is computed entirely based on the value of another parameter, therefore we do not need to specify it. The last parameter simply build a repeated array of size given as parameter, filled with values from another parameter. I have to recognize that my creativity suddenly dropped at negative levels, at it does not make much sense. It exists only for illustrative purposes.
The builder section is basically our interface. What we have in this section are static methods which produces named parameters. The name of the methods serves as the name in generic named parameter theory. You have to choose it to be as expressive as possible. The reference used in constructor make the connection between the method and it’s underlying named parameter. The second method is the implementation function. Having that we have the freedom to produce values for any parameter using any input we want. Since we build a function, the evaluation is lazy and happens when is needed. Since we have access to the whole parameter set, here is the place which can be used to create conditional computed values for parameters.
The last section contains getter methods. Their code exists in the parent class, but a casting is required and an identifier which is hidden from outside.
That’s all for implementation part. How we will use it? Below you will find an example of implementation.
class MyBeloverPrinter {
...
public void print(GOpt<?> ...opts) {
GOpts options = new GOpts(null).bind(opts);
if(options.getVerbose()) {
doVerboseStuff();
}else {
doNonVerboseStuff();
}
...
}
}
The example is simple, no hierarchy, only default values and current values. The usage is trivial also:
myPrinter.print( verbose(true), color(Color.GREEN) );
We can create hierarchies with ease and inject them in our hierarchies. A good example for that is the plotter tool that I built in my pet library rapaio. For example a grid layer creates a grid of cells which splits a graphical view. It contains inside a set of graphical option. Each cell or rectangular group of cells can be rendered by a plotter. The plotter will create a set of graphical options which has as a parent the grid layer options. Next, on each plotter and artist can start to do things (histograms, points, lines, whatever is available). Each artist creates it’s own set of graphical parameters which are bound to their plotter set of parameters. Thus you can specify named parameter values for all object at grid layer level, or only for objects of a plotter, or custom for each artist.
Specifying named parameters exploits the var args syntax and the named parameter type you have defined. Thus, one can easily use them.
The explained scenario for graphical parameters is described in some medium/large classes, so I will not put it here to waste your space. For further reference you can check it here: GOptions.java. It contains 4.5 hundred lines, but I am sure it is very easy to read.
In the end I will show you another example of a graph build using the same syntax:
GridLayer grid = GridLayer.of(2, 2, lwd(0.5f));
BiFunction<Double, Double, Double> f = (x, y) -> pow(x * x + y - 11, 2) + pow(x + y * y - 7, 2);
Grid2D gd = Grid2D.fromFunction(f, -3, 3, -3, 3, 256);
int levelCount = 30;
double[] p = VarDouble.seq(0, 1, 1. / levelCount).elements();
double[] levels = gd.quantiles(p);
grid.add(isoCurves(gd, levels, palette(bicolor(NColor.darkred, NColor.dodgerblue, gd.minValue(), gd.maxValue()))));
grid.add(isoBands(gd, levels, palette(hue(0, 240, gd.minValue(), gd.maxValue()))));
grid.add(isoLines(gd, levels, palette(hue())));
grid.add(isoCurves(gd, levels, palette(hue()));
grid.xLim(-2, 2).yLim(-4, 4));
WS.draw(grid);
Further thoughts
I do not see the need to implement stuff like this directly in the language. Besides the reasons that I described in the introduction, I can add one more. It is impossible to build a reach set of features for named parameters directly in the language. As such, I do not consider it’s worth the effort.
But some simplifications can be done. One idea would be to use ASM and annotations to generate getters at runtime. Another straightforward idea is to generate builders for annotated parameters. Maybe using an annotation to designate named parameter definitions to improve type safety is also desirable, instead of obey a convention, as I did. Maybe that efort will be done, by me or somebody else, who knows. But even if not, I hope this construct can be simply used in your projects, by simply copying the base classes code.
Leave a Reply