Skip to content

Procedures & functions

Procedures are where a program stops being a transcript and starts becoming a vocabulary. Name the actions with SUB; name the calculations with FUNCTION.

SUB: perform an action

DECLARE SUB DrawRule (Width AS INTEGER, Mark AS STRING)

CALL DrawRule(12, "-")

SUB DrawRule (Width AS INTEGER, Mark AS STRING)
  FOR I% = 1 TO Width
    PRINT Mark;
  NEXT I%
  PRINT
END SUB

The DECLARE publishes the signature before use. The body may appear later in the file.

Call syntax can be explicit:

CALL DrawRule(12, "-")

or in the compact form:

DrawRule 12, "-"

Use EXIT SUB for an early, intentional return.

FUNCTION: produce a value

Assign to the function's own name to set its result:

DECLARE FUNCTION Clamp% (Value AS INTEGER, Low AS INTEGER, High AS INTEGER)

PRINT Clamp%(120, 0, 100)

FUNCTION Clamp% (Value AS INTEGER, Low AS INTEGER, High AS INTEGER)
  IF Value < Low THEN
    Clamp% = Low
  ELSEIF Value > High THEN
    Clamp% = High
  ELSE
    Clamp% = Value
  END IF
END FUNCTION

A return type can be expressed by suffix or a trailing AS type. Use EXIT FUNCTION when the result is already assigned and no more work is needed.

BYREF and BYVAL choose what the callee may change

Parameters are BYREF by default. If the caller supplies a writable variable, the procedure can change that caller's storage.

DECLARE SUB Nudge (N AS INTEGER)

DIM Score AS INTEGER
Score = 40
CALL Nudge(Score)
PRINT Score          ' 41

SUB Nudge (N AS INTEGER)
  N = N + 1
END SUB

Write BYVAL when the procedure needs a value but must not rebind the caller:

DECLARE SUB PreviewNudge (BYVAL N AS INTEGER)

The extra-parentheses compatibility rule is also preserved:

CALL Nudge((Score))  ' passes a temporary value; Score is unchanged

An ordinary BYREF alias requires an exact-typed writable place. A literal or calculated expression has no place to alias, so it travels through a temporary value. A writable variable of a different numeric type is not silently converted and then presented as a reference; make the value boundary explicit instead:

DECLARE SUB ShowLong (N AS LONG)

DIM Count AS INTEGER
Count = 7

' CALL ShowLong(Count)       ' refused: INTEGER storage is not a LONG place
CALL ShowLong(BYVAL Count)   ' converted value; Count cannot be changed
CALL ShowLong((Count))       ' the same forced-value rule

That exact-place rule prevents a callee from appearing to update a caller when it was really changing a hidden conversion temporary.

A good new-code default

Mark read-only inputs BYVAL. Leave BYREF for outputs, in/out parameters, large value records that should not be copied, and APIs whose purpose is mutation.

The important question is not merely “does this copy?” It is what may the callee change that the caller can observe?

Argument kind BYVAL BYREF
numeric scalar receives an independent value may replace the caller's scalar
STRING may reassign its local parameter only may replace the caller's string
TYPE record receives an independent record copy may edit the caller's record in place
CLASS reference cannot rebind the caller, but still refers to the same object's identity may mutate the object and rebind the caller's reference
whole array A() forbidden shares the array binding; element changes are visible

Object parameters deserve a second look. BYVAL protects the caller's variable, not the object from its own methods:

SUB RenameButton (BYVAL Button AS WorksButton)
  Button.SetText "Continue"    ' same object: caller observes the new label
  Button = NOTHING             ' local parameter only: caller stays bound
END SUB

Use a return value when a function naturally produces one thing. Use a clear BYREF output when a procedure must publish several related results:

SUB DivideWithRemainder (BYVAL Value AS INTEGER, BYVAL By AS INTEGER, BYREF Quotient AS INTEGER, BYREF Remainder AS INTEGER)
  Quotient = Value \ By
  Remainder = Value MOD By
END SUB

Do not use BYREF merely to save typing. It enlarges the procedure's authority, so its name and documentation should make mutation unsurprising.

Parameter forms

DECLARE SUB Mix (BYVAL Count AS INTEGER, BYREF Total AS LONG, Names() AS STRING, BYVAL Theme AS WorksTheme)
  • Scalar parameters name one typed value or reference.
  • A() names a whole-array parameter; bounds remain available through LBOUND and UBOUND.
  • Records can travel by value or reference.
  • Class values are counted object references. BYVAL passes an alias without allowing the parameter to rebind the caller; BYREF may rebind it.

WEAK is not a parameter modifier. It is legal only on a CLASS field; parameters, locals, globals, arrays, and TYPE fields are always ordinary values or strong object references.

Whole-array parameters

DECLARE SUB ClearScores (Scores() AS INTEGER)

SUB ClearScores (Scores() AS INTEGER)
  ARRAYFILL Scores(), 0
END SUB

The empty parentheses are part of the contract: this parameter receives an array binding, not one element. See Arrays.

Scope and storage duration

Form Visibility Lifetime
module DIM module program/session
DIM SHARED procedures in the module program/session
procedure DIM current call until the call returns
procedure STATIC current procedure retained between calls
trailing STATIC procedure procedure locals retained between calls
DIM SHARED SessionName AS STRING

SUB Tick STATIC
  DIM Count AS LONG
  Count = Count + 1
  PRINT SessionName; Count
END SUB

DEF FN: the compact function

For a small expression-only calculation, classic DEF FN remains useful:

DEF FNCelsius#(Fahrenheit#) = (Fahrenheit# - 32) * 5 / 9
PRINT FNCelsius#(72)

DEF FN parameters are values. Move to a full FUNCTION when the calculation needs branches, local state, object members, or a longer explanation.

Records as results

A function can return a TYPE value. The caller receives an independent record copy:

TYPE Point
  X AS INTEGER
  Y AS INTEGER
END TYPE

DECLARE FUNCTION Origin () AS Point

FUNCTION Origin () AS Point
  Origin.X = 0
  Origin.Y = 0
END FUNCTION

This reads naturally once you remember the central rule: records are values; classes are references.

Object members

Class method signatures live inside the CLASS; bodies use qualified names at module scope:

CLASS Counter
  PUBLIC:
    Value AS INTEGER
    DECLARE SUB Add (BYVAL Amount AS INTEGER)
END CLASS

SUB Counter.Add (BYVAL Amount AS INTEGER)
  THIS.Value = THIS.Value + Amount
END SUB

Constructors, destructors, properties, access control, and virtual dispatch are taught in Records & Objects.

Design advice from a BASIC teacher

  • A SUB should sound like a verb: PaintButton, LoadPage, AdvanceFocus.
  • A FUNCTION should sound like the value it returns: Clamp, HitItem, Pending.
  • Keep I/O at the edge. A calculation that does not print, draw, or call an Intent is easier to test and easier for the optimizer to understand.
  • Prefer one clear output over a thicket of BYREF side effects.
  • Let a procedure earn its parameters. Passing half the program is usually a sign that the data wants to become a record or object.

Next: Arrays →