Records and Interfaces
Use ELPS record types to keep data and the methods that change it together, with checks on every change.
A record type defines objects with named, typed fields and with methods that change them. The type checks every field when an object is made and every change a method makes. A failed method leaves the object as it was before the call.
Use a record type for a business object with rules, for example an account that must never go below zero. Use a validator for data that you only read, such as a request body.
Record types are part of the schema package (s:), from ELPS v1.95.0. Every example on this page runs in your browser. Edit an example to see the new result.
Define a record type
(set 'type-name
(s:make-record "type-name"
field...
method...
:check check-function))
- A field is
(s:has-key "name" type...)or, for a field that can be left out,(s:may-have-key "name" type...). The types are the ones a validator takes. - A method is
(s:method name (receiver args...) body...). The receiver is the object the method is called on. :checkis optional. It is a function of the object that raises an error when the object breaks a rule.- Bind the type with
setat the top level of a file.
Call the type to make an object. Call the object with a quoted name to read a field or call a method.
You can also make an object from a sorted map, for example a JSON object: (account (json:load-string "...")). The type checks each field, so a request with a bad field fails there.
Method calls are atomic
A method call works like a database transaction. If the method raises an error, or :check refuses the object after it, every field goes back to its value before the call. Code that catches the error still has a valid object.
The rollback covers one object. If a method changes two objects and then fails, the other object keeps its change. A phylum endpoint that raises an error already rolls back its whole transaction, so the rollback matters when your code catches an error and keeps going.
Rules for changes
| Rule | Why |
|---|---|
Change a field only with s:set-field!, in a method of the object's type |
So every change goes through the type's checks |
Do not change a field in :check |
:check only reads the object |
Add a method later with s:defmethod only at the top level of a file |
The type must be complete when the phylum finishes loading |
Build a new value and set it, do not change a field's value in place (for example with assoc!) |
The type cannot see or undo a change in place |
A call that breaks one of the first three rules raises record-error. The ELPS lint checks, such as record-set-outside-method, report these mistakes before the code runs.
Typed arguments
Write a method argument as [name type] to check it before the body runs. A bad call changes nothing. :returns type, after the arguments, checks the value the method returns.
Call (w 'top-up -5) alone to see the error. It names the method and the argument.
| Argument | Checked |
|---|---|
[amount positive] |
Always |
&optional [memo s:string] |
When the call gives it |
&key [fee s:number] |
When the call gives :fee |
&rest [xs s:number] |
Each item |
Use [other :self] for an argument of the type you define, and [to 'payee] for a type that is defined later in the file.
Interfaces
An interface names a set of methods. An object satisfies an interface when its type has each method, with arguments that accept each call the interface allows. A type does not need to say which interfaces it satisfies.
Use an interface when code must work with more than one record type. This example pays any object that has a pay method:
An interface is a validator. Use it as the type of a field, (s:has-key "payer" payable), or of a list, (s:of payable).
To require a type to satisfy an interface, add :implements (payable) to s:make-record. A method that does not fit then fails when the type is defined.
Use records with match
match can test the type of an object and bind its fields:
Other functions
| Function | Returns |
|---|---|
(s:fields obj) |
A copy of the object's fields as a sorted map |
(s:record? value) |
true when the value is an object of a record type |
(s:defmethod type name (receiver args...) body...) |
Adds a method to a type, at the top level of a file |
(s:ref type) |
A type that passes an object of type and does not check its fields. Use it only for objects your own code made |