Documentation/Chapters

Generics, callbacks and custom value behavior

On this page

Generics describe one operation over multiple statically known types. Callbacks let a caller supply behavior. Neither requires dynamically typed values or runtime method inheritance.

Generic functions

FUNCTION Identity[T](value AS T) AS T = value
PRINT Identity(7)
PRINT Identity("BASIC")

T is a type parameter inferred from the supplied argument. The compiler specializes and checks the relevant concrete operation. An unsupported operation for one type is an error, rather than a runtime guess. Explicit bracketed call type arguments are not supported. Generic records use TYPE Box[T] and concrete types such as Box[STRING], including named Box[STRING] {Value="text"} and positional Box[STRING](https://github.com/geekhideout/basic/blob/main/docs/book/chapters/"text") constructors. Opaque records keep the same privacy rules. Generic record contracts and current limits .

Some calls need more context than their arguments provide. An explicit destination type or a checked AS T expression can complete supported inference. AS is type ascription, not numeric conversion. It does not change one representation into another. Missing generic information cannot always be inferred from a later use.

Advanced generic overloads may use WHERE T IS COPYABLE or WHERE T IS RESOURCE. These constraints select the supported ownership category before specialization; they are not a general user-defined trait system. Use them when an implementation genuinely needs independent copies or resource ownership.

Build reusable data with generic records

A generic record lets you reuse a data shape while retaining exact static field types. Define its parameters on TYPE, then bind them at construction or through checked destination context:

TYPE Box[T]
    Value AS T
END TYPE
text = Box[STRING] { Value="original" }
copy = text
copy.Value = "changed"
PRINT text.Value
PRINT copy.Value
number = MakeBox(42)
PRINT number.Value
FUNCTION MakeBox[T](value AS T) AS Box[T]
    RETURN Box[T] { Value=value }
END FUNCTION

Box[INTEGER] and Box[STRING] are distinct nominal types. Type parameters can appear inside lists, optionals and other records. They remain part of identity even when no field uses them. A destination such as DIM box AS Box[INTEGER] can supply the type context for Box(42); later mutation does not retroactively infer an earlier declaration. Ordinary typed constructors also have positional forms, such as Box[STRING](https://github.com/geekhideout/basic/blob/main/docs/book/chapters/"text").

Copies are independent whenever the actual fields are copyable. A Box[File] that stores a File is a resource owner; a type parameter mentioned only in the type’s identity does not itself create an owned resource. Read-only functions borrow; owning construction/insertion still requires fresh or explicit TAKE values. Generic records do not erase the difference between copies and transfers.

Finite recursive data can use containers:

TYPE Node[T]
    Value AS T
    Children AS LIST OF Node[T] = []
END TYPE
root = Node[INTEGER] { Value=1 }
Append(BYREF root.Children, Node[INTEGER] { Value=2 })
copy = root
copy.Children[0].Value = 9
PRINT root.Children[0].Value
PRINT copy.Children[0].Value

Direct inline containment cycles and indefinitely expanding type instantiations are rejected. Generic records may be opaque, keeping the same construction and field privacy rules. One WHERE T IS COPYABLE or WHERE T IS RESOURCE constraint may follow the record header. A function pattern Box[T] is more specific than an otherwise applicable unconstrained T pattern; incomparable overloads still need disambiguation.

Generic record extension, generic errors/choices, generic cleanup hooks and generic OPERATOR declarations are not implemented. Existing nongeneric extension can contain generic-record fields. JSON/FORMAT adapters work for eligible concrete records; opaque storage still needs a deliberate formatter or logical snapshot.

Pass a named function

FUNCTION Apply(value AS INTEGER, action AS CALLABLE(INTEGER) AS INTEGER) AS INTEGER
    RETURN action(value)
END FUNCTION
FUNCTION DoubleIt(value AS INTEGER) AS INTEGER = value * 2
PRINT Apply(21, DoubleIt)

CALLABLE lists parameter types and modes, then an optional successful result type. Without a result it describes a SUB. A failure set uses FAILS (ErrorType, ...). Indirect calls enforce those contracts. Default parameter names/values on the original declaration do not become an unchecked indirect-call feature.

Named functions do not capture locals. Pass explicit data, store independent configuration in supported records, or use an anonymous function when a snapshot of outer copyable data is useful. A named local helper remains noncapturing even though its declaration is nested.

Lambdas: short functions written where needed

A lambda is an anonymous function value. The expression form is (parameters) => expression. Parameter types can be explicit or supplied by an expected concrete CALLABLE, as with a collection predicate.

minimum = 10
selected = Filter([1, 10, 20], (n) => n >= minimum)
PRINT LEN(selected)
square = (x AS DOUBLE) => x * x
PRINT square(3.0)

Filter supplies INTEGER parameter context for n. A stand-alone square needs its parameter type. The successful result is inferred from the expression. () is the parameter list for a zero-argument lambda.

Closures own snapshots, rather than shared variables

A closure is a function value together with data it captured from its creation scope. A free local is a name used by the function that is not one of its own parameters or locals. Copyable free locals become independent read-only snapshots when the closure is constructed:

values = [10, 20]
first = () => values[0]
copy = first
values[0] = 99
PRINT first()
PRINT copy()
PRINT values[0]

Both function values still return 10. The snapshot includes owned strings, collections and other copyable callables. A closure can be returned after its creator exits because it owns its captures; it does not retain dangling borrowed locals. Copying a closure creates an independent value with ordinary cleanup.

For several statements, use an anonymous FUNCTION or SUB block:

offset = 2.0
transform = FUNCTION(x AS DOUBLE) AS DOUBLE
    adjusted = x + offset
    IF adjusted < 0.0 THEN RETURN 0.0
    RETURN adjusted * adjusted
END FUNCTION
notify = SUB(message AS STRING)
    PRINT message
END SUB
PRINT transform(3.0)
notify("Done")

Block lambdas infer checked escaping failures unless explicitly declared with FAILS. Successful returns must be consistent and complete. Collection predicates still require an infallible callback: a lambda does not weaken the receiving API’s contract. Other checked callbacks propagate their real failures as in chapter 11. Fields, list elements and returned CALLABLE values can be invoked directly.

Captured bindings are read-only. They do not provide mutable shared cells, and resources or task/channel capabilities cannot be captured. Pass such owners explicitly to an operation or an owned-context API. For example, this is rejected:

file = OpenFile("/dev/null")
inspect = () => GetFileInfo(file).Size

TAKE is not an implicit capture modifier. Noncapturing foreign CALLBACK and NOSUSPEND delegates retain their separate restrictions; anonymous adaptation to those kinds and nested named declarations inside anonymous bodies are unsupported. See callables and captures .

Supply a sort comparator

values = [1, 3, 2]
Sort(BYREF values, Descending)
FOR EACH value IN values
    PRINT value
NEXT
FUNCTION Descending(a AS INTEGER, b AS INTEGER) AS INTEGER
    IF a > b THEN RETURN -1
    IF a < b THEN RETURN 1
    RETURN 0
END FUNCTION

An INTEGER comparator returns negative, zero or positive according to the desired ordering. CompareResult overloads provide typed Less/Equal/Greater/Unordered outcomes. Comparators are checked and must satisfy their supported contract. The collection reference covers callback signatures and unordered handling.

An ordinary capturing lambda can supply comparison policy too:

direction = -1
comparator = (a AS INTEGER, b AS INTEGER) => direction * (a - b)
FOR EACH value IN Sorted([1, 3, 2], Compare=comparator)
    PRINT value
NEXT

Here subtraction is safe for the small demonstrated values. For arbitrary INTEGER inputs, branch on comparisons as Descending does instead of risking overflow in a - b. The lambda snapshots direction at construction and remains subject to the comparator’s infallible, read-only access contract.

Define a type’s equality, ordering and hash

Default ordinary-record equality compares all fields. Sometimes a value’s application identity uses fewer fields. Canonical OPERATOR declarations make that policy consistent across equality, nested values and hash collections.

TYPE Person
    Name AS STRING
    Age AS INTEGER
END TYPE
OPERATOR Equal(a AS Person, b AS Person) AS BOOLEAN = a.Name = b.Name
OPERATOR Hash(value AS Person) AS INTEGER = Hash(value.Name)
OPERATOR Compare(a AS Person, b AS Person) AS CompareResult = Compare(a.Name, b.Name)
a = Person("Ada", 30)
b = Person("Ada", 40)
PRINT a = b
DIM people AS SET OF Person = SET()
Insert(BYREF people, a)
Insert(BYREF people, b)
PRINT LEN(people)

These declarations belong in the type’s defining module. Merely naming a normal function Equal does not register a capability. Custom Equal disables incompatible generated Hash/Compare; supply coherent operations when needed. Equal keys must hash equally, and ordering must agree with equality. The compiler checks exact signatures and effects but cannot prove your mathematical laws.

Equality, ordering and hash operators borrow operands and cannot transfer them. They cannot perform I/O, inspect clocks/randomness, start tasks, explicitly suspend or invoke unverified foreign/indirect behavior. Resource operators are rejected. A derived record does not virtually inherit a base’s canonical operator. The operator reference gives exact effects, unordered behavior and nested dispatch rules.

Define a readable text representation

OPERATOR Display gives a nominal record, choice, error or enum a deliberate text representation. PRINT, interpolation, explicit Display and logging use the same capability. An ordinary FUNCTION Display does not register it.

TYPE Person
    Name AS STRING
    Age AS INTEGER
END TYPE
OPERATOR Display(value AS Person) AS STRING
    RETURN f"{value.Name}, age {value.Age}"
END OPERATOR
person = Person("Ada", 36)
PRINT person
PRINT f"Person: {person}"
PRINT Display(person)
PRINT f"[{person:.3}]"

The final line prints [Ada]: formatting first obtains text, then applies STRING precision/alignment/fill rules. Numeric format specifications are rejected for a displayed nominal value. There is no automatic field dump, implicit concatenation or general conversion to STRING for an unregistered type.

Declare one module-level hook in the exact type’s defining module, with one required read-only operand and an owned STRING result. Generic/default/owning parameters, BORROW results and escaping FAILS are not allowed. The hook and its helpers follow the same pure-computation restrictions as other canonical operators. Ordinary extension does not inherit it.

Display also supports resources without copying or transferring them:

TYPE Ticket
    Name AS STRING
END TYPE
SUB __Cleanup(BYREF ticket AS Ticket)
    PRINT "Released "; ticket.Name
END SUB
OPERATOR Display(ticket AS Ticket) AS STRING = "Ticket: " + ticket.Name
ticket = Ticket { Name="job" }
PRINT ticket
PRINT ticket.Name

The owner remains usable and closes once at scope exit. Display adds no resource equality, hashing or copying. Direct nominal logging arguments are rendered only after severity filtering; repeated message fields reuse that text. Explicit Display/LogArgument calls are ordinary eager expressions. A deferred function retains its actual errors and normal result cleanup. See the public value-operator reference .

From the BASIL documentation. View source ↗

Find your way around BASIL

Type to search the manual.