recio turns a binary file into an evenly-spaced grid of
records — like a car park with numbered bays, all exactly the same
size. Every record takes up the same fixed number of bytes. So record number
5 always lives at the same spot no matter what's in it, and jumping
straight to it is simple arithmetic: skip over the four bays in front and
park in the fifth. That's the whole job of this machine — it does
the offset sums for you. (An offset is a distance from the start of the file,
counted in bytes.) You supply the record size in bytes and a pair of little
functions that read and write one record. recio then handles
seeking to record number k, reading it back, overwriting it in
place, adding a new one on the end, or slurping the lot into a list.
It sits directly on Shoddy's binary builtins — Seek,
BSize, and the GetStr / GetNum /
PutStr / PutNum family. That is the same foundation
isam builds its keyed files on, each machine
standing on the builtins side by side. (Those builtins aren't only
for records, either. Used freehand they serialize whole values — a
header, then a payload — the way neural's
NetSaveBin/NetLoadBin save a trained network.
The guide's chapter 10 walks
through both uses.)
Before files there were cards, and a card was a record: eighty
columns, fixed fields at fixed positions, one fact of the world per card.
That is why the tabulators, sorters and collators that processed them were
sold, for half a century, as unit record equipment. When the data
moved from drawers of cards onto tape and then disk, the shape came with it,
because the shape is what makes arithmetic possible. If every record is the
same size, record k lives at offset
(k − 1) × size. Finding it
is a multiplication instead of a search.
That multiplication is the entire foundation of random-access data processing. It is what BASIC's random files, COBOL's fixed layouts, and every indexed file since have stood on, including isam one floor up from here. This machine is the shape itself with nothing added: you say how wide a bay is, and it does the multiplication.
Sometimes you don't want a text file full of lines — you want a file of uniform records you can reach into by number. Every record is the same size, so you can read or replace any one of them without touching the rest. Change record 900 in a file of a million, and you write exactly one record's worth of bytes — not the whole file. That's the payoff of fixed-size records, and it's exactly the trick that makes random-access data files fast.
On its own, recio is the right tool when your records already
have a natural numbering — row 1, row 2, row 3 — and you want to
seek to one by that number. The moment you'd rather look records up by a
key (a name, an ID) instead of a bay number, that's where
isam comes in. It keeps a sorted index on disk that
translates a key into a record number, then leans on this very machine to
fetch the bytes. Learn recio and you've learned the floor
that isam stands on.
To use recio you decide three things and stick to them: the
byte layout of one record, a reader that pulls one record out
of an open file, and a writer that lays one back down. The
reader and writer must agree on the exact same size — that's the
contract that makes the bays line up. Records are numbered from 1.
Include "recio.shoddy"
Type Student
Name As String
Score As Number
Def RdStudent(f As Number) As Student
Student(GetStr(f, 12), GetNum(f)) ' 12 bytes name + 8 bytes score
Def WrStudent(f As Number, s As Student)
PutStr(f, Name(s), 12)
PutNum(f, Score(s))
Def Main()
Let f = BOpen("class.dat")
AppendRec(f, 20, WrStudent, Student("ADA", 96))
AppendRec(f, 20, WrStudent, Student("LIN", 78))
AppendRec(f, 20, WrStudent, Student("GRACE", 91))
Print(RecCount(f, 20)) ' 3
Print(Score(GetRec(f, 2, 20, RdStudent))) ' 78 (record 2 is LIN)
PutRec(f, 2, 20, WrStudent, Student("LIN", 85)) ' overwrite record 2 in place
Print(Score(GetRec(f, 2, 20, RdStudent))) ' 85
Each(AllRecs(f, 20, RdStudent), Fn(s) => Print(Name(s)))
' ADA LIN GRACE
BClose(f)
The 20 threaded through every call is the record size: 12
bytes for the name plus 8 for the score. A few things worth remembering:
1 + (k - 1) * size
(RecPos does this sum). Byte position 1 is the very start of the
file, so record 1 sits right at the front and each later record follows
directly on.PutRec seeks to bay k and drops the record there,
replacing whatever was in it. AppendRec asks how many records
there are with RecCount and writes the new one just past the end
— the equivalent of parking in the next empty bay.RecCount is the
file's byte length divided by the record size (rounded down), so it's
instant — it never reads a single record. It relies on the file being
nothing but whole records back to back. If you write a header or stray bytes,
the count won't mean what you expect.If you find yourself wanting to fetch a record by name rather than by bay number, don't build the index by hand — reach for isam, which does exactly that on the same binary builtins.
The twelve binary-file words the runtime dispatches — not defined here, documented here
These are not recio's Defs. The engine
dispatches them, and a Def whose name is a builtin is refused.
They are listed on this page because recio is the machine whose
domain they belong to — this whole machine is a layer over exactly
these twelve. And a reader who opens the record-I/O machine looking for
GetStr should find it here, rather than be expected to know that
the flat reference exists. They need
no Include. The same twelve are documented word for word in
machines/recio.shoddy's own header block.
Classic BASIC record files. A handle is a Number, drawn
from the same table of sixteen that sockets use. Byte positions are
1-based — counted from 1, not from 0. Every transfer
word advances the position by exactly the width it moved, and that is what
makes record layouts computable. A Name(12) + Score(8) record is 20 bytes, so
record k starts at 1 + (k-1)*20. That sum is
RecPos below, and it is the whole of what this machine adds.
Reading past end of file aborts, and so does over-running a
PutStr field. All twelve are effectful.
| Word | Description |
|---|---|
| BOpen(path) | Open path read/write and answer its
handle, creating the file if it is absent; up to 16 handles
at once. Aborts if it cannot open. The open-or-create is a trap worth naming:
asking whether a binary file is there by opening it leaves an empty file
behind when it was not. |
| TryBOpen(path) | Ok(handle), or
Err(why, 0) with why =
CANNOT OPEN 'path' (…) from the same closed set
TryReadFile uses, plus TOO MANY OPEN FILES. It
differs from BOpen twice over, and both differences are the
point. The failure is reported rather than fatal. And it opens an
existing file only. Nothing can learn a file's size or
its leading bytes without opening it, so this is the only way to pre-flight a
binary read at all. |
| BClose(h) | Close the handle and free its slot. |
| Word | Description |
|---|---|
| Seek(h, pos) | Move to byte position pos, counting
from 1. |
| BPos(h) | Where the handle is now, counting from 1. |
| BSize(h) | How many bytes the file holds.
RecCount below is this divided by the record size. |
| Word | Description |
|---|---|
| PutNum(h, x) | Write x as an 8-byte IEEE double
— the standard binary spelling of a number — and advance
8. |
| GetNum(h) | Read an 8-byte double and advance 8. |
| PutBool(h, b) | Write b as one byte and advance
1. |
| GetBool(h) | Read one byte as a Boolean and advance 1. |
| PutStr(h, s, width) | Write exactly
width bytes: s, zero-padded out to the
field. An over-length string aborts rather than being silently
truncated, which is what keeps a fixed layout honest. |
| GetStr(h, width) | Read exactly width bytes and
strip the zero padding back off. |
Every word this machine exports
| Word | Description |
|---|---|
| RecPos(k, size) | The byte position where record k
begins, computed as 1 + (k - 1) * size. Pure arithmetic —
it doesn't touch any file. This is the offset sum the rest of the machine
is built on. |
| RecSeek(f, k, size) | Moves the open file f's
position to the start of record k, so the next read or write
lands on that record. |
| RecCount(f, size) | How many whole records the file
f holds: its byte length (BSize) divided by
size, rounded down. Instant — reads no records. |
| Word | Description |
|---|---|
| GetRec(f, k, size, reader) | Seeks to record k and
returns it, using your reader function to turn the bytes into a
value. The record's type is whatever your reader produces. |
| PutRec(f, k, size, writer, v) | Seeks to record k
and writes value v there with your writer function,
overwriting whatever was in that slot. |
| AppendRec(f, size, writer, v) | Writes value v as a
brand-new record just past the current end of the file — at position
RecCount(f, size) + 1. |
| Word | Description |
|---|---|
| AllRecs(f, size, reader) | Reads every record in the file, in
order from 1 to RecCount, and hands them back as a list. Uses
your reader for each one. |
| User | How | |
|---|---|---|
| neural | GetRec
/ PutRec are the binary model file — the bit-exact
save/load contract between trainer and predictor. |
None — a thin layer over the binary-file builtins, standalone by design.