On this page

Actions and callables

An action names an operation. It may return a value, change application state, or call a library. Use the same action keyword for each; the result contract tells the reader which kind of operation it is.

action / returns: declare a typed operation

An order summary contains several related calculations. Keep the calculations independently callable and make the final sequence visible:

public class OrderTotals
    public static action Subtotal(quantity as Integer, unitPrice as Decimal)
        returns Decimal
        require quantity > 0
        require unitPrice >= 0
        return quantity * unitPrice
    end action

    public static action Discount(subtotal as Decimal, rate as Decimal = 0)
        returns Decimal
        require subtotal >= 0
        require rate >= 0 and rate <= 1
        return subtotal * rate
    end action

    public static action Tax(net as Decimal, rate as Decimal)
        returns Decimal
        require net >= 0
        require rate >= 0
        return net * rate
    end action

    public static action Calculate(
        quantity as Integer,
        unitPrice as Decimal,
        taxRate as Decimal,
        discountRate as Decimal = 0
    )
        returns Decimal
        let subtotal = Subtotal(quantity, unitPrice)
        let discount = Discount(subtotal, discountRate)
        let net = subtotal - discount
        let tax = Tax(net, taxRate)
        return net + tax
    end action

    public static action Run()
        let total = Calculate(3, 25, taxRate: 0.18, discountRate: 0.10)
        Global.System.Console.WriteLine("Order total: {{total}}")
    end action
end class

An optional parameter has a default value. Required parameters precede optional ones. Named arguments make this call's two rates distinct without forcing the reader to remember their positions.

The example deliberately leaves rounding to a separate financial policy. Decimal arithmetic and a variable named tax do not decide where a business must round, what legal tax rates apply, or which currency an amount represents.

return / result: complete an action

returns Decimal states the logical result type. return expression produces that value and exits. A value-returning action must produce a result on every normal path unless it explicitly declares an initial result.

The contextual result can be assigned without immediately exiting:

public class DiscountPolicy
    public static action Rate(isPreferred as Boolean, orderCount as Integer)
        returns Decimal = 0
        require orderCount >= 0
        ensure result >= 0 and result <= 1

        if isPreferred then
            result = 0.10
        else
            if orderCount >= 20 then
                result = 0.05
            end if
        end if
    end action
end class

The declared zero default permits successful fallthrough. Without that default or a proven result assignment, an incomplete path is a compile-time error. An absent class result is never silently substituted to make a missing return legal.

Choose the form that exposes the policy clearly. Direct return statements suit independent branches; result suits a calculation assembled before a common successful exit. take belongs to constructs that explicitly produce scoped values and does not replace return in an action.

self / arguments: distinguish members from inputs

A parameter and a member with the same name cannot be distinguished by writing one with a capital letter. Qualify the intended symbol:

public class CustomerName
    public Name as String = ""

    public action Rename(name as String)
        require arguments.name is not nothing
        require arguments.name is not whitespace
        self.Name = arguments.name.Trim()
    end action
end class

self is the current object. arguments.name names the current action parameter. This is statically bound parameter access, not a reflection dictionary. It also avoids turning a casing convention into a correctness requirement.

Ordinary value parameters receive their argument value. Reassigning a value parameter does not rebind the caller's local. Reference parameters have a separate explicit contract; use them when caller-visible rebinding is actually required.

lambdas / selectors: pass behavior to a library

Many .NET APIs accept a delegate or an expression tree. DirectSharp provides receiver-based and named-parameter expressions for these contexts:

import Global.System.Linq.*

public class Customer
    public Name as String = ""
    public IsActive as Boolean = false
    public CreditLimit as Decimal = 0
end class

public class CustomerQueries
    public static action Eligible(
        customers as Global.System.Collections.Generic.IEnumerable <of Customer>
    )
        returns Global.System.Collections.Generic.List <of Customer>
        require customers is not nothing

        return customers
            .Where(.IsActive)
            .Where(customer: customer.CreditLimit > 0)
            .OrderBy(.Name)
            .ToList()
    end action
end class

.IsActive is an expression over the lambda's receiver. The named form makes the subject explicit when the expression needs it. These are ordinary calls to LINQ; Where, OrderBy and ToList are library methods.

The target parameter determines whether the compiler supplies a delegate or an expression tree. An IQueryable provider keeps its translation restrictions. A named action containing arbitrary statements does not automatically become a database expression tree.

LINQ pipelines commonly defer execution. ToList materializes this pipeline, so the caller receives the results computed during this call. Omitting it and returning a sequence would make enumeration time part of the API's behavior.

of / where: declare generic actions

public class SequenceTools
    public static action FirstOrDefault <of T>(
        items as Global.System.Collections.Generic.IEnumerable <of T>
    )
        returns T
        require items is not nothing
        for_each item in items
            return item
        end for_each
        return default
    end action
end class

The generic type connects the sequence's element type to the result. This application helper returns the type's effective default when the sequence is empty. For a reference type whose effective default is absence, that is nothing; for another type, it can be a real value. If callers must distinguish emptiness from a valid default-valued item, use an explicit result model instead of this convenience action.

See action contracts for binding and callable details.

Practice: actions and callable values

Repair a parameter/property rename without relying on capitalization. The answer separates name binding from validation policy.

Previous: Conditions and loops · Next: Objects and class kinds