Skip to content

Procedures & functions

This page explains procedures, the named pieces a program is built from: SUB for an action and FUNCTION for a calculation that gives back a value. It covers how to declare and call them, what a procedure can and cannot change in the code that called it, where its variables live, and the classic DEF FN shorthand. After reading it you will be able to split a program into named parts and know exactly what each part is allowed to touch.

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 announces the procedure's name and parameters before it is used. The body may come later in the file.

You can call it the explicit way:

CALL DrawRule(12, "-")

or in the compact form:

DrawRule 12, "-"

Use EXIT SUB to return early on purpose.

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

The return type can be given by a suffix or by a trailing AS type. Use EXIT FUNCTION when the result is already set and there is nothing more to do.

BYREF and BYVAL choose what the callee may change

A parameter is BYREF (by reference) unless you say otherwise. That means if the caller passes a variable, the procedure is working on that very variable and can change it.

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 (by value) when the procedure needs the value but must not be able to change the caller's variable:

DECLARE SUB PreviewNudge (BYVAL N AS INTEGER)

The classic extra-parentheses rule is kept as well:

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

An ordinary BYREF parameter needs a writable place of exactly the right type to stand in for. A literal or a calculated expression has no such place, so it travels as a temporary value. A writable variable of a different numeric type is not quietly converted and then presented as if it were the original; instead you make the copy explicit:

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

The commented-out call is one the compiler will not accept, and it says why: an INTEGER variable is not a LONG place. That exact-place rule stops a procedure from appearing to update the caller's variable when it was really changing a hidden converted copy.

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 procedure change that the caller can see?

Argument kind BYVAL BYREF
numeric scalar receives an independent value may replace the caller's value
STRING may reassign its own local parameter only may replace the caller's string
TYPE record receives an independent copy of the record may edit the caller's record in place
CLASS reference cannot point the caller's variable elsewhere, but still refers to the same object may change the object and point the caller's variable at a different object
whole array A() not allowed shares the array; changes to elements are visible to the caller

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 has to hand back 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 widens what the procedure is allowed to touch, so its name and documentation should make the change unsurprising.

Parameter forms

DECLARE SUB Mix (BYVAL Count AS INTEGER, BYREF Total AS LONG, Names() AS STRING, BYVAL Theme AS WorksTheme)
  • A scalar parameter names one typed value or reference.
  • A() names a whole-array parameter; its bounds are still available through LBOUND and UBOUND.
  • Records can travel by value or by reference.
  • A class value is a reference to an object (d/OS keeps count of how many references point at each object). BYVAL passes a reference to the same object without letting the parameter point the caller's variable elsewhere; BYREF may do that too.

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 ordinary (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 signature: this parameter receives a whole array, 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 kept between calls
trailing STATIC procedure the procedure's locals kept 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 calculation that is a single expression, 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 its own independent copy of the record:

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

A class declares its methods inside the CLASS block; the method bodies use the qualified name ClassName.Method at module level:

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 input and output at the edges. A calculation that does not print, draw, or send an intent (a named request such as "fill this rectangle" or "play this tone" that the machine carries out however it can) is easier to test and easier for the optimizer to understand.
  • Prefer one clear result 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 an object.

Next: Arrays →