Volatile functions

5.05K viewsScripting
0

I’m writing a plugin function which is volatile – repeated calls to it return varying values – but I’m having trouble making this work.

As an example, consider the built-in rand() function. This returns a different value after each call (i.e. it has type “”IO double”” rather than “”double””, to borrow the Haskell convention).

Now if I write a Groovy function:

double groovy_rand() {
return new Random().nextDouble();
}

Then this behaves as I would expect, in that a 2D matrix with e.g. A1 = groovy_rand() produces a list of random numbers across category B.

But if I write a plugin function:

public class PluginRand extends AbstractQFunction {

public QValueList evaluate(QValueList[] args) {
return QValueFactory.createSingletonValueList(new Random().nextDouble());
}

public QType[] argumentTypes() {
return new QType[] {};
}

public QType returnType() {
return QType.cValueType;
}
}

then doing the same A1 = pluginrand() just produces a list across B of the same *first* random number.

How can I obtain the Groovy-like volatile behaviour from a plugin?

Many thanks.

0

Ah, that’s great, many thanks for the “under the hood” details Ben. So there is actually something special about how rand() works which is not re-implementable in a plugin. I think I can solve this with careful use of a dummy parameter, with rand() as a last resort.

This sounds like it’s one for the next version of QAPI: add a flag for whether the function should be cached or not.

Interesting however that groovy functions are not cached. This is what I need so an alternative to the hack is to do my plugin in a script. I actually prototyped it first in groovy, but ran into typing limitations.

To go off-topic now, the only reason I used a plugin function is that its parameters and return type can be a cValueType which can happily be either a String or a double.

On the other hand, I believe for a groovy function to be picked up by Quantrix, its parameters and return types have to be marked specifically as one of String or double (or int, Date etc.). Am I correct in saying we can’t yet define a polymorphic function in groovy that will work as Quantrix function?

Also I don’t think that varargs work for groovy functions, whereas a plugin can use cRepeatType to obtain a Quantrix function with varargs.

Thanks.

0

Whoops. Sorry about that. I overlooked that fact that QAPI functions are inherently cached. Unfortunately, I [i:2ezu17og]can’t[/i:2ezu17og] offer any elegant way around that. However, here is the least offensive hack we can think of:

You can write your plug-in function so that it takes a dummy argument, and then pass (built-in) [font=””Courier New””]rand()[/font] as the argument value when you call the function from a formula. Although it’s not very pretty, this kills two birds with one stone: first, it forces evaluation of the plug-in function because it recognizes that its input is [font=””Courier New””]rand()[/font] which is subject to change. Second, it forces caching of the plug-in function to be turned off because [font=””Courier New””]rand()[/font] itself is not cached and therefore the engine will not cache any function whose value depends on an evaluation of [font=””Courier New””]rand()[/font]. This isn’t pretty, but I think it will get the job done. Here is what the function code would look like:

[code:2ezu17og]
public class PluginRand extends AbstractQFunction
{
@Override
public QValueList evaluate(QValueList[] args)
{
return QValueFactory.createSingletonValueList(Math.random());
}

@Override
public QType[] argumentTypes()
{
return new QType[]{QType.cValueType};
}

@Override
public QType returnType()
{
return QType.cValueType;
}
}
[/code:2ezu17og]

Incidentally, you could also apply this same approach to a scripted function, which is [i:2ezu17og]already[/i:2ezu17og] non-cached but which would present the same problem as before with non-calculation due to lack of dependencies. In the case of a scripted function, the dummy argument with a [font=””Courier New””]rand()[/font] value gets you past the issue of non-auto-calculation, and you get the non-cached behavior for free. It’s also simpler to create than the plug-in version, although I understand that you may have your reasons for wanting or needing to go the plug-in route.

Hope this helps.
-Ben

0

Thanks Ben – but still not quite what I’m after. This leads to just one call of evaluate() for a whole block.

Whilst the calculationListener now forces evaluate() to be called again after each model change, each cell in the block is equal to the first random value.

It sounds like there is no way to mark a function as “IO double” (i.e. having side effects) for the optimisation engine to understand that it must call it more than once.

So I think the fix here is to add a dummy “trigger” parameter, which varies per cell: i.e.

A = plugin_rand(@A)

and ignore the trigger parameter.

Ideally I wanted to achieve the same effect via

A = ReturnFirstValueAndIgnoreRest(plugin_rand(), @A)

but the optimisation engine is seemingly very aggressive in that even though the entire formula does depend upon the cell, it “caches” the subexpression that does not.

Any other thoughts on how this might work?

For background on this, the actual project I’m working on is an implementation of the ObjectHandler framework as described by Christian Fries in “Comments on Handling Objects in Spreadsheets” and also implemented commercially by him (in Java) in “Obba” (obba.info).

0

Hi Apollo,

This is a consequence of Quantrix’s optimized calculation process. Quantrix will only recalculate a cell’s value if it detects a change to another cell on which that value depends. When you use your function to calculate a cell, that cell’s value isn’t derived from any other cells, so Quantrix decides that it can’t possibly need to be re-calculated because it has no dependencies. Since the cell isn’t being calculated, your function never gets evaluated and it doesn’t get the chance to offer up a new value.

Luckily, you can write your function so that it saves a reference to its owning formula, then listens for calculation events on the model and forces the formula to recalculate itself if the model calculates. Try this:

[code:165h4yu0]
public class PluginRand extends AbstractQFunction
{
private QFormulaContext formula;
private QuantrixEventListener calculationListener = new QuantrixEventListener()
{
@Override
public void notify(QuantrixEvent event)
{
if (event.getType() == QuantrixEventType.cModelWillCalculate)
formula.forceRecalc();
}
};

@Override
public void registerFormulaContext(QFormulaContext context)
{
formula = context;
Model model = formula.getModel();
if (model != null)
model.addListener(calculationListener);
}

@Override
public void unregisterFormulaContext(QFormulaContext context)
{
Model model = formula.getModel();
if (model != null)
model.removeListener(calculationListener);
formula = null;
}

@Override
public QValueList evaluate(QValueList[] args)
{
return QValueFactory.createSingletonValueList(Math.random());
}

@Override
public QType[] argumentTypes()
{
return new QType[]{};
}

@Override
public QType returnType()
{
return QType.cValueType;
}
}
[/code:165h4yu0]

0

Yes, QAPI does allow for returning a list of values but really I want evaluate() to be called for each cell (in my project there are side effects to the call, and I want these evaluated in order of the cell dependencies).

0

Yes, the reason for this behavior is this function:

[code:3nysy3ps] QValueFactory.createSingletonValueList(getRandomNumber()) [/code:3nysy3ps]

createSingletonValueList() creates a list with just one single (-> singleton) value.

Is there another function in the QAPI who allows creating a list with different values? Or is it possible to make this function volatile?

Dominik

0

Thanks for looking at this, Dom. Unfortunately this doesn’t change anything – adding a println() shows that evaluate() is only called once per spreadsheet recalculate.

0

Apollo,

Just a quick and maybe a silly idea: Create a seperate function inlcuding the code

[code:o88cyp90] return new Random().nextDouble() [/code:o88cyp90]

Your class looks then like this:

[code:o88cyp90]
public class PluginRand extends AbstractQFunction {

public QValueList evaluate(QValueList[] args) {
return QValueFactory.createSingletonValueList(getRandomNumber());
}

private static double getRandomNumber()
{
return new Random().nextDouble();
}

public QType[] argumentTypes() {
return new QType[] {};
}

public QType returnType() {
return QType.cValueType;
}
}

[/code:o88cyp90]

But I suppose this won’t work as I hope because the function createSingletonValueList() just creates one single value.

All the best,
Dominik