8. Actions and callables
An action is a named routine. It may be resultless, return one value, or yield a sequence. A callable is a value that can be invoked with an action-compatible signature. Both participate in ordinary .NET method and delegate contracts.
8.1 Declarations, results and normal completion
action CalculateFee(amount as Decimal, preferred as Boolean = false)
returns Decimal
require amount >= 0
if preferred then
return 0
end if
return amount * 0.015
end action
CS-ACTION-001. action Name(parameters) ... end action declares a routine. A returns Type clause declares its result type. Without returns or yields, the action is resultless. Parameter and result types must be valid in their declaration context and accessible through the action's effective public contract.
CS-ACTION-002. return expression evaluates the expression once, captures its value under the declared result type, and exits the current action through applicable cleanup and postcondition checks. A resultless action cannot return a value. Returning from a local action exits that local action, not its enclosing action.
CS-ACTION-003. A value-returning action has its own contextual result. Assigning result sets the prospective return value without exiting. Bare return and normal fallthrough return its current value, provided it is definitely assigned on that path. returns Type = expression explicitly initializes the result. Without that initializer, every normally completing path must assign or return a compatible value; the compiler must not inject nothing, zero or another default to repair incomplete flow.
action CalculateDiscount(amount as Decimal, preferred as Boolean)
returns Decimal = 0
require amount >= 0
if preferred then
result = amount * 0.05
end if
end action
The result initializer makes the non-preferred path complete. An equivalent action without the initializer would need an explicit result on that path. Exceptional paths do not need to manufacture a result. Awaiting and sequence production are defined in chapter 17.
8.2 Parameters and call binding
CS-ACTION-004. Ordinary parameters use name as Type. A parameter without a default must be supplied. name as Type = initializer declares an optional parameter. Argument presence is a binding requirement; value validity is checked separately through types, contracts or ordinary application logic.
CS-ACTION-005. Named arguments use name: expression. Names are checked case-insensitively against the selected declaration. Positional arguments precede named arguments. Unknown names, repeated inputs, missing required inputs and an ambiguous applicable overload are compilation errors. = is not a named-argument separator.
let fee = CalculateFee(1500, preferred: true)
let another = CalculateFee(amount: 1500, preferred: false)
Arguments retain source-order, once-only evaluation even when named arguments are arranged differently from parameter order. A backend may capture argument values into temporaries before invoking the selected method in its physical parameter order.
CS-ACTION-006. Overload resolution uses parameter signatures, applicable conversions, optional inputs and ordinary .NET-compatible rules. Return type alone does not distinguish overloads. Case differences alone cannot distinguish action names. An ambiguous call is rejected rather than selecting an overload by declaration order.
CS-CALL-001. Generic arguments and parameters use <of ...>. Generic method inference follows ordinary .NET/C#-style argument inference; an otherwise-unbound type parameter is not inferred solely from the assignment target or expected result type.
action FirstItem <of T>(items as Global.System.Collections.Generic.IEnumerable <of T>)
returns T
return Global.System.Linq.Enumerable.First(items)
end action
Calling FirstItem(items) can infer T from the source's enumerable element type. Explicit arguments remain available as FirstItem <of Customer>(customers). Generic constraints and imported constructed-type identities are preserved; inference is not permission to change a referenced library's signature.
8.3 Block-form named calls and variable-length inputs
CS-ACTION-007. call Target with ... end with supplies a named-input block to an action or callable. Each entry uses name: expression; the complete input set participates in ordinary binding. The call introducer is required for this block form. An arbitrary Target with suffix is invalid.
let fee = call CalculateFee with
amount: 1500
preferred: true
end with
new Type ... with is the corresponding constructor-input form in chapter 9. These blocks supply arguments; they do not mutate an object after invocation.
CS-ACTION-008. A variable-length parameter uses name as Type.... There may be one such parameter and it must be last. Inside the action it is one typed collection. Omitting it supplies an empty collection; callers may supply compatible individual values or a compatible existing collection. It occupies one declared parameter position in arguments, rather than flattening into several positions.
action Total(amounts as Decimal...)
returns Decimal = 0
for_each amount in amounts
result += amount
end for_each
end action
The CLR-facing representation is a suitable ordinary params-style parameter. No spread operator is implied.
8.4 By-reference interoperability
CS-ACTION-009. The by-reference forms represent native CLR parameter passing, not heap-allocated reference wrappers.
| DirectSharp declaration | CLR behavior | Assignment obligation |
|---|---|---|
value as T |
By value | Caller supplies a compatible value |
value as reference <of T> |
ref T |
Caller supplies an initialized writable location; callee can read and change it |
let value as T |
out T |
Caller need not initialize it; callee must assign it on every successful return |
value as readonly reference <of T> |
in T |
Callee can read the referenced value and cannot modify it through this parameter |
action TryReadAmount(text as String, let amount as Decimal)
returns Boolean
return Decimal.TryParse(text, let amount)
end action
A referenced API's ref, out or in contract is retained. Definite assignment, writable-location requirements, lifetime restrictions and restrictions on retaining references across suspension follow the corresponding CLR/C# contract. The compiler must not copy a ref parameter into a detached wrapper or claim that an unassigned output became initialized merely because the action returned false.
8.5 Live parameter context
CS-ACTION-010. arguments is a live view of the current action's effective parameters. arguments.name is statically bound, strongly typed access to that parameter. Assigning it immediately changes subsequent parameter reads. Ordinary by-value assignment still does not replace the caller's variable; a by-reference parameter retains its native caller-visible behavior.
| Access | Meaning |
|---|---|
arguments[0] |
Readonly fully qualified action identity |
arguments[1] |
First declared parameter |
arguments[2] |
Second declared parameter, and so on |
arguments.amount |
Current parameter named amount |
arguments.ToArray() |
Detached shallow copy; identity occupies element zero |
arguments.ToMap() |
Detached shallow parameter-name/value map |
The view is materialized or lowered only when used. It is not a universal Object[] ABI. An indexed operation evaluates its index exactly once. A constant index is checked against its known parameter at compile time; a dynamic index requires a typed runtime dispatcher. Invalid indexes and incompatible assignments must fail; neither may be silently ignored or coerced. String-keyed parameter maps use the normal case-insensitive key contract.
8.6 Action references, callable types and closures
CS-ACTION-011. An action reference does not invoke the action. action Process() selects a zero-parameter signature; action Process(Customer) selects by parameter type signature; action Process is allowed when resolution is unambiguous. A receiver-qualified reference binds to that receiver. Ambiguous overload selection is an error.
CS-ACTION-012. Callable types use action(T1, T2) for a resultless signature and action(T1, T2) returns R for a value-producing signature. Parameters may instead be named, such as action(amount as Decimal, preferred as Boolean) returns Decimal. Names do not participate in signature compatibility; count, order, types, ref-kind and result contract do.
let calculate as action(amount as Decimal, preferred as Boolean) returns Decimal = CalculateFee
let fee = calculate(preferred: true, amount: 1500)
The callable type's names determine its stable named-call surface, even if a different compatible action is assigned later. An unnamed callable type accepts positional invocation only; generated delegate parameter names do not invent source names. Defaults are not permitted inside callable type signatures. A named action's optional implementation parameters do not make an argument optional through a callable type that requires it.
CS-ACTION-013. Local actions are multiline action declarations in an enclosing action's lexical scope. Each has its own result and arguments context. Captures use normal .NET closure behavior. Local declaration does not require or promise backend inlining.
CS-CALL-002. A lambda target determines both its input types and representation. A delegate target receives an ordinary delegate/closure; an expression-tree target receives an expression tree. An implicit-parameter selector such as .Name, or a named expression such as customer: customer.Name, refers to the target's input parameter. Captures retain ordinary .NET closure/expression-tree semantics. A body not representable in the required target model is a compilation error; the compiler must not silently switch models.
let active = customers.Where(.IsActive)
let names = customers.Select(customer: customer.Name).ToList()
For a multi-parameter expression lambda, names precede the separating colon, as in item, index: index. Large logic belongs in a named/local action. Normal LINQ and provider execution remain unchanged.
8.7 Diagnostics and reference C# lowering
Required diagnostics include incomplete normal result paths; values returned from resultless actions; incompatible results; duplicate or unknown named inputs; positional inputs after named inputs; multiple/nonfinal variadic parameters; unassigned output parameters; invalid reference locations; ambiguous action references; callable-signature defaults; and a lambda incompatible with its required expression-tree target.
The fee action has the following representative lowering:
public decimal CalculateFee(decimal amount, bool preferred = false)
{
if (amount < 0m)
throw new ArgumentException("Requirement failed: amount >= 0", nameof(amount));
if (preferred)
return 0m;
return amount * 0.015m;
}
Func<decimal, bool, decimal> calculate = CalculateFee;
decimal fee = calculate(1500m, true);
The callable's DirectSharp names are resolved before emitting positional delegate invocation. Ordinary CLR methods and delegates carry the runtime behavior; no generated public argument-record class is required.
Specification contents · Previous: 7. Statements and control flow · Next: 9. Classes, data and members