Documentation/Chapters

Functions and subroutines

On this page

A function gives a name to an operation and returns a result. A subroutine performs work without a successful result. Both let you divide a program into pieces you can understand and reuse.

Define an operation

PRINT Area(3.0, 4.0)
ShowGreeting("Ada")
FUNCTION Area(width AS DOUBLE, height AS DOUBLE) AS DOUBLE
    RETURN width * height
END FUNCTION
SUB ShowGreeting(name AS STRING)
    PRINT "Hello, " + name
END SUB

Parameters name the inputs. AS DOUBLE after the parameter list names the function’s successful result type. Every reachable successful path in a FUNCTION must return a value of that type. A SUB can reach END SUB or use bare RETURN; EXIT SUB also returns successfully. A function cannot use bare RETURN or EXIT FUNCTION. Checked failure is a separate exit path, explained in chapter 11.

An expression-bodied function is convenient for a short calculation:

PRINT Square(5)
FUNCTION Square(value AS INTEGER) AS INTEGER = value * value

Defaults and named arguments

PRINT Greeting("Ada")
PRINT Greeting(prefix="Welcome, ", name="Grace")
FUNCTION Greeting(name AS STRING, prefix AS STRING = "Hello, ") AS STRING
    RETURN prefix + name
END FUNCTION

Required parameters precede optional ones. Defaults use checked constant values and construction recipes, rather than arbitrary runtime calls. Positional arguments precede named arguments; a name cannot be supplied twice. Supplied expressions evaluate once in written order; omitted defaults follow. Naming arguments changes which parameter receives a value, not the order in which supplied expressions execute.

Default records and collections

Defaults can construct records, choices, typed errors, nested lists, empty collections, SOME/NONE and scalar tables. Only omitted arguments construct their defaults, after supplied arguments have evaluated. Names resolve in the function’s declaring module; caller locals cannot change which constructor or constant a default means. Arbitrary function calls and mutable variables are not defaults.

TYPE Config
    Limit AS INTEGER = 20
    Names AS LIST OF STRING = []
END TYPE
FUNCTION Label(config AS Config = Config { Limit=20, Names=["default"] }) AS STRING
    RETURN config.Names[0] + ": " + STR(config.Limit)
END FUNCTION
PRINT Label()
PRINT Label(Config { Limit=42, Names=["custom"] })

Use an ordinary default when omission means a known configuration. Use an optional when absence is itself meaningful or a fallback must run ordinary code. BYVAL defaults create independent mutable owners on each call:

FUNCTION ExpandedSize(BYVAL values AS LIST OF INTEGER = [1, 2]) AS INTEGER
    Append(BYREF values, 3)
    RETURN LEN(values)
END FUNCTION
PRINT ExpandedSize()
PRINT ExpandedSize()

Both calls print 3. There is no persistent shared default list. Fresh resource literal defaults are constructed and cleaned normally, without copying an existing owner. BYREF and TAKE defaults remain rejected. Task entries use the same checked recipes. Generic recipes are checked when their concrete omitted value is needed.

Normalize an optional parameter locally

An explicit DIM can reuse the name of an ordinary or BYVAL parameter. The initializer reads the original parameter; subsequent statements see the new local, which may have a different type. This is called shadowing. Combined with the lazy ?? fallback from chapter 6, it gives a convenient configuration pattern:

TYPE Config
    Limit AS INTEGER = 20
    Names AS LIST OF STRING = []
END TYPE
FUNCTION Run(config AS OPTIONAL OF Config = NONE) AS INTEGER
    DIM config = config ?? Config { Limit=20 }
    Append(BYREF config.Names, "local")
    RETURN config.Limit + LEN(config.Names)
END FUNCTION
given = SOME(Config { Limit=40, Names=["original"] })
PRINT Run()
PRINT Run(given)
IF LET original = given THEN
    PRINT LEN(original.Names)
END IF

The local change leaves the caller’s value independent. Leaving a nested block restores the original parameter’s visibility. BYREF and TAKE parameters cannot be shadowed, and duplicate locals in one scope remain errors. Shadowing does not extend the original parameter’s borrowed-return lifetime.

Grant writable access explicitly

Default parameters borrow read-only access. Reading a large list parameter does not automatically deep-copy the list. A writable operation requires BYREF at the declaration and the call site:

count = 10
Increment(BYREF count)
PRINT count
SUB Increment(BYREF value AS INTEGER)
    value += 1
END SUB

BYREF gives the function exclusive writable access for the call. Passing the same storage through conflicting arguments is rejected. BYVAL creates a writable independent local value instead. Chapter 9 explains when to choose each mode.

Overloads and local helpers

Functions may share a name when their signatures let the compiler choose a unique operation. Numeric conversions are explicit; an INTEGER overload and a DOUBLE overload do not make those types interchangeable. Result context can help resolve some otherwise incomplete calls, but the compiler rejects genuine ambiguity.

A local FUNCTION or SUB keeps a helper inside its lexical block. Declare it before use. It does not capture surrounding local variables; pass needed data as parameters. For a function value that snapshots outer copyable data, use an anonymous lambda rather than a named local declaration. Chapter 13 explains both styles and their different capture rules.

Recursion is supported, but stacks are finite. For data whose depth comes from untrusted input, consider an explicit list of pending work and a checked size limit rather than assuming arbitrarily deep recursive calls will fit.

From the BASIL documentation. View source ↗

Find your way around BASIL

Type to search the manual.