Shoddy Documentation

The Beginner’s Guide

Eleven short chapters: from your first Print to a program that keeps its data on disk

Welcome. First things first: Shoddy is the language’s name, not a review of it, and it is definitely not a description of you. The name honors the shoddy woolen mills of the West Riding of Yorkshire. Those mills took worn-out rags and reclaimed them into new cloth: Useless Things Made Useful Through Skill. That is the tagline, meant literally. It is the spirit of the whole language. (The heritage of the name tells the full story of the mills in five minutes.)

This guide assumes no programming experience at all. If you have written a little code in any language you will move faster, but nothing here requires it. (The full language reference lives in spec.html. Come back to it later. This is the friendly door in.)

1. Hello, Shoddy

Shoddy is a small programming language. It looks like classic BASIC, and it uses indentation to show structure, the way Python does. One unusual rule sits at its heart: nothing ever changes. You do not modify things. You make new things from old things. That one rule gives the language most of its personality. This guide will show you it is much less strange than it sounds.

Get the mill (the Shoddy toolchain) working first. Setup is two steps: the .NET runtime, then the VS Code extension, which brings the mill and the whole library with it. The VS Code page is the tour of what that gives you: a Run button, snippets, and a real debugger. Then put this in a file called hello.shoddy:

Def Main()
    Print("HELLO, WORLD")
    Let who = Input("WHAT IS YOUR NAME? ")
    Print("NICE TO MEET YOU, " & Upper(who))

Run it. In VS Code, click the ▶ Run button. From a terminal, run bin/mill run hello.shoddy from the repo root (or just mill run hello.shoddy, from anywhere, once bin is on your PATH). Three things to notice:

Case does not matter (print, Print, and PRINT are all the same word). This guide follows a convention: PascalCase for functions and types (Print, Number, Grade), and camelCase for your own variables (count, nums). Classic BASIC all-caps still works.

2. Values, and why there’s no X = X + 1

The kinds of value you will use every day:

Let count = 42              ' a Number
Let price = 3.99            ' also a Number (there's only one kind)
Let name  = "ADA"           ' a String
Let open  = True            ' a Boolean - True or False, its own type

(' starts a comment, as does Rem.)

Let gives a value a name. Here is the big rule: a name is given its value once, and that is final. Nothing can change that value later.

Think of names as labels on values, not boxes you put things in. If you want “X, but one bigger,” that is a new value, and it deserves its own name. More often, you will pass it to a function and name it there. Every language makes you think about something. Shoddy makes you think about where new values come from. In exchange, whole categories of bugs (“who changed my variable?”) cannot happen.

A second Let with the same name inside a function changes nothing either. It makes a new label that hides the earlier one. Anything that already used the earlier value keeps it.

One more place Let works: at the very left margin, outside any Def. That makes a constant the whole program can see, set up once, before anything runs:

Let taxRate = 0.08
Let menu    = { "TEA", "SCONE", "JAM" }

Def WithTax(price As Number) As Number
    price * (1 + taxRate)

Math works as expected, including ^ for powers:

Print(2 + 3 * 4)         ' 14 - multiplication first
Print(2 ^ 10)            ' 1024
Print(17 Mod 5)          ' 2 - remainder
Print(Sqr(2))            ' 1.414213562

Strings have a family of helpers: Len, Left, Right, Mid, Upper, Lower, Instr (find a substring), Str (number to string), and Val (string to number). They come from classic BASIC, without the dollar signs BASIC put on the string ones. Try a few:

Print(Left("HELLO", 3))          ' HEL
Print(Mid("HELLO", 2, 3))        ' ELL (positions start at 1)
Print(Instr("HELLO", "LL"))      ' 3
Print("AGE: " & Str(7 * 6))      ' AGE: 42

To read a single character, use CodeAt, not Mid. They look interchangeable and they are not. Mid(s, k, 1) cuts out a one-character string. If k is past the end, it quietly hands back an empty one, so the program carries on and fails somewhere else, usually inside Asc, which is not where the mistake was. CodeAt(s, k) answers the character's code as a number and refuses at once when there is no character there, naming the position and the length. Use Codes when you mean to walk a whole string, FromCodes to build one back from codes, and InstrFrom to search from a position rather than from the start:

Print(CodeAt("HELLO", 1))              ' 72
Print(Codes("HI"))                    ' Array(72, 73)
Print(FromCodes(ToArray({ 72, 73 })))  ' HI
Print(InstrFrom("HELLO", "L", 4))      ' 4 (Instr, but starting at 4)

Booleans are their own type. A number is never “truthy” (treated as true because it is not zero). Comparisons (= <> < > <= >=) make booleans. And, Or and Not combine them.

3. Decisions and your own functions

If looks like BASIC, ends like Python:

Def Main()
    Let age = Val(Input("HOW OLD ARE YOU? "))
    If age >= 18 Then
        Print("WELCOME IN")
    Else
        Print("COME BACK IN " & Str(18 - age) & " YEARS")

One subtlety that pays off everywhere: If is an expression. Each branch’s last line is a value, and the If as a whole produces whichever branch ran. That is why functions do not need a return statement. Here is your first function:

Def Bigger(a As Number, b As Number) As Number
    If a > b Then
        a
    Else
        b

Read the header out loud: “define Bigger, which takes A as a number and B as a number, and results in a number.” The As types document what goes in and out. The mill does not check them, so keep them accurate yourself. The body has no return because the body is the result.

Print(Bigger(3, 7))      ' 7

When one value is checked against several possibilities, chained Ifs get hard to read. Select Case is the tidy version. It comes from classic BASIC, and it is also an expression:

Def Describe(n As Number) As String
    Select Case n
        Case 0
            "ZERO"
        Case 1, 2, 3            ' any of these
            "SMALL"
        Case 4 To 10            ' this range, both ends included
            "MEDIUM"
        Case Is > 10            ' any comparison (the Is is optional)
            "BIG"
        Case Else
            "NEGATIVE"

The value being tested is computed once, the first matching clause wins, and Case Else catches everything left over. You will meet it again grading students in chapter 9.

4. Doing things many times (the chapter with the twist)

Here is the twist: Shoddy has no for loop. A loop counter is a variable that changes (i becomes 2, then 3), and nothing changes here. So how does anything happen more than once? Two ways.

Way one: a function that calls itself.

Def Countdown(n As Number) As String
    If n = 0 Then
        "LIFTOFF"
    Else
        Str(n) & "... " & Countdown(n - 1)

Print(Countdown(5))      ' 5... 4... 3... 2... 1... LIFTOFF

No counter ever changed. Each call gets its own new n, one smaller. This is called recursion, and it is the loop of this language. The classic example:

Def Fact(n As Number) As Number
    If n = 0 Then
        1
    Else
        n * Fact(n - 1)

Way two (the everyday way): tell a helper what to do to each item. Most “loops” in real programs visit every item in a collection. For that, Shoddy has words that take a function as an argument:

Def Square(n As Number) As Number
    n * n

Def Main()
    Let nums = { 1, 2, 3, 4, 5 }
    Print(Map(nums, Square))          ' [ 1 4 9 16 25 ]
    Print(Filter(nums, >=(3)))        ' [ 3 4 5 ]
    Print(Fold(nums, 0, +))           ' 15
    Each(nums, Print)                 ' prints each on its own line

The big four, in plain words:

Notice the three ways we handed over the “what to do”:

  1. Just name it: Map(nums, Square), Each(nums, Print), even a bare operator: Fold(nums, 0, +). Naming a function without parentheses passes it along instead of calling it.
  2. A section: >=(3) means “≥ with the 3 already filled in”, a ready-made test for “is this at least 3?”. Same trick: *(2) doubles, =("Y") tests for a Y.
  3. An Fn, when you need something custom, right here, just once:
Print(Filter(nums, Fn(x) => x Mod 2 = 0))      ' [ 2 4 ] - the evens

And Range(a, b) makes the list a through b, which brings back the for loop’s most common use without the changing counter:

Print(Fold(Range(1, 100), 0, +))               ' 5050

5. Collections: lists and arrays

You have met lists: { 1, 2, 3 }. Lists are built for the recursion style. First(xs) is the first item, Rest(xs) is everything after, Prepend(x, xs) puts an item on the front, and IsEmpty(xs) asks if it is empty. Watch those four make a function:

Def MySum(xs As List Of Number) As Number
    If IsEmpty(xs) Then
        0
    Else
        First(xs) + MySum(Rest(xs))

That shape (“empty? done. Otherwise handle the first, recurse on the rest”) is the list idiom. You will write it many times.

Arrays are the other collection: fixed length, and any position can be read at once:

Let a = ToArray({ 10, 20, 30, 40 })
Print(Nth(a, 3))                 ' 30 - positions start at 1
Print(SetNth(a, 3, 99))          ' Array(10, 20, 99, 40)
Print(Nth(a, 3))                 ' still 30!
Let zeros = Dim(5, 0)            ' Array(0, 0, 0, 0, 0)

Look at those last lines carefully: SetNth did not change a. It handed back a new array with one spot different. Nothing ever changes, remember? “Updating” always means “getting a new one.” The rule: recurse on lists, index into arrays. Map, Filter, Fold, Each, Nth, and Length accept either.

6. Your own types: records

A student has a name and a score. Gluing those into strings or into two parallel lists gets awkward at once. Instead, declare the shape:

Type Student
    Name  As String
    Score As Number

That one declaration (it goes at the left margin, like Def) hands you three tools. A constructor:

Let s1 = Student("ADA", 96)
Let s2 = Student(Name = "GRACE", Score = 91)     ' or spell fields out

Accessors: each field name becomes a function:

Print(Name(s1))                  ' ADA
Print(Score(s1) + Score(s2))     ' 187

And With for “the same, except…”, because records do not change either:

Let retested = With(s1, Score = 100)
Print(s1)                        ' Student(Name = "ADA", Score = 96) - untouched
Print(retested)                  ' Student(Name = "ADA", Score = 100)

Two records are equal (=) when every field is equal, and two lists are equal when their items are, so a record that holds a list compares all the way down. Only a function compares by identity: two functions written separately are never equal.

Records and the chapter-4 words work well together, because accessors are functions you can pass by name:

Let class = { s1, s2, Student("LIN", 78) }
Print(Map(class, Name))                          ' [ "ADA" "GRACE" "LIN" ]
Print(Filter(class, Fn(s) => Score(s) >= 90))    ' the honor roll
Print(Fold(class, 0, Fn(t, s) => t + Score(s)))  ' 265 - total points

One more trick, now that records and Select Case have both been introduced: a Case can look inside a record. Name the type, list its fields, and the field values are yours to use, with an optional Where condition to narrow the match:

Def Intro(s As Student) As String
    Select Case s
        Case Student(n, sc) Where sc >= 90
            n & " (STAR STUDENT, " & Str(sc) & " POINTS)"
        Case Student(n, sc)
            n

Read the first clause as: “if S is a Student, calling its name N and its score SC, and SC is at least 90, then…”. The names last exactly as long as their clause. This is called pattern matching. Many newer languages offer it too, and most need more syntax for it.

Two more tricks in the same spirit. A Type can offer alternatives:

Type Shape = Circle(r) | Rect(w, h)

Def Area(s As Shape) As Number
    Select Case s
        Case Circle(r)
            Pi() * r ^ 2
        Case Rect(w, h)
            w * h

A Shape is either a circle or a rectangle, and Select Case sorts out which: no flags, no “type” field you maintain by hand. Each variant’s fields are accessors too: R(s) reads a circle’s radius. Use one only when you already know which variant you hold, because R on a Rect stops the program. And patterns nest: Case Pair(Pair(a, b), c) reaches two levels deep in one line, with _ as the name for “don't care.”

Lists take apart the same way. Case Empty matches the empty list, and Case Prepend(x, more) matches a non-empty one, naming its first item and the rest, so a walk over a list reads as the words that built it:

Def Total(xs As List Of Number) As Number
    Select Case xs
        Case Empty
            0
        Case Prepend(x, more)
            x + Total(more)

Alternatives also answer a question you will meet soon. Shoddy has no NULL, the “no value here” hole most languages leave open. So how does a function say “I have nothing for you”? By saying it in the type. The usual pair of names is Some and None, and you will meet them in many functional languages. Shoddy predeclares them, so there is nothing to write and nothing to include:

' Type Option = Some(Value) | None   — already there, don't declare it

Def Halve(n As Number) As Option
    If n Mod 2 = 0 Then
        Some(n / 2)
    Else
        None()

Def Describe(o As Option) As String
    Select Case o
        Case Some(v) Where v >= 100
            "BIG: " & Str(v)
        Case Some(v)
            Str(v)
        Case None
            "NOTHING THERE"

Halve never returns a number. It returns an Option, which is either a number wrapped in Some or a None. The only way to get the number back out is a Select Case, and a Select Case makes you say what happens on None. So the “forgot to check for nothing” bug cannot be written. One bit of small print: building an empty variant takes parentheses (None(), a constructor like any other), while matching one does not (Case None).

There is a second predeclared pair for when failure has a reason: Ok(Value) and Err(Why, At). Use Option when the only possible reason is “not there” (a missing key, an empty list) and Result when the caller could act differently depending on what went wrong. At is 0 when there is no position to report.

Neither pair gives you accessor words, on purpose. There is no Value(o) to reach past the check with. If there were, calling it on a None would fail at run time, and you would be back to NULL by another name. Select Case is the way in.

7. Under the hood: the stack

Everything you have written so far is sugar: a friendly surface over a much simpler machine. Shoddy’s real engine is a stack: a pile of values. Every operation takes its inputs off the top of the pile and puts its result back on.

When you write Hypot(3, 4) (a word from the math machine, which chapter 8 introduces), the machine runs 3 4 Hypot: push 3, push 4, run Hypot (which takes both and leaves 5). Every piece of surface syntax translates this way:

You write The machine runs Reading the right side
f(a, b) a b f push A, push B, run F
2 + 3 * 4 2 3 4 * + the * happens first
Let x = 5 5 Take x push 5, name the top value X
Map(xs, Square) xs [ Square ] Map [ ] = code passed, not run
>=(100) [ 100 >= ] a test, waiting for its input
Fn(x) => x * 2 [ Take x x 2 * ] same, with a named input
If c Then a Else b c [ a ] [ b ] Ifte pick one bundle of code and run it

The brackets are the one core idea with no everyday surface form: [ ... ] means “here is code as a thing, don’t run it yet.” That is what Map receives: a package of code to run on each item.

You can write in the machine dialect directly. Leave the parentheses off a Def header and the body is stack code:

Def Square ( Number -- Number )
    Dup *

( Number -- Number ) documents “takes a number, leaves a number.” Dup copies the top of the pile. * multiplies the top two. So: copy the input, multiply the copies. That is squaring, in two words, with no variable at all. A handful of these shuffle words (Dup, Swap, Drop…) move values around the pile.

Why would you? Because sometimes data flows left to right, and then the machine dialect is the clearer one:

Def IsEven(n As Number) As Boolean
    n Mod 2 = 0

Def EvenSquareSum(n As Number) As Number
    Range(1, n)
        Filter(IsEven)
        Map(Square)
        Fold(0, +)

That is a pipeline: the range flows into Filter, which flows into Map, which flows into Fold. It works because of the stack: each step’s result sits on the pile waiting for the next step. You now know the language’s secret: BASIC on the outside, a stack machine underneath, and the two mix on any line. (Want to watch it run? The Stack traces this exact function word by word, with the pile drawn at every step. It is the full tour of the machine this chapter only introduced.)

8. Bigger programs: Include and the library

Split programs across files with Include (left margin, near the top):

Include "seq.shoddy"

A whole standard library ships with the language: a shed full of machines, every one written in the language. Open them up. They are readable with what you know now. Four to start with:

And the shed is bigger than that:

Every machine has its own page (summary, history, guide, and word reference) in the machines catalog.

A taste:

Include "seq.shoddy"
Include "str.shoddy"

Def Main()
    Print(Sort({ 3, 1, 4, 1, 5, 9, 2, 6 }))       ' [ 1 1 2 3 4 5 6 9 ]
    Print(Join(Split("A,B,C", ","), " AND "))     ' A AND B AND C
    Print(CountIf(Range(1, 100), Fn(n) => n Mod 7 = 0))    ' 14

And one line from inside the library: Flatten is Fold(xss, { }, Concat).

When two machines want the same word

Include enough of the shed and sooner or later two machines will have used the same name. turtle has a Close: it shuts the drawing window. net has a Close too: it hangs up a socket. Include both and Shoddy stops rather than guessing:

ERROR: duplicate definition of CLOSE across machines. Include one of them under a namespace (AS)

That is deliberate. Silently picking a winner is how you spend an afternoon wondering why closing a socket redrew your window. The fix is to say which one you meant, by giving one of the two machines a namespace when you include it:

Include "net.shoddy" As Sock
Include "turtle.shoddy"

Def PackUp(t As Turtle) As Turtle
    Close(t)                 ' bare Close — the turtle's

Def Hangup(s As Number)
    SockClose(s)             ' net's, under its namespace

As Sock puts a Sock in front of everything that file declares, so net's Close becomes SockClose and Connect becomes SockConnect. It collapses to one plain word, and there is a second spelling for the same thing when it reads better with the arguments in view: Close In Sock (s). Use whichever suits the line.

Three things stop this being a nuisance.

Your own code gets the same kind of scrutiny, from two guards that have nothing to do with namespaces. Declaring a name twice is an error, and the error names the earlier file and line. A Def whose name is already a builtin is refused outright: ROUND is a builtin. A Def of that name would hide it everywhere. A Def outranks a builtin across the whole program, so Shoddy makes you say you meant it. Write Redef in place of Def when you mean it:

Redef Round(x As Number) As Number       ' yes, really, mine

There is more on all of this (the full resolution order, and how namespaced record accessors behave) in the spec.

9. A real program: the grade book

Everything so far, in about 50 lines. This is gradebook.shoddy, which ships in the repository’s tst/ folder. Run it and type a few students in:

Include "seq.shoddy"
Include "stats.shoddy"

Type Student
    Name  As String
    Score As Number

Rem Ask for students until the user types done.
Def ReadStudents(soFar As List Of Student) As List Of Student
    Let who = Input("STUDENT NAME (OR DONE)? ")
    If Upper(who) = "DONE" Then
        soFar
    Else
        Let pts = Val(Input("SCORE FOR " & who & "? "))
        ReadStudents(Append(soFar, Student(who, pts)))

Def Grade(s As Student) As String
    Select Case Score(s)
        Case >= 90
            "A"
        Case >= 80
            "B"
        Case >= 70
            "C"
        Case Else
            "F"

Def ShowStudent(s As Student)
    Print("  " & Name(s) & ": " & Str(Score(s)) & "  GRADE " & Grade(s))

Def Main()
    Print("=== GRADE BOOK ===")
    Let roster = ReadStudents({ })
    If IsEmpty(roster) Then
        Print("NO STUDENTS ENTERED.")
    Else
        Print("\nROSTER:")
        Each(roster, ShowStudent)
        Let scores = Map(roster, Score)
        Print("CLASS AVERAGE: " & Str(Average(scores)))
        Print("TOP SCORE:     " & Str(Maximum(scores)))
        Let passing = Filter(roster, Fn(s) => Score(s) >= 70)
        Print("PASSING:       " & Str(Length(passing)) & " OF " & Str(Length(roster)))
        Print("HONOR ROLL:")
        Each(Filter(roster, Fn(s) => Score(s) >= 90), ShowStudent)

Worth pausing on: the “input loop” is ReadStudents calling itself with a longer list each time, recursion doing the job a while loop would do elsewhere. And the whole report at the bottom is chapter 4’s vocabulary pointed at chapter 6’s records: Map a field out, Filter by a condition, aggregate. Once the roster exists, no line of the report can disturb it.

10. Keeping things: files

The grade book has one weakness: it forgets. You type in the whole roster, read the report, the program ends, and the roster is gone. Files are the cure. Shoddy has two kinds: text files, read and written whole, and binary files, reached into byte by byte. One convention before either. Every file word is effectful, like Print and Input: it changes the world outside the program. So keep file words at the edges, on Main’s side, and let pure functions do the thinking in between.

Text, whole files at once. A handful of builtins, with no setup and no handles (those come with binary files, below). A text file is written or read in one piece:

WriteFile("motto.txt", "USELESS THINGS MADE USEFUL")   ' replaces the file
AppendFile("motto.txt", " THROUGH SKILL")              ' adds to the end
Print(ReadFile("motto.txt"))                           ' the whole file as one String
DeleteFile("motto.txt")

Paths resolve against the working directory (the folder the program was started from). The rule to respect: ReadFile on a missing file is an error, not an empty string. Each direction has a guarded twin, and the twins are the only way to ask “can this path be read?” or “can this path be written?” FileExists can answer neither question: a directory reports False, and a file you cannot open reports True. TryWriteFile(path, s) is WriteFile answering a Boolean instead of aborting. TryReadFile(path) is ReadFile answering the language’s own Result, because a Boolean would have nowhere to put the text:

Select Case TryReadFile("motto.txt")
    Case Ok(text)
        Print(text)
    Case Err(why, _)
        Print(why)               ' CANNOT READ 'motto.txt' (NO SUCH FILE)

That is chapter 6’s Result doing what it exists for. The reason comes back as a value, the program carries on, and no FileExists guess stands between you and the answer. The reasons are Shoddy’s own short phrases (NO SUCH FILE, IS A DIRECTORY, ACCESS DENIED, UNREADABLE), never the operating system’s wording. That way a program can compare them, and a test can check them exactly.

Text, line by line. Most text files are lists of lines, and file.shoddy (chapter 8) turns them into exactly that: ReadLines gives a List Of String, WriteLines writes one back, AppendLine adds to the end. And once a file is a list, chapter 4 takes over. Here is the grade book’s cure, in three short functions:

Include "file.shoddy"                ' ReadLines, WriteLines
Include "str.shoddy"                 ' Split

Def SaveRoster(path As String, roster As List Of Student)
    WriteLines(path, Map(roster, Fn(s) => Name(s) & "," & Str(Score(s))))

Def ParseStudent(line As String) As Student
    Let bits = Split(line, ",")
    Student(First(bits), Val(Nth(bits, 2)))

Def LoadRoster(path As String) As List Of Student
    If FileExists(path) Then
        Map(ReadLines(path), ParseStudent)
    Else
        { }

Have Main call LoadRoster before asking for students, and SaveRoster after the report. Now the roster survives between runs, as ordinary text (ADA,96 per line) you can open in any editor. Notice the shape: a record becomes a string with &, and a string becomes a record with Split and Val. Map carries the whole roster each way.

But text has a limit: to change one score you load, change, and rewrite the whole file. For thirty students, fine. For a million records, there is the other kind.

Binary, random access. This comes from classic BASIC’s record files. BOpen(path) opens a file for reading and writing, creating it if absent. It hands back a handle: a Number that stands for the open file, and that you pass to every other word. Up to 16 files can be open at once, each until its BClose(h). Inside the file every byte has a position, numbered from 1. Seek(h, pos) jumps to one, BPos(h) says where you are, and BSize(h) says how big the file is.

Reading and writing go through typed pairs, and each pair advances the position as it works. PutNum/GetNum move a Number as 8 bytes. PutBool/GetBool move a Boolean as 1 byte. PutStr(h, s, Len)/GetStr(h, Len) move a fixed-length field: exactly Len bytes, zero-padded on the way in, padding stripped on the way out. A string too long for its field is an error: Shoddy aborts rather than silently cut your data short. So is reading past the end of the file.

Why fixed lengths? Because they make every record the same size, and same-size records make finding one arithmetic. A Name(12) + Score(8) student is 20 bytes, so record k starts at byte 1 + (k - 1) * 20:

Def WrStudent(f As Number, s As Student)
    PutStr(f, Name(s), 12)               ' exactly 12 bytes, zero-padded
    PutNum(f, Score(s))                  ' exactly 8

Def RdStudent(f As Number) As Student
    Student(GetStr(f, 12), GetNum(f))

Def SaveClass(path As String, roster As List Of Student)
    If FileExists(path) Then
        DeleteFile(path)                 ' BOpen never truncates - start clean
    Let f = BOpen(path)
    Each(roster, Fn(s) => WrStudent(f, s))
    BClose(f)

Def SetScore(path As String, k As Number, score As Number)
    Let f = BOpen(path)
    Seek(f, 1 + (k - 1) * 20 + 12)       ' record k, skip past the name
    PutNum(f, score)
    BClose(f)

Look at SetScore. It changes student k’s score by writing eight bytes, wherever they live, record 3 of 30 or record 900,000 of a million. It never reads, rewrites or even touches the rest of the file. That trick is the foundation every database stands on, and you wrote it in five lines. When you do this for real, recio.shoddy does the offset sums for you (GetRec, PutRec, AppendRec, AllRecs; its page works this same student file). And isam.shoddy goes one further: it finds records by key (“GRACE”) instead of by number.

A file format of your own. Records are not the only thing binary files are for. The same words also serialize: they turn a value into bytes today, and back into the same value tomorrow. Every binary format ever designed follows the same recipe. First comes a header, which says what the file is and how much follows. Then comes the payload. And the loader checks the header before trusting a single byte. In full:

Let numsMagic = 12648430                 ' C0FFEE, in hexadecimal

Def SaveNums(path As String, xs As List Of Number)
    If FileExists(path) Then
        DeleteFile(path)
    Let f = BOpen(path)
    PutNum(f, numsMagic)                 ' "this is one of ours"
    PutNum(f, Length(xs))                ' how many numbers follow
    Each(xs, Fn(x) => PutNum(f, x))
    BClose(f)

Def LoadNums(path As String) As List Of Number
    Let f = BOpen(path)
    If BSize(f) < 16 Or GetNum(f) <> numsMagic Then
        Error("NOT A NUMS FILE: " & path)
    Let n = GetNum(f)
    Let xs = Map(Range(1, n), Fn(k) => GetNum(f))
    BClose(f)
    xs

A magic number is a fixed value planted at the start of a file to say what the file is. Ours is C0FFEE in hexadecimal (base 16, where the digits run 0 to 9 then A to F), which is how a number gets to spell a word. The magic number is the check: every nums file starts with those 8 bytes. So LoadNums given somebody’s holiday photos stops with a clear error instead of reading garbage.

This is not a toy pattern. It is how neural.shoddy saves a trained network. NetSaveBin writes a magic number, the three layer sizes, then every weight as raw 8-byte doubles, the same 8 bytes PutNum moves. So NetLoadBin needs nothing but the path, and the round trip is bit-exact. (It even re-checks the file’s byte size against the header before trusting a weight.) The demographics mill ships its trained model in that format: train on Tuesday, predict on Friday. That is serialization.

There is a lesson in what that file has to contain. Weights alone are not a predictor. Whoever trained the network also decided how to scale the inputs. A program that rebuilds that decision by hand keeps a second copy of it, in another file, and nothing will notice if the two ever stop agreeing. So ModelSave writes the weights, and the scaling, and which kind of answer the network gives, behind its own magic number. ModelPredict then takes raw measurements and applies the scaling the file remembers, as the iris mill does. When you design a format, ask what the reader would otherwise have to know by other means. Put that in the file.

The complete binary-file word list, sizes and all, is one table in quickref.html.

11. Gotchas, error messages, and BASIC-veteran notes

Reading errors. Every runtime error names its line.

Things that surprise people.

For BASIC veterans specifically.

Appendix: cheat sheet

One screen of the things you will reach for first. The complete word-by-word reference (every builtin, every form) is quickref.html.

Structure Def Name(p As t, ...) As t · body = indented lines · last value = result · Def Main() runs first · Type for records · Include "F.SHODDY" · Rem or ' for comments

Bindings Let x = expr (immutable; a later Let of the same name in a body hides the earlier one)

Operators (loose → tight): Or · And · = <> < > <= >= · + - & · * / Mod · ^. Not and - are prefix. And/Or stop early

Conditionals If cond Then / Else (indented branches; it is an expression) · inline: Ifte(c, t, f) · Select Case expr with Case v / Case v1, v2 / Case a To b / Case Is >= v / Case Type(n1, n2) Where cond (destructure a record) / Case Else

Collections { 1, 2, 3 } list · ToArray / ToList / Dim(n, v) · First Rest Prepend IsEmpty (lists) · Nth SetNth Length (both, 1-based)

The big four Map(xs, f) · Filter(xs, test) · Fold(xs, start, f) · Each(xs, f). Pass functions as Name, a section >=(100), or Fn(x) => ...; Range(a, b) makes 1..N lists

Records Type + fields · t(v1, v2) or t(f1 = v1, ...) · field name = accessor function · With(r, f = v) copies-with-change

Variants Type Shape = Circle(r) | Rect(w, h) · build with Circle(2) · take apart with Case Circle(r)

Predeclared Some(Value) / None for absence, Ok(Value) / Err(Why, At) for failure with a reason: no Include, no accessor words, Select Case is the way in

Strings & Len Str Val Left Right Mid Instr InstrFrom Chr Asc Codes FromCodes CodeAt Upper Lower · escapes \n \t \r \" \\ · more (Split, Join, Trim…) in str.shoddy

Math Abs Min Max Sqr Floor Ceil Round Sin Cos Tan Atn Exp Log Pi Rnd

I/O & checks Print(v) · Input(prompt) → String · InputLine(prompt) → [atEof, line] · InKey() non-blocking key · Args() → the command line · text: ReadFile TryReadFile WriteFile TryWriteFile AppendFile FileExists DeleteFile TryDeleteFile (lines: file.shoddy) · binary: BOpen TryBOpen BClose Seek BPos BSize · PutNum/GetNum PutBool/GetBool PutStr/GetStr (records: recio.shoddy) · sockets: TCPConnect … TCPClose (gated behind --allow-net, with NetAllowed and TryTcpConnect the guarded pair that is not; friendly layer: net.shoddy) · time: Ticks Sleep Clock · Error(MSG) · Assert(COND, MSG). Chapter 10 walks through the files; the full table is in QUICKREF

The machines seq · str · regex · sinq · stats · math · eng · alg · bool · random · neural · money · fin · dict · json · xml · html · csv · shaker · matrix · lin · sparse · file · recio · isam · simplex · mip · mps · clock · julian · geo · ephemeris · net · https · vt100 · keys · terminal · scribbler · turtle · plotter · buzzer · lineshaft · cuttle · reckoner. One Include each (terminal and lineshaft need none); every one has a page in the machines catalog

Machine dialect Def Name ( in -- out ) · [ code ] quotes · Dup Drop Swap Over Rot shuffle · Take a, b names stack values · adjacency = pipeline