Context
FunctionFactory currently handles array and text explicitly because both functions need access to spread-aware positional arguments.
This introduces function-name hardcoding in the factory even though array and text share the same spread semantics:
A spread argument evaluates to multiple values that are inserted at that position in the function's positional argument sequence.
For example:
is structurally equivalent to:
and:
text("foo", ...{"Nikola", "Tesla"}, "bar")
is structurally equivalent to:
text("foo", "Nikola", "Tesla", "bar")
The receiving function decides what to do with the resulting values: array collects them, while text concatenates them. The spread semantics themselves are identical.
Proposed design
Introduce a marker interface:
public interface IValueSpreadAware
{
}
Functions implementing this interface declare that their positional arguments must be bound through the value-spread-aware argument mechanism.
Initially:
public class Array : IFunction, IValueSpreadAware
public class Text : IFunction, IValueSpreadAware
The interface should remain a marker. Spread is a binding/argument-evaluation concern and should not expose runtime spread operations on the function instance itself.
Common value argument evaluator
Introduce a common evaluator abstraction used by every IValueSpreadAware function, for example:
public sealed class ValueArgumentEvaluator
{
public Func<object?, object?> Evaluator { get; }
public bool IsSpread { get; }
public ValueArgumentEvaluator(
Func<object?, object?> evaluator,
bool isSpread)
=> (Evaluator, IsSpread) = (evaluator, isSpread);
}
The exact naming can vary, but the abstraction must preserve:
- how the argument is evaluated against the current input;
- whether the argument was introduced using spread syntax.
A common helper should expand arguments while preserving order:
public static IEnumerable<object?> Evaluate(
IEnumerable<ValueArgumentEvaluator> arguments,
object? input)
{
foreach (var argument in arguments)
{
var value = argument.Evaluator(input);
if (!argument.IsSpread)
{
yield return value;
continue;
}
foreach (var item in SpreadValues.Enumerate(value))
yield return item;
}
}
Reuse the existing SpreadValues behavior where appropriate rather than introducing another definition of which runtime values can be spread.
A non-spread argument contributes exactly one value. A spread argument contributes zero or more values.
Pipeline semantics
Spread must not alter pipeline semantics.
In particular, spread should not implicitly turn a regular pipeline into element-wise evaluation.
Element-wise transformation must remain the responsibility of |>:
text(...({"Nikola", "Tesla"} |> prepend-space))
Conceptually:
{"Nikola", "Tesla"}
|> prepend-space
produces:
which is then spread into the positional arguments of text.
Consequently, remove the text-specific handling of spread InputExpressionParameter if its purpose is to provide implicit element-wise transformation.
array and text must use exactly the same value-spread argument binding semantics.
Function constructors
array and text should consume the same common evaluator abstraction, for example:
public class Array : IFunction, IValueSpreadAware
{
private Func<ValueArgumentEvaluator[]> Arguments { get; }
public Array(Func<ValueArgumentEvaluator[]> arguments)
=> Arguments = arguments;
}
public class Text : IFunction, IValueSpreadAware
{
private Func<ValueArgumentEvaluator[]> Arguments { get; }
public Text(Func<ValueArgumentEvaluator[]> arguments)
=> Arguments = arguments;
}
Both functions should consume the expanded value sequence using the same helper. Their behavior diverges only after expansion.
Review ArrayArgumentEvaluator and TextArgumentEvaluator. If their difference only exists because spread handling evolved independently, replace them with the common ValueArgumentEvaluator.
Function-specific evaluator types should remain only where they represent genuinely function-specific semantics unrelated to spread.
FunctionFactory impact
FunctionFactory must resolve the function type through FunctionTypeMapper before deciding whether special argument binding is required.
Then dispatch by capability:
if (!TypeMapper.TryExecute(function.Name, out var type))
{
// existing predicate / not implemented handling
}
if (typeof(IValueSpreadAware).IsAssignableFrom(type))
return InstantiateValueSpreadAware(type, function, context);
Introduce a common path such as:
private IFunction InstantiateValueSpreadAware(
Type type,
Bindings.Function function,
IContext context)
{
var arguments = function.Arguments
.Select(argument => new ValueArgumentEvaluator(
BuildValueEvaluator(argument.Value, context),
argument.IsSpread))
.ToArray();
// instantiate the resolved type using the common evaluator provider
}
The method must instantiate the resolved type; it must not branch again based on whether that type is Array or Text.
Explicit cleanup requirement
As part of this implementation, remove array and text from the manually handled function names in FunctionFactory.
The factory must no longer contain routing equivalent to:
if (name.Equals("array", StringComparison.OrdinalIgnoreCase))
return BuildArrayFunction(function, context);
if (name.Equals("text", StringComparison.OrdinalIgnoreCase))
return BuildTextFunction(function, context);
BuildArrayFunction and BuildTextFunction should be removed, or reduced into one generic capability-based mechanism that contains no knowledge of the concrete function names/types.
Adding another function implementing IValueSpreadAware must not require modifying FunctionFactory.
Expected factory flow
function name
↓
FunctionTypeMapper
↓
CLR function type
↓
does type implement IValueSpreadAware?
├─ yes
│ ↓
│ build ValueArgumentEvaluator[]
│ ↓
│ instantiate resolved function type
│
└─ no
↓
other special capabilities
↓
normal generic instantiation
The factory should reason about capabilities attached to the resolved type rather than maintaining another registry of function names.
Tests
Preserve/add coverage for:
array(...{1, 2}, ...{3, 4})
text("a", ...{"b", "c"}, "d")
Also cover mapped values followed by spread:
array(...({1, 2, 3} |> multiply(10)))
Expected:
and:
text(...({"nikola", "tesla"} |> upper))
Expected:
No test should rely on spread changing | into element-wise evaluation.
Add a test demonstrating that value-spread-aware dispatch is based on IValueSpreadAware / resolved type rather than the function name.
Acceptance criteria
Context
FunctionFactorycurrently handlesarrayandtextexplicitly because both functions need access to spread-aware positional arguments.This introduces function-name hardcoding in the factory even though
arrayandtextshare the same spread semantics:For example:
is structurally equivalent to:
and:
is structurally equivalent to:
The receiving function decides what to do with the resulting values:
arraycollects them, whiletextconcatenates them. The spread semantics themselves are identical.Proposed design
Introduce a marker interface:
Functions implementing this interface declare that their positional arguments must be bound through the value-spread-aware argument mechanism.
Initially:
The interface should remain a marker. Spread is a binding/argument-evaluation concern and should not expose runtime spread operations on the function instance itself.
Common value argument evaluator
Introduce a common evaluator abstraction used by every
IValueSpreadAwarefunction, for example:The exact naming can vary, but the abstraction must preserve:
A common helper should expand arguments while preserving order:
Reuse the existing
SpreadValuesbehavior where appropriate rather than introducing another definition of which runtime values can be spread.A non-spread argument contributes exactly one value. A spread argument contributes zero or more values.
Pipeline semantics
Spread must not alter pipeline semantics.
In particular, spread should not implicitly turn a regular pipeline into element-wise evaluation.
Element-wise transformation must remain the responsibility of
|>:Conceptually:
produces:
which is then spread into the positional arguments of
text.Consequently, remove the
text-specific handling of spreadInputExpressionParameterif its purpose is to provide implicit element-wise transformation.arrayandtextmust use exactly the same value-spread argument binding semantics.Function constructors
arrayandtextshould consume the same common evaluator abstraction, for example:Both functions should consume the expanded value sequence using the same helper. Their behavior diverges only after expansion.
Review
ArrayArgumentEvaluatorandTextArgumentEvaluator. If their difference only exists because spread handling evolved independently, replace them with the commonValueArgumentEvaluator.Function-specific evaluator types should remain only where they represent genuinely function-specific semantics unrelated to spread.
FunctionFactory impact
FunctionFactorymust resolve the function type throughFunctionTypeMapperbefore deciding whether special argument binding is required.Then dispatch by capability:
Introduce a common path such as:
The method must instantiate the resolved
type; it must not branch again based on whether that type isArrayorText.Explicit cleanup requirement
As part of this implementation, remove
arrayandtextfrom the manually handled function names inFunctionFactory.The factory must no longer contain routing equivalent to:
BuildArrayFunctionandBuildTextFunctionshould be removed, or reduced into one generic capability-based mechanism that contains no knowledge of the concrete function names/types.Adding another function implementing
IValueSpreadAwaremust not require modifyingFunctionFactory.Expected factory flow
The factory should reason about capabilities attached to the resolved type rather than maintaining another registry of function names.
Tests
Preserve/add coverage for:
Also cover mapped values followed by spread:
Expected:
and:
Expected:
No test should rely on spread changing
|into element-wise evaluation.Add a test demonstrating that value-spread-aware dispatch is based on
IValueSpreadAware/ resolved type rather than the function name.Acceptance criteria
IValueSpreadAware.arrayimplementsIValueSpreadAware.textimplementsIValueSpreadAware.|>, not....text-specific spread-expression handling.FunctionFactorydetects value-spread-aware functions through the resolved CLR type.arrayhandling fromFunctionFactory.texthandling fromFunctionFactory.BuildArrayFunctionandBuildTextFunctionso the factory has no concrete knowledge of these functions.IValueSpreadAwarefunction can use the same path without modifyingFunctionFactory.arrayandtextremains unchanged.