From 2a187cf0b639b7e2797b41cf8dd0437969db215b Mon Sep 17 00:00:00 2001 From: Andreas Eberhart Date: Wed, 4 Nov 2026 14:23:37 +0100 Subject: [PATCH 1/2] Partial application of callables and lambdas without declared arity #95 - JLambda (Fn0..Fn8) reports its number of declared parameters, which JFunctions created via Jsonata.function(...) keep - arity unknown (callable / FnVarArgs without signature): use the number of args supplied at the partial application - implement the JS "typeof proc === 'function'" branch: lambdas bound via Frame.bind can be partially applied Co-Authored-By: Claude Opus 5.5 --- .../java/com/dashjoin/jsonata/Jsonata.java | 60 +++++++++++++++---- .../com/dashjoin/jsonata/PartialTest.java | 33 ++++++++++ 2 files changed, 80 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/dashjoin/jsonata/Jsonata.java b/src/main/java/com/dashjoin/jsonata/Jsonata.java index 72266d5..d001020 100644 --- a/src/main/java/com/dashjoin/jsonata/Jsonata.java +++ b/src/main/java/com/dashjoin/jsonata/Jsonata.java @@ -1927,8 +1927,8 @@ Object evaluateLambda(Symbol expr, Object input, Frame environment) { result = partialApplyProcedure((Symbol)proc, (List)evaluatedArgs); } else if (Utils.isFunction(proc)) { result = partialApplyNativeFunction((JFunction)proc /*.implementation*/, evaluatedArgs); - // } else if (typeof proc === "function") { - // result = partialApplyNativeFunction(proc, evaluatedArgs); + } else if (proc instanceof JLambda) { + result = partialApplyNativeFunction(new JFunction((JLambda)proc, null), evaluatedArgs); } else { throw new JException("T1008", //stack: (new Error()).stack, @@ -2024,6 +2024,12 @@ Object partialApplyNativeFunction(JFunction _native, List args) { // create a lambda Object that wraps and invokes the native function // get the list of declared arguments from the native function // this has to be picked out from the toString() value + if (_native.getNumberOfArgs() < 0) { + // Java: parameters unknown (callable without signature), use the number of args supplied + var fn = new JFunction(_native.function, null); + fn.numberOfArgs = args.size(); + _native = fn; + } var sigArgs = getNativeFunctionArguments(_native); sigArgs = sigArgs.stream().map(sigArg -> { return "$" + sigArg; @@ -2087,34 +2093,34 @@ public static JFunction function(String name, String signature, Class clazz, Obj } public static JFunction function(String name, FnVarArgs func, String signature) { - return new JFunction(func.getJFunctionCallable(), signature); + return new JFunction(func, signature); } public static JFunction function(String name, Fn0 func, String signature) { - return new JFunction(func.getJFunctionCallable(), signature); + return new JFunction(func, signature); } public static JFunction function(String name, Fn1 func, String signature) { - return new JFunction(func.getJFunctionCallable(), signature); + return new JFunction(func, signature); } public static JFunction function(String name, Fn2 func, String signature) { - return new JFunction(func.getJFunctionCallable(), signature); + return new JFunction(func, signature); } public static JFunction function(String name, Fn3 func, String signature) { - return new JFunction(func.getJFunctionCallable(), signature); + return new JFunction(func, signature); } public static JFunction function(String name, Fn4 func, String signature) { - return new JFunction(func.getJFunctionCallable(), signature); + return new JFunction(func, signature); } public static JFunction function(String name, Fn5 func, String signature) { - return new JFunction(func.getJFunctionCallable(), signature); + return new JFunction(func, signature); } public static JFunction function(String name, Fn6 func, String signature) { - return new JFunction(func.getJFunctionCallable(), signature); + return new JFunction(func, signature); } public static JFunction function(String name, Fn7 func, String signature) { - return new JFunction(func.getJFunctionCallable(), signature); + return new JFunction(func, signature); } public static JFunction function(String name, Fn8 func, String signature) { - return new JFunction(func.getJFunctionCallable(), signature); + return new JFunction(func, signature); } /** @@ -2170,6 +2176,14 @@ public Frame createFrame(Frame enclosingEnvironment) { } public static interface JLambda { + JFunctionCallable getJFunctionCallable(); + + /** + * @return the number of declared parameters, -1 if unknown (varargs) + */ + default int getNumberOfArgs() { + return -1; + } } public static interface FnVarArgs extends JLambda, Function, R> { @@ -2180,21 +2194,25 @@ default JFunctionCallable getJFunctionCallable() { } } public static interface Fn0 extends JLambda, Supplier { + default int getNumberOfArgs() { return 0; } default JFunctionCallable getJFunctionCallable() { return (input, args) -> get(); } } public static interface Fn1 extends JLambda, Function { + default int getNumberOfArgs() { return 1; } default JFunctionCallable getJFunctionCallable() { return (input, args) -> apply((A) args.get(0)); } } public static interface Fn2 extends JLambda, BiFunction { + default int getNumberOfArgs() { return 2; } default JFunctionCallable getJFunctionCallable() { return (input, args) -> apply((A) args.get(0), (B) args.get(1)); } } public static interface Fn3 extends JLambda { + default int getNumberOfArgs() { return 3; } R apply(A a, B b, C c); default JFunctionCallable getJFunctionCallable() { return (input, args) -> apply((A) args.get(0), (B) args.get(1), @@ -2202,6 +2220,7 @@ default JFunctionCallable getJFunctionCallable() { } } public static interface Fn4 extends JLambda { + default int getNumberOfArgs() { return 4; } R apply(A a, B b, C c, D d); default JFunctionCallable getJFunctionCallable() { return (input, args) -> apply((A) args.get(0), (B) args.get(1), @@ -2209,6 +2228,7 @@ default JFunctionCallable getJFunctionCallable() { } } public static interface Fn5 extends JLambda { + default int getNumberOfArgs() { return 5; } R apply(A a, B b, C c, D d, E e); default JFunctionCallable getJFunctionCallable() { return (input, args) -> apply((A) args.get(0), (B) args.get(1), @@ -2216,6 +2236,7 @@ default JFunctionCallable getJFunctionCallable() { } } public static interface Fn6 extends JLambda { + default int getNumberOfArgs() { return 6; } R apply(A a, B b, C c, D d, E e, F f); default JFunctionCallable getJFunctionCallable() { return (input, args) -> apply((A) args.get(0), (B) args.get(1), @@ -2224,6 +2245,7 @@ default JFunctionCallable getJFunctionCallable() { } } public static interface Fn7 extends JLambda { + default int getNumberOfArgs() { return 7; } R apply(A a, B b, C c, D d, E e, F f, G g); default JFunctionCallable getJFunctionCallable() { return (input, args) -> apply((A) args.get(0), (B) args.get(1), @@ -2232,6 +2254,7 @@ default JFunctionCallable getJFunctionCallable() { } } public static interface Fn8 extends JLambda { + default int getNumberOfArgs() { return 8; } R apply(A a, B b, C c, D d, E e, F f, G g, H h); default JFunctionCallable getJFunctionCallable() { return (input, args) -> apply((A) args.get(0), (B) args.get(1), @@ -2260,6 +2283,12 @@ public static class JFunction implements JFunctionCallable, JFunctionSignatureVa Signature signature; Method method; Object methodInstance; + int numberOfArgs = -1; + + public JFunction(JLambda lambda, String signature) { + this(lambda.getJFunctionCallable(), signature); + this.numberOfArgs = lambda.getNumberOfArgs(); + } public JFunction(JFunctionCallable function, String signature) { this.function = function; @@ -2306,11 +2335,16 @@ public Object validate(Object args, Object context) { return args; } + /** + * @return the number of declared parameters, -1 if unknown + */ public int getNumberOfArgs() { if (method != null) return method.getParameterTypes().length; + if (numberOfArgs >= 0) + return numberOfArgs; // Java: a JFunctionCallable has no declared parameters, use the signature (if any) - return signature != null ? signature.getNumberOfArgs() : 0; + return signature != null ? signature.getNumberOfArgs() : -1; } } diff --git a/src/test/java/com/dashjoin/jsonata/PartialTest.java b/src/test/java/com/dashjoin/jsonata/PartialTest.java index afc894e..e22be0e 100644 --- a/src/test/java/com/dashjoin/jsonata/PartialTest.java +++ b/src/test/java/com/dashjoin/jsonata/PartialTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import com.dashjoin.jsonata.Jsonata.JFunction; +import com.dashjoin.jsonata.Jsonata.Fn2; import com.dashjoin.jsonata.Jsonata.JFunctionCallable; /** @@ -72,4 +73,36 @@ public void missingDollar() { var ex = Assertions.assertThrows(JException.class, () -> jsonata("substring(?, 1)").evaluate(null)); Assertions.assertEquals("T1007", ex.getError()); } + + // Known gap: a JFunctionCallable without signature has no arity (#83) + @Test + public void customFunctionWithoutSignature() { + var expression = jsonata("$add(1, 2) ~> $add(?, 3)"); + expression.registerFunction("add", new JFunction(new JFunctionCallable() { + @SuppressWarnings("rawtypes") + @Override + public Object call(Object input, List args) throws Throwable { + return (Integer) args.get(0) + (Integer) args.get(1); + } + }, null)); + Assertions.assertEquals(6, expression.evaluate(null)); + } + + // Known gap: same for the Fn* factories when no signature is given + @Test + public void fnFactoryWithoutSignature() { + var expression = jsonata("$add(1, 2) ~> $add(?, 3)"); + expression.registerFunction("add", + Jsonata.function("add", (Fn2) (a, b) -> a + b, null)); + Assertions.assertEquals(6, expression.evaluate(null)); + } + + // Known gap: Fn* lambdas bound via Frame.bind cannot be partially applied (T1008) + @Test + public void boundLambda() { + var expression = jsonata("$f(1, 2) ~> $f(?, 3)"); + var frame = expression.createFrame(); + frame.bind("f", (Fn2) (a, b) -> a + b); + Assertions.assertEquals(6, expression.evaluate(null, frame)); + } } From 46d2bcd8ff720000c16ae14415c7bf7ae741c283 Mon Sep 17 00:00:00 2001 From: Andreas Eberhart Date: Thu, 5 Nov 2026 11:27:46 +0100 Subject: [PATCH 2/2] Add #85 pipe tests for custom functions without signature These need the arity of a JFunctionCallable without signature (#83). Also update the PartialTest comments: the cases are no longer gaps. Co-Authored-By: Claude Opus 5.5 --- .../java/com/dashjoin/jsonata/OldPrsTest.java | 24 ++++++++++++++++++- .../com/dashjoin/jsonata/PartialTest.java | 6 ++--- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/dashjoin/jsonata/OldPrsTest.java b/src/test/java/com/dashjoin/jsonata/OldPrsTest.java index 7d820f1..d77d9d6 100644 --- a/src/test/java/com/dashjoin/jsonata/OldPrsTest.java +++ b/src/test/java/com/dashjoin/jsonata/OldPrsTest.java @@ -9,7 +9,7 @@ import com.dashjoin.jsonata.Jsonata.JFunction; import com.dashjoin.jsonata.json.Json; -/** tests from PRs #80, #85, #87, adapted to the existing API (partial application of custom functions without signature: see #83) */ +/** tests from PRs #80, #85, #87, adapted to the existing API */ public class OldPrsTest { static Object res(String name) throws Exception { @@ -50,6 +50,28 @@ public void testFunctionWithInputData() { Assertions.assertEquals(9, expression.evaluate(Map.of("factor", "3"))); } + @SuppressWarnings("unchecked") + @Test + public void testFunctionWithInputData2() { + JFunction multiply = new JFunction((input, args) -> { + Object first = args.get(0); + Object second = args.size() > 1 ? args.get(1) + : Integer.parseInt(((Map) input).get("factor").toString()); + return (Integer) first * (Integer) second; + }, null); + var expression = Jsonata.jsonata("$multiply(3) ~> $multiply(?, 2)"); + expression.registerFunction("multiply", multiply); + Assertions.assertEquals(18, expression.evaluate(Map.of("factor", "3"))); + } + + @Test + public void testCustomFunctionPipe() { + JFunction add = new JFunction((input, args) -> (Integer) args.get(0) + (Integer) args.get(1), null); + var expression = Jsonata.jsonata("$add(1, 2) ~> $add(?, 3)"); + expression.registerFunction("add", add); + Assertions.assertEquals(6, expression.evaluate(null)); + } + // #87 @Test public void testMapWithAppend() { diff --git a/src/test/java/com/dashjoin/jsonata/PartialTest.java b/src/test/java/com/dashjoin/jsonata/PartialTest.java index e22be0e..64297ad 100644 --- a/src/test/java/com/dashjoin/jsonata/PartialTest.java +++ b/src/test/java/com/dashjoin/jsonata/PartialTest.java @@ -74,7 +74,7 @@ public void missingDollar() { Assertions.assertEquals("T1007", ex.getError()); } - // Known gap: a JFunctionCallable without signature has no arity (#83) + // #83: a JFunctionCallable without signature: arity = number of args supplied @Test public void customFunctionWithoutSignature() { var expression = jsonata("$add(1, 2) ~> $add(?, 3)"); @@ -88,7 +88,7 @@ public Object call(Object input, List args) throws Throwable { Assertions.assertEquals(6, expression.evaluate(null)); } - // Known gap: same for the Fn* factories when no signature is given + // Fn* factories without signature: arity of the lambda @Test public void fnFactoryWithoutSignature() { var expression = jsonata("$add(1, 2) ~> $add(?, 3)"); @@ -97,7 +97,7 @@ public void fnFactoryWithoutSignature() { Assertions.assertEquals(6, expression.evaluate(null)); } - // Known gap: Fn* lambdas bound via Frame.bind cannot be partially applied (T1008) + // Fn* lambdas bound via Frame.bind can be partially applied @Test public void boundLambda() { var expression = jsonata("$f(1, 2) ~> $f(?, 3)");