Records and methods

Group fields into TYPE records, copy them as values, reach their fields with WITH, and give them methods, a constructor and a destructor.

In one line#

TYPE declares a record: a group of named fields, each with its own type. A record is a value, like a number: assigning it to a declared variable makes a copy. METHOD adds functions that belong to the record, with SELF inside them.

TYPE Ship
  name AS string
  fuel AS integer
  speed AS float
ENDTYPE
hero AS Ship
hero.name = "Comet"
hero.fuel = 80
hero.speed = 2.5
Console(hero.name + " " + Str(hero.fuel) + " " + Str(hero.speed))   ' Comet 80 2.5

TYPE … ENDTYPE#

TYPE <Name>
  <field>
  …
  [METHOD …]
ENDTYPE
Field formThe field holds
namea 32-bit integer
name$a string
name#a decimal number
name AS <type>that type: integer, float, string, uint8… or another record type
name AS <type> [<last index>, …]an array of that type
name [<last index>, …]an array of integers
LIST name.<type>()a list, see Collections
MAP name.<type>([<slots>])a map
ARRAY name.<type>(<last index>, …)an array, declared the PureBasic way
TYPE Player
  name AS string
  score
  lives AS uint8
  slots AS integer [3]
  LIST items.s()
  MAP counts.i()
ENDTYPE
p AS Player
p.name = "Ada"
p.score = 2.9
p.lives = 260
p.slots[3] = 7
AddElement(p.items())
p.items() = "Potion"
p.counts("Potion") = 3
Console(p.name + " " + Str(p.score) + " " + Str(p.lives) + " " + Str(p.slots[3]))   ' Ada 2 4 7
Console(p.items() + " " + Str(p.counts("Potion")))   ' Potion 3

A field with no sigil and no AS is a whole number. score above stored 2.9 as 2. Positions and speeds need AS float (32-bit) or AS float64. AGK and DarkBASIC have the same default.

  • A new record starts with 0 in numbers, "" in strings, and empty collections.
  • Writing a field converts the value to the field's type: lives AS uint8 turned 260 into 4.
  • Field names ignore case, and two fields with the same name are a compile error: "Duplicate BASIC field in Player: …". Two types with the same name are refused too.
  • A field can use a record type declared later in the program.
  • An unknown field type is a compile error: "Unknown BASIC field type".
  • The closing word is ENDTYPE. END TYPE (QBasic, VB) is refused:
TYPE Point
  x
END TYPE

⚠️ Prefer AS to the sigils. A field written name$ keeps its type only while the same name is never used without the $ anywhere in the program. A variable called name elsewhere is enough: the field then becomes a whole number, and storing text in it stops the program. name AS string has no such problem.

⚠️ Avoid one-letter field names among b a w u l i q f d s c. These letters are PureBasic type suffixes. Inside a method, SELF.w = 10 is read as "declare a 16-bit variable", and the field is never written. Name it width.

PureBasic's field suffix (x.f) is not accepted inside a TYPE. Write x AS float.

The one-line form#

TYPE Point x AS float y AS float ENDTYPE
TYPE Hero name AS string hp AS integer bag AS integer [2] ENDTYPE
p AS Point
p.x = 1.5
h AS Hero
h.bag[2] = 4
Console(Str(p.x) + " " + Str(h.bag[2]))   ' 1.5 4

The whole TYPE fits on one line when its fields are simple: a name, an optional AS <type>, an optional [bounds]. LIST, MAP and ARRAY fields and methods need the long form.

Records inside records#

A field can be a record. A TYPE can even be declared inside another one; its name is still usable everywhere.

TYPE Vec
  x AS float
  y AS float
ENDTYPE
TYPE Ship
  pos AS Vec
  vel AS Vec
ENDTYPE
s AS Ship
s.pos.x = 3
s.vel.y = -1.5
Console(Str(s.pos.x) + " " + Str(s.vel.y))   ' 3 -1.5

Array fields#

Index an array field with one pair of square brackets per dimension: b.grid[1][2]. The other spellings compile and go wrong: b.grid(1, 2) stops the program when it runs, and b.grid[1, 2] silently reaches another cell.

TYPE Board
  ARRAY grid.i(2, 2)
  marks AS integer [4]
ENDTYPE
b AS Board
b.grid[1][2] = 5
b.marks[4] = 1
Console(Str(b.grid[1][2]) + " " + Str(b.marks.length))   ' 5 4

The array members (.length, .insert, .sort()…) work on array fields too. See Collections.

Declaring a record#

<name> AS <Type>
<name> AS <Type> = <record>
<name> AS <Type> = NEW <Type>(<arguments>)
<name> AS <Type>[<last index>]
DIM <name>(<last index>) AS <Type>
NewList <name>.<Type>()
NewMap <name>.<Type>()
  • A record variable must be declared with AS. Writing q.x = 1 on a name never declared compiles, then stops the program: "q is not defined".
  • Type names ignore case: hero AS ship is fine.
  • Every declaration makes a fresh record and runs its New method, if it has one. So does every cell of a record array, every AddElement on a record list, every new map entry, and every record field inside a new record.

Reaching a field#

Where the record isSpelling
a variablehero.fuel
a record inside a records.pos.x
an array cellfleet(2).fuel or fleet[2].fuel
the current list elementfoes().hp
a map entrybag("sword").qty

The backslash spelling hero\fuel belongs to WITH blocks. Outside one, it is refused with "Not a BASIC statement".

⚠️ A write through a list element or a map entry is not converted to the field's type: foes().hp = 3.7 keeps 3.7 in an integer field. Writes through a variable or an array cell are converted.

Records are values#

TYPE Vec
  x AS float
ENDTYPE
TYPE Ship
  pos AS Vec
  name AS string
ENDTYPE
a AS Ship
a.pos.x = 3
b AS Ship
b = a
b.pos.x = 9
Console(Str(a.pos.x) + " " + Str(b.pos.x))   ' 3 9

Assigning a record to a declared variable makes a deep copy. Nested records, arrays, lists and maps are copied with it. The two records are then independent.

The same copy happens for x AS T = other, for a list element (l() = rec) and for a map entry (m("k") = rec). Passing a record to a FUNCTION copies it too, unless the parameter is REF, see Functions.

⚠️ Three assignments do not copy, and share instead:

AssignmentResult
fleet(1) = hero, an array cellthe cell and hero are the same record
s.pos = v, a record fieldthe field and v are the same record
w = hero, with w never declared AS Shipw and hero are the same record

After any of these, a change through one name shows through the other. To store an independent copy in an array cell or a field, copy the fields one by one:

TYPE Vec
  x AS float
  y AS float
ENDTYPE
DIM path(1) AS Vec
v AS Vec
v.x = 1
v.y = 2
path(0).x = v.x
path(0).y = v.y
v.x = 7
Console(Str(path(0).x))   ' 1, the cell kept its own value

Comparing records. a = b on two records asks whether they are the same record, not whether their fields are equal. Two separate records, even with equal fields, are never =. Compare the fields you care about.

WITH#

WITH <record>
  \<field> …
ENDWITH
TYPE Vec
  x AS float
  y AS float
ENDTYPE
TYPE Ship
  pos AS Vec
  name AS string
  fuel AS integer
ENDTYPE
s AS Ship
WITH s
  \name = "Comet"
  \fuel = 80
  \pos\x = 1.5
  \pos.y = 2.5
  Console(\name + " " + Str(\fuel) + " " + Str(\pos\x) + " " + Str(\pos\y))   ' Comet 80 1.5 2.5
ENDWITH
  • Inside the block, \field means <record>.field, anywhere on the line.
  • A chain is written \pos\x or \pos.x.
  • The block closes with ENDWITH or END WITH.
  • The record can be any expression: WITH fleet(i), WITH foes().
  • Text in strings and comments is left alone: "C:\games" stays a path.
  • WITH blocks do not nest. A WITH inside a WITH is a compile error: "Nested WITH blocks are not supported".

⚠️ The record expression is re-read at every \. The compiler writes the expression in front of each field. With WITH fleet(i), changing i inside the block moves the following lines to another cell.

WITH hero is not WITH SPRITE s. One word after WITH makes a record block. Two words, where the first names a command family, make the command shorthand: \position 100, 200 then calls SpritePosition(s, 100, 200). See Shorthands.

METHOD#

TYPE <Name>
  <fields>
  METHOD <method>([<parameters>])
    <statements>
    [EXITMETHOD [<value>]]
  ENDMETHOD
ENDTYPE
TYPE Counter
  value AS integer
  METHOD Add(amount AS integer)
    SELF.value = SELF.value + amount
    EXITMETHOD SELF.value
  ENDMETHOD
  METHOD Reset()
    SELF.value = 0
  ENDMETHOD
ENDTYPE
c AS Counter
Console(Str(c.Add(5)))   ' 5
Console(Str(c.Add(3)))   ' 8
c.Reset()
Console(Str(c.value))   ' 0
  • A method is declared inside its TYPE, among the fields.
  • SELF is the record the method was called on. Writing SELF.value changes that record, also when it is an array cell or a list element: counters(2).Reset(), counters().Add(3).
  • Call it with record.method(arguments), as a statement or inside an expression. Method names ignore case.
  • EXITMETHOD leaves the method. EXITMETHOD <value> returns a value. RETURN <value> does the same.
  • The parentheses are required, even empty: METHOD Reset().
  • A method is a FUNCTION underneath: typed parameters, default values, LOCAL variables and OPTIONAL all work. See Functions.
  • A method can call another one through SELF: SELF.Reset().
  • A copy of a record has the same methods as the original.
  • In a program with a DO loop, a program variable that a method uses must be declared GLOBAL, as for any FUNCTION.

Refused at compile time: a METHOD outside a TYPE ("METHOD requires a containing TYPE"), a METHOD inside a METHOD, two methods with the same name in one TYPE, and a value after ENDMETHOD. Unlike ENDFUNCTION, ENDMETHOD takes no value, and END METHOD is not a spelling:

TYPE Counter
  value AS integer
  METHOD Get()
  ENDMETHOD SELF.value
ENDTYPE

⚠️ Do not name a method Length, Find, Sort, Insert or Remove. These names belong to the array members, and the call stops the program when it runs.

New: the constructor#

A method named New runs every time a record of that type is created.

TYPE Counter
  value AS integer
  METHOD New(start AS integer = 0)
    SELF.value = start
  ENDMETHOD
ENDTYPE
a AS Counter
b AS Counter = NEW Counter(7)
Console(Str(a.value) + " " + Str(b.value))   ' 0 7
  • NEW <Type>(<arguments>) creates a record and passes the arguments to New. It works in a declaration and in a later assignment: b = NEW Counter(2).
  • Every other creation calls New with no argument: x AS T, each cell of DIM a(n) AS T, AddElement on a record list, a new map entry, a record field of that type.
  • A copy does not run New: b = a and x AS T = other copy the fields as they are.

⚠️ Give every parameter of New a default value. Since most creations pass no argument, a typed parameter without a default stops the program ("BASIC integer requires an integral value"), and an untyped one leaves the field without a value.

DELETE and the Delete method#

DELETE <record variable>
DELETE <array>[<index>]
DELETE <record>.<field>
alive = 0
TYPE Enemy
  hp AS integer
  METHOD New()
    alive = alive + 1
  ENDMETHOD
  METHOD Delete()
    alive = alive - 1
  ENDMETHOD
ENDTYPE
e AS Enemy
f AS Enemy
Console(Str(alive))   ' 2
DELETE e
Console(Str(alive))   ' 1
  • DELETE calls the record's Delete method, if it has one, then marks the record as deleted.
  • On a variable, DELETE also empties it. Reading a field of it afterwards stops the program.
  • On an array cell or a field, the cell stays, but calling one of its methods stops the program: "BASIC record has been deleted". The path takes square brackets: DELETE fleet[2]. DELETE fleet(2) is refused.
  • Delete runs only through DELETE. Reassigning a variable, or reaching the end of the program, does not call it.

PureBasic's Structure#

Structure … EndStructure is accepted as another spelling of TYPE … ENDTYPE, one-line form included. See Shorthands.

Structure Size width height EndStructure
s AS Size
s.width = 2
Console(Str(s.width))   ' 2

Coming from another BASIC#

You may writeIn aukimi
Type … End TypeTYPE … ENDTYPE
x.f in a Structure (PureBasic)x AS float
p\x outside a With (PureBasic)p.x
AllocateStructure (PureBasic)p AS Point, or NEW Point()
FreeStructure (PureBasic)DELETE p
END METHOD, ENDMETHOD valueENDMETHOD, with EXITMETHOD value before it
nested Withone WITH at a time