Do You Actually Need an LLM?

I spent a weekend running two small language models against three jobs I would normally hand to a language model without thinking about it. For one of those jobs I also wrote a regular expression, as a control.

The regex won. Thirty test cases out of thirty, in zero milliseconds, from a pattern that took an afternoon to write. The better of the two models scored twenty-eight, took five seconds a call, and the two it missed are both mistakes a regex is structurally incapable of making.

That is the kind of result I was looking for, and the question behind it is one I keep avoiding: what is the smallest amount of computation that reliably does this job? Not the cheapest model. Not the best small model. The smallest thing that works, which means the ladder has four rungs and the bottom one is not a model at all.

  1. Deterministic code. A regex, a lookup table, an if statement.
  2. A small local model. Runs on hardware you already own.
  3. A larger local model. Needs a GPU.
  4. A remote frontier model. Someone else’s hardware, metered.

Most projects start at rung four and never ask about the others. This is a weekend spent asking.

The hardware was a Raspberry Pi 4, 4 GB of RAM, heat sinks and a fan, a hundred and seventy dollars most of which I already owned. The models were smollm2:360m and qwen2.5:1.5b through Ollama. The test cases are ninety-five questions about a fictional small winery, so nothing can be looked up and every answer can be judged by reading. Sixteen of the fifty classification messages are deliberately ambiguous, because real inboxes are mostly ambiguous.

Four verdicts, assigned per workflow, to force a result you can act on instead of a number:

  • Unnecessary: deterministic code already does this.
  • Sufficient: a small local model does it well enough to run unattended.
  • Marginal: usable with a person checking the output.
  • Insufficient: the error profile or the latency is not acceptable.

And one rule, which is the part I would insist on if you copy nothing else: I wrote predictions for every workflow and committed them before I installed Ollama. Twenty-two of them, each marked confident, leaning, or coin flip. Once there is a timestamped expectation there is a defined moment when you are done, and no room to decide afterward that you knew all along. I will come back to how they scored, because it is not flattering.

Extraction: pulling an order number out of a message

Find ORD- followed by six digits, or report that there isn’t one. Thirty cases: twenty-two with an identifier, five without, and three decoys. The decoys are a membership number, a UPS tracking number, and a phone number, all of which look like identifiers and none of which are order numbers.

regex qwen2.5:1.5b smollm2:360m
Overall 30/30 28/30 8/30
Declined correctly when absent (8) 8 8 0
Decoys ignored (3) 3 3 0
Invented an answer 0 0 7

The regex won. Thirty out of thirty, in zero milliseconds, at zero watts above idle.

That result is boring until you read the two the best model missed.

On ORD-999000: wrong address, please hold. it answered 999000, dropping the prefix. Annoying, recoverable, the kind of thing you fix with a post-processing step you should not need.

On ORD-111222 please refund it answered NONE.

That is the shortest and least decorated input in the whole set. Almost the entire message is the order number. And the model returned a confident, correctly formatted assertion that there is no order number present. In a pipeline, that is a refund request for an order that exists arriving at a system that has decided it does not.

A regex cannot make that mistake. Not “rarely does” — cannot. The two errors separating 28 from 30 are both of a class that the deterministic baseline is structurally incapable of producing, which is a different and much stronger statement than a two-point accuracy gap.

The 360M result needs two numbers, not one

By the contract, smollm2:360m scored 8 of 22 on the cases that contained an identifier. That reads like a model that cannot find things.

It is not. All fourteen of its wrong answers are the correct six digits with ORD- stripped. ORD-556120 came back as 556120, fourteen times out of fourteen.

It located the right identifier in 22 of 22 and formatted it correctly in 8. Those are two different skills and the single score hides one of them. The model understood the task perfectly and could not obey the output contract, which is a completely different engineering problem than “the model is too small,” and it has a completely different fix. (Constrained decoding. I have not tested it. It is the obvious follow-up and it is not in this post.)

The decoys follow from the same gap. It handed back MEM-443322, 1Z999AA10123456784 and 503-555-0142 verbatim, all three. It has no model of the format at all. It returns whatever in the message looks most like an identifier.

Which suggests a use for it that is not extraction

A model with perfect recall and no precision is not a bad extractor. It is a candidate generator that has been asked to do the wrong job.

Twenty-two of twenty-two located, eight of twenty-two formatted. Put that output through the regex as a validator rather than trusting it directly, and you get the model’s recall with the pattern’s precision. The model proposes anything that looks like an identifier, including format variants the pattern was never written for. The pattern rejects anything malformed.

Crucially, neither half of that pipeline can produce the error the 1.5B model produced alone. The model cannot cause a false negative because it does not decide, and the regex cannot invent because it only matches.

I have not tested that composition and it is not in these numbers. But it is the one place in the extraction result where a small model has a job worth giving it, and it is a job where being sloppy is free.

Verdict for extraction itself: Unnecessary. Write the regex.

Classification: routing a message to one of six queues

Appointment, billing, availability, complaint, question, escalate. Fifty messages. The instruction said to use escalate when the message needs a person, including when you cannot tell what it is about.

qwen2.5:1.5b smollm2:360m
Overall 23/50 (46%) 15/50 (30%)
Labels used 6 of 6 3 of 6
Most common label complaint, 38% escalate, 86%
Wrong and not an escalation 26 3 (meaningless)

The 360M model answered escalate to 43 of 50 messages, which is not classification, and there is nothing further to say about it here. The rest of this section is the 1.5B model.

It is classifying, and it is not good at it. Forty-six percent, with twenty-six messages routed confidently to a wrong queue that was not the safe one.

Read those twenty-six and the verdict writes itself:

  • Someone slipped on the steps by the barrel room and hurt their wrist →
    complaint
  • A formal notice regarding a trademark → complaint
  • “I have a severe sulfite allergy, what is actually in the wine” →
    complaint
  • A reporter asking about the 2024 harvest → question
  • A distributor wanting to carry the wine in forty stores → question

An injury report, a legal notice and an allergy question, all routed to the complaints queue, confidently, with no escalation. All five billing messages about money problems also went to complaints.

Forty-six percent sounds like something you could work with. Twenty-six confident misroutes including an injury and a legal notice is not something you can work with.

And then I broke the score down by class

This is the part I nearly missed, and it is the most useful thing in the experiment.

Label Correct Of
complaint 6 6
question 9 10
availability 3 6
appointment 4 9
billing 1 7
escalate 0 12

Two classes near perfect. Four poor. One at zero.

The split is not difficulty, and it is not class size. It is what kind of label each one is.

complaint and question are topics. You can tell what a message is from the words in it. Someone complaining sounds like someone complaining.

escalate is a routing decision. Nothing in the content of a message makes it an escalation. It is an escalation because of what should happen next. “I have a severe sulfite allergy, what is actually in the wine” and “what time do you close” are both questions about the business, with the same grammar and the same tone, and one of them is a liability event.

The model did topics at 90 to 100 percent and routing decisions at zero. And billing sat in the middle for exactly the same reason: a message about being charged twice is a complaint, as text. The label is about which department owns the problem, which is not a property of the sentence.

Small models answer “what is this about.” They do not answer “what should we do about it.”

That reframes the five examples above. They are not five unlucky misroutings. All five are escalations, and escalate scored zero of twelve. The model was never going to find them, because finding them requires knowing a consequence that the text does not contain.

Two caveats on how hard to lean on this. The classes hold between six and twelve cases each, so “six of six” is six cases and the individual numbers are thin. And this is one model. The pattern across all six classes is consistent enough that I believe the explanation, but I am offering it as the best account of the data rather than as a measurement.

Verdict: Insufficient. And the actionable version: if one of your labels means “a human needs to see this,” a small model will not find it. Build that one as a rule.

Grounded QA: answering a question from four documents

Fifteen questions against a 650-token corpus: club policy, tasting room, production notes, shipping. Eight are answerable from one passage, three require combining two, and four have no answer in the material at all.

I read all thirty answers by hand against the corpus. That is the only way to score this, and I will say why in a moment.

qwen2.5:1.5b smollm2:360m
Single passage (8) 8/8 7/8
Two passage (3) 1/3 0/3
Answerable total (11) 9/11 7/11
Correctly said “not in the material” (4) 2/4 0/4

Three boundaries separate cleanly here, and they are the useful result.

Retrieval works

The 1.5B model answered all eight single-passage questions correctly. Six bottles in the Vineyard tier. Twenty dollars per person. September 9th. Thirty-four states. No dogs indoors. 21.8 Brix on the 12th. Twenty-five dollar restocking fee. Ten people on the barrel room tour.

Asked what happens if you join the club and immediately want out, it returned the two sentences that answer the question and stopped:

Cancellation requires one completed shipment. Members who cancel before receiving a shipment are charged for that shipment.

That is competent lookup across four documents, on a $170 computer, with the wifi switched off. I did not expect it and it is the most encouraging thing in the weekend.

Combination does not

One club member brings five guests on a Saturday. Club policy waives the tasting fee for the member plus three guests. The tasting room charges twenty dollars a head. Six people, four waived, two paying: forty dollars.

Both models answered twenty.

Neither could hold “plus three guests” against a party of six and subtract. The 1.5B model did get the second half right, correctly working out that a party of six is not a group larger than six and therefore needs no reservation, which is the only piece of multi-document reasoning either model performed all weekend.

The second two-passage question is the one I keep thinking about. Asked why someone would not find a 2024 rose and what they would be offered instead, the 1.5B model quoted the corpus exactly, and then:

…the reference material states that “We did not bottle a rose in 2024. The fruit that would normally go to rose was needed to make volume on the estate Pinot Noir.” Therefore, the 2024 rose would be replaced with the fruit that would typically go into the estate Pinot Noir production.

The quotation is verbatim and right. The next sentence reverses the only relationship in it. The fruit went into the Pinot Noir, so what you would be offered is the Pinot Noir. You cannot be offered fruit.

Correct retrieval and inverted inference, in consecutive sentences, with the evidence on screen between them.

The 360M model’s multi-document failures are worse than wrong. They are contaminated. It took the fourteen-days-notice rule, which governs skipping a club shipment, and applied it to booking a tasting room reservation. It reached into the shipping document to answer a cancellation question. Given more documents to search, it finds more wrong ones.

Knowing the answer is absent is the weakest link

Four questions have no answer in the corpus, chosen to be things a reader would reasonably expect a winery to have written down: a wine’s alcohol percentage, who the winemaker is, what a club tier costs, what yeast was used.

The 1.5B model abstained on two and fabricated two. The 360M model fabricated all four.

And the fabrications are a more dangerous species than invention. Asked for the alcohol percentage of the 2024 estate Pinot Noir, the 1.5B model answered:

The 2024 estate Pinot Noir had a Brix range of 23.1 to 24.6 at pick.

Every component of that is true. Right wine, right vintage, right numbers, right document. Brix is sugar at harvest and has nothing to do with alcohol in the bottle. It reached for the nearest real fact instead of making one up, and produced something that reads as expertise while answering a question nobody asked. If you do not know what Brix is, that answer is indistinguishable from a correct one.

Both models made the same mistake on what the Cellar tier costs: both answered “twelve bottles per quarter.” Grounding gave them the right tier and they handed back the quantity in place of the price.

The one answer the evidence did not change

The 360M model, asked whether a dog can come inside the tasting room:

Yes, you can bring a dog inside the tasting room.

The corpus says the patio is dog friendly and the indoor room is not, service animals excepted. I have the same answer from a run where the model had no corpus at all, and it is identical byte for byte. Having the right answer directly in the prompt changed nothing.

In a category where being wrong means a family drives out to a winery and gets turned around at the door.

Verdict: Marginal. Usable with a person reading the output. Not usable without one.

What it costs

Workflow Model tok/s Prompt Answer Median call
extraction qwen2.5:1.5b 4.3 92 9 5.0 s
classification qwen2.5:1.5b 7.5 104 2 2.9 s
QA qwen2.5:1.5b 3.7 708 15 6.9 s

Two things in here surprised me.

The compute tax does not track parameter count. The 1.5B model is slower than the 360M on extraction, 4.3 against 5.9 tokens per second, and faster on classification, 7.5 against 6.2, because on classification it answers in two tokens where the smaller model takes six. Four times the parameters, and which one wins depends entirely on how much each one decides to say.

A bigger prompt is cheap. A longer answer is what you pay for. I have a clean comparison here by accident: an earlier QA run went out with 97-token prompts and the real one with 708. Seven times the prompt added fifteen percent to the wait before output started, because prompt evaluation parallelizes. The latency increase came from answers three times longer, plus a twenty-one percent drop in per-token generation speed from attending over a longer context. Median latency went from 3.6 to 6.9 seconds and the slowest single call took ninety-eight.

If you are sizing a local system, that is the inversion to internalize. Stuffing more context in is affordable. Letting the model be chatty is not.

And check your thermals, which is not optional advice. My first attempt at this ran with heat sinks and no fan. Thirty calls to a 360M model was enough to hit the Pi 4’s soft temperature limit and start capping the clock. vcgencmd get_throttled returned 0xe0000. I threw those latency numbers away.

Nothing in Ollama’s output, my scorer, or the result files said a word about it. Every number on screen was internally consistent and quietly describing a machine that was slowing down. The only way to find out was to ask the firmware a question I had to already suspect the answer to. Buy the fan.

And it does run with the network off. I switched the radio off, proved there was no route out, and ran one call from each workflow. All three worked. The claim that supports is narrow and worth stating carefully: this
configuration, on this afternoon, ran with no route to the internet. It is not a claim that local models are private or that this setup is secure, and a Pi can be misconfigured as thoroughly as anything in a data center. But for the specific question of whether the inference needs the network, it does not.

So: do you actually need an LLM?

Workload Verdict
Pull a known-format identifier out of text Unnecessary
Route a message to one of six queues Insufficient
Answer a question from your own documents Marginal

Three workflows that all sound like language tasks, three different answers, and not one of them is “yes, use a model.”

And the same line runs through all three. Extraction asks what this string is. Topic labels ask what this message is about. Single-passage lookup asks what the document says. Those went well, or well enough. Escalate asks what we should do, the fee question asks what the customer owes, and the unanswerable questions ask whether we know. Zero of twelve, both models wrong, and two of four.

A small model will tell you what something is. It will not tell you what to do about it. Every result here falls on one side or the other of that sentence, and I did not have it written down when I started.

Which gives four properties worth checking before you reach for one:

  1. The answer is already present in the input. Not inferable from it. Present in it.
  2. There is exactly one of it. The moment two facts have to be combined, this stops working, and the arithmetic in that tasting fee question is the proof.
  3. The output is short. Longer answers are where the latency lives on this hardware, and where the fabrication lives everywhere.
  4. Something other than the model decides whether the output is acceptable. A regex, a schema, a person, an index that either returns rows or does not.

Take away any one of those four and the experiment says do not do it. The 46 percent and the zero of twelve are what it looks like when you ask a small model to be the thing that decides.

And if you have all four, you may not need the model either. That is worth checking first, because it was true for one of my three workflows and I did not expect it to be.

Where I was wrong

Twenty-two predictions, committed before I installed anything. Ten right, eleven wrong, one unresolvable.

Two of my seven confident calls broke. I was sure the 360M model would get most of the easy extraction cases, and it got 8 of 22 by failing at format rather than at finding. I was sure it would produce invalid output often, and it did so once in fifty, then emitted the same valid word forty-three times.

But the number that bothers me is not in the confident column.

Confidence Right Wrong
Confident 5 2
Leaning 2 7
Coin flip 3 2

My hunches did worse than my coin flips. Two of ten on the calls where I had a feeling but not much behind it, three of five on the ones I explicitly labeled as guesses.

That ordering is backwards and it is the most useful thing on the sheet. On the coin flips I knew I was guessing, so I guessed cleanly. On the leaning calls I was pattern-matching against intuitions about model behavior that turned out not to describe these models at all, and the feeling of having a reason was the problem. If I had marked all twelve of those as coin flips I would have scored better.

I will take that over a clean result. The honest version of this exercise is not “here are the numbers,” it is “here is what I believed, here is what happened, and here is the gap.”

The gap, this time, was mostly in the hunches.


The harness that produced these numbers had five measurement flaws in it, none of which manufactured an entire finding out of a missing directory and nearly got published. That is a separate post.

Everything here is reproducible: fixtures, runner, scorer, the committed predictions and the run record are at
github.com/kenwalger/right-size-the-model.
Fixture set v1. Read LIMITATIONS.md before citing any of these numbers:
two models, one board, one quantization, ninety-five cases. This is an illustration of a method, not a benchmark.

Facebooktwitterredditlinkedinmail

Data Types Across Programming Languages: What You Actually Need to Know

Every introduction to programming eventually reaches data types.

An integer is a whole number. A string contains text. A Boolean is true or false. A floating-point number represents numbers with fractional components.

Then you learn another programming language and discover that apparently none of this was quite as simple as advertised.

An integer in Python is not the same thing as an integer in C. JavaScript’s number covers values other languages separate. Rust would rather make absence explicit than let null wander through a program. R has several different ways for something to be missing.

The definitions are not wrong. They are incomplete. A data type does more than tell a computer how to store a value. It expresses what a language assumes you should be able to represent, which distinctions matter, and which mistakes should be caught before the program runs.

So instead of memorizing another list of types, let’s give several languages the same small collection of facts and see what happens.

Meet Alex.

Alex is about to have a complicated afternoon, mostly because of one missing middle name.

The deceptively simple data model

Suppose our program needs to represent a customer:

name = "Alex"
age = 42
discount = 0.15
middle_name = no value
favorite_numbers = 3, 7, 11
status = ACTIVE

Nothing exotic. Text, an integer, a decimal value, an absent value, a collection, and a status chosen from a known set.

If data types were merely different spellings for the same concepts, moving this model between languages would be syntax conversion.

It isn’t. And watch which field causes the most trouble.

Python: let the values tell us what they are

Python makes our first version pleasantly straightforward.

name = "Alex"
age = 42
discount = 0.15
middle_name = None
favorite_numbers = [3, 7, 11]
status = "ACTIVE"

At runtime those values have types such as str, int, float, NoneType, and list. We did not declare them before assigning because Python is dynamically typed.

That makes Alex easy to get into the system. It also means constraints exist only if we deliberately add them.

Nothing about status = "ACTIVE" prevents a later status = "ACTVE". Python does not know our domain recognizes ACTIVE, SUSPENDED, and DELETED and has never heard of ACTVE. We can model that with an Enum, type hints, validation, or classes, but the bare string does not carry the rule.

Python’s integers hold another surprise for anyone arriving from C or Java. A normal Python int is not limited to a fixed 32-bit range.

age = 999999999999999999999999999999999999

An absurd customer age, and a perfectly valid Python integer.

What nothing looks like in Python

Spelling What it means Who makes you deal with it
None The only nothing available Nobody

Already, “integer” has stopped meaning one universal thing. And None is a single, undifferentiated way of saying nothing is here, which will turn out to be a choice rather than an inevitability.

JavaScript: one number, and two different nothings

const name = "Alex";
const age = 42;
const discount = 0.15;
const middleName = null;
const favoriteNumbers = [3, 7, 11];
const status = "ACTIVE";

Similar enough, until we ask JavaScript about the values.

typeof age;         // "number"
typeof discount;    // "number"
typeof middleName;  // "object"

Yes, typeof null returns "object", a famous historical artifact that cannot be fixed without breaking existing code.

More important for our comparison, 42 and 0.15 share the ordinary type number, based on IEEE 754 double-precision floating point. JavaScript also has BigInt for integers beyond the safe range of number.

Absence is where it gets interesting. An uninitialized variable is undefined. A variable deliberately set to nothing is null. The language provides two spellings for emptiness and then declines to tell you which one means what. In practice one tends to mean “nobody has assigned this yet” and the other “somebody decided this is empty,” except when a codebase uses them the other way around, or interchangeably.

Loose equality shows what the language really thinks. Zero, the empty string, false, and an empty array all agree they are the same thing. The two values that actually mean nothing is here do not join in:

0 == "";             // true
"" == false;         // true

null == undefined;   // true
null === undefined;  // false
null == 0;           // false
null == "";          // false

null and undefined are loosely equal to each other and to nothing else in the language. They form their own small island.

So JavaScript does distinguish absence from emptiness, and the rule is arguably the right one: a missing thing is not a zero thing. The problem is where that rule lives. It sits inside an equality operator famous for being unpredictable, next to a typeof that reports null as "object". Alex Dorey’s JavaScript Equality Table renders every comparison as a grid, if you want the full picture and a certain amount of despair.

What nothing looks like in JavaScript

Spelling What it means Who makes you deal with it
undefined Never assigned Nobody
null Deliberately empty, by convention Nobody

A type system is already more than a vocabulary list.

C: representation becomes difficult to ignore

#include <stdint.h>

typedef enum {
    ACTIVE,
    SUSPENDED,
    DELETED
} Status;

typedef struct {
    char *name;
    int32_t age;
    double discount;
    char *middle_name;
    int favorite_numbers[3];
    Status status;
} Customer;

Representation is visible almost everywhere. We chose an explicitly 32-bit signed integer for age, double for the discount, an array of exactly three integers, pointers to characters for names, and an enumeration for status.

A null pointer might represent an absent middle name:

customer.middle_name = NULL;

But C does not attach our intended meaning to that pointer. NULL here could mean Alex has no middle name, that we have not loaded it yet, that the allocation failed, or that somebody forgot to initialize the struct. The program establishes the convention and the program is responsible for respecting it. The compiler will not help.

What nothing looks like in C

Spelling What it means Who makes you deal with it
NULL pointer Whatever this program decided it means Convention, and only convention

C also forces us to care directly about ranges and representation. A fixed-width signed integer cannot grow forever. This is not C being gratuitously difficult. C was designed around a different relationship between programmer and machine, and its type system reflects that.

Java: primitives, references, and the null question

enum Status {
    ACTIVE,
    SUSPENDED,
    DELETED
}

class Customer {
    String name;
    int age;
    double discount;
    String middleName;
    int[] favoriteNumbers;
    Status status;
}

Java distinguishes primitive types such as int, double, and boolean from reference types such as String, Customer, and arrays.

An int cannot be null. A String can.

That single asymmetry does a lot of work. It means absence is available for some of Alex’s fields and structurally impossible for others, and the choice was made by whoever picked the field types rather than by anyone thinking about the domain. Unless we add conventions or tooling, String middleName does not tell us whether null is an expected state or a bug waiting for the right execution path.

What nothing looks like in Java

Spelling What it means Who makes you deal with it
null An empty reference Nobody
(not available) Primitives cannot be absent at all The compiler, by refusing

Java also has wrapper classes such as Integer, which means two things that sound like “an integer” behave differently:

int age;
Integer optionalAge;

One of those can be absent. The other cannot. Learning the keyword is only the beginning; the useful question is what guarantees come with it.

Rust: make some invalid states harder to represent

enum Status {
    Active,
    Suspended,
    Deleted,
}

struct Customer {
    name: String,
    age: u32,
    discount: f64,
    middle_name: Option<String>,
    favorite_numbers: Vec<i32>,
    status: Status,
}

Look at middle_name: Option<String>.

Instead of allowing a String to secretly contain null, Rust represents the possibility of absence in the type itself. The value is either Some(...) or None, and code consuming it has to deal with that possibility before it can get at the string.

This is the same information every other language has been carrying around implicitly, finally written down somewhere the compiler can read it. The other languages let you forget. Rust makes forgetting a compile error.

The Status enum similarly makes the valid alternatives explicit. We cannot accidentally assign ACTVE, because no such variant exists.

What nothing looks like in Rust

Spelling What it means Who makes you deal with it
None, inside Option<T> This value may legitimately be absent The compiler

A type system can encode valid states, not merely storage categories. That does not make incorrect programs impossible. It moves some classes of error from runtime behaviour into representations the compiler can reject.

The type system is participating in the design.

Go: useful zero values and deliberate simplicity

type Status string

const (
    Active    Status = "ACTIVE"
    Suspended Status = "SUSPENDED"
    Deleted   Status = "DELETED"
)

type Customer struct {
    Name            string
    Age             int
    Discount        float64
    MiddleName      *string
    FavoriteNumbers []int
    Status          Status
}

One important Go idea appears before we populate Alex at all: types have zero values. The zero value of an int is 0, of a bool is false, of a string is "". Pointers, slices, maps, functions, channels, and interfaces can be nil.

That creates a question. If Age contains 0, is Alex zero years old, or did nobody supply an age?

The type cannot answer. If the application uses a zero value to stand for “not supplied,” that state becomes indistinguishable from a legitimately supplied zero. If the distinction matters, the model has to represent it deliberately, which is why MiddleName above is a *string rather than a string.
What nothing looks like in Go

Spelling What it means Who makes you deal with it
0, "", false Zero/default value; cannot by itself tell us whether a domain value was supplied The model must distinguish this if it matters
nil Empty pointer, slice, map, channel, or interface Nobody

Convenient defaults acquire meaning only when they enter a domain.

R: “missing” is not one simple state

name <- "Alex"
age <- 42L
discount <- 0.15
middle_name <- NA_character_
favorite_numbers <- c(3L, 7L, 11L)
status <- "ACTIVE"

R was designed around statistical computing, and its types and missing-value semantics reflect that.

NA represents a missing value, with typed forms such as NA_integer_ and NA_character_. R also has NULL, which is not another spelling of NA, and numeric computation can produce NaN.

These answer different questions. A missing observation in a dataset is not the same thing as the absence of an object. Neither is the same thing as a mathematically undefined result. R treats those as three distinct facts, because in statistics they are three distinct facts, and conflating them produces wrong answers rather than crashes.

Consider:

mean(c(10, 20, NA))
mean(c(10, 20, NA), na.rm = TRUE)

What nothing looks like in R

Spelling What it means Who makes you deal with it
NA A missing observation Functions ask, via na.rm
NULL No object at all Nobody
NaN An undefined numeric result Nobody

The second call makes an explicit methodological choice to exclude missing observations. The language made you say so.

The field that caused all the trouble

Alex’s missing middle name has now been through seven general-purpose languages. Stack those seven tables on top of each other and the third column tells the whole story.

Python gave it None, one undifferentiated nothing. JavaScript offered two and left the meaning to you. C gave a null pointer whose meaning lives entirely in a convention the compiler cannot see. Java made absence available for reference types and impossible for primitives, as a side effect of a decision about memory. Rust put it in the type signature and refused to let you ignore it. Go made it indistinguishable from a valid value unless you reach for a pointer. R split it into three concepts because its users needed three.

Not one of those languages is wrong. They disagree about a prior question: how much does the programmer have to admit they do not know?

Representations of absence carry semantics whether or not the programmer acknowledges them. The difference between these languages is who is required to do the acknowledging, and when.

Which is why the missing middle name turned out to be more interesting than the integer.

The table gets weird quickly

Language Typing Ordinary integers 42 and 0.15 same type? How absence is spelled
Python Dynamic Arbitrary size No None
JavaScript Dynamic Fixed (BigInt for more) Yes, both number null and undefined
C Static Fixed width, chosen Usually no Null pointer, by convention
Java Static Fixed width No null, references only
Rust Static Fixed width No Option<T>, in the type
Go Static Fixed width No Zero values, nil for some types
R Dynamic Fixed width Usually distinct NA, NULL, NaN

Closed sets of alternatives vary just as much. Rust, Java, and C have real enums, Python offers Enum if you reach for it, Go builds them from a named type plus constants, JavaScript models them by hand, and R uses factors. Collections differ too: C, Java, Rust, and Go enforce an element type in the declaration, Python and JavaScript do not, and R’s atomic vectors quietly coerce everything toward a common type.

There is no universal translation saying:

Python int = JavaScript number = C int = Java int = Rust i32 = Go int = R integer

Those types overlap. They do not carry identical guarantees, ranges, operations, failure modes, or assumptions.

The same name does not imply the same contract.

If you were designing a language, what would you choose?

This gets much more interesting when you reverse the problem.

Suppose you are designing a language.

You need an integer.

Easy.

Except now you have questions. Fixed widths, or one integer that grows until memory runs out? What happens on overflow? Should 3 become 3.0 on its own? Should "42" + 1 be legal, and if so, what should it mean?

Then absence, which by now you can see is the hard one. Do you have null? Can every reference be null, or do you require an explicit optional type? Do you distinguish a missing observation from a non-existent object? Whichever you choose, every program ever written in your language inherits that decision.

Then collections, where you decide whether elements must share a type and whether values can be moved or consumed. Then domain state, where you decide whether a programmer can say a value is only ever ACTIVE, SUSPENDED, or DELETED, and whether the compiler can prove every case was handled.

Finally, timing. When should a bad type relationship surface: while parsing, during compilation, at runtime, or only when the offending path finally executes?

There is no universally correct set of answers. There are consequences.

Every choice makes some programs easier to express, some mistakes harder to make, some implementations simpler, some runtimes more complicated, and some programmers wonder why on Earth the language designer did that.

Sometimes the answer is historical compatibility. Sometimes performance. Sometimes safety. Sometimes the problem domain.

And sometimes the language designer has a very good explanation and would appreciate it if everyone stopped bringing up typeof null.

One language where absence is not the question

Everything above is a general-purpose language, and every one of them had to decide what nothing means. That is not a universal preoccupation. It is what happens when a language expects you to model a world.

The e language, commonly associated with Cadence Specman, was designed for hardware verification. Verification code describes legal ranges of values, generates constrained test data, and explores systems whose state spaces are far too large to enumerate by hand. So its declarations carry constraints rather than storage categories:

struct customer {
    age : uint (bits: 8);
    keep age in [18..120];

    status : [ACTIVE, SUSPENDED, DELETED];
    keep soft status == ACTIVE;
};

There is no absence card for e in this article, and that is the point. In ordinary application programming we ask what type of value this is. A verification environment asks what values are legal here, and how valid combinations should be generated so we can explore what the system does.

If you look only at Python, JavaScript, Java, and C, it is easy to assume programming languages are all solving approximately the same problem with different punctuation.

They aren’t.

Types are compressed language philosophy

At the beginner level, “a type tells us what kind of data a value contains” is perfectly serviceable. Eventually that runs out of road. A type can tell us what values exist, how they are stored, which operations are permitted, which conversions happen silently, what counts as absence, whether absence must be acknowledged, whether alternatives form a closed set, and in some languages how long a value lives and who owns it. “Integer, string, Boolean, float” is not the definition of a type system. It is the introductory vocabulary.

Which is why learning the types in a new language beats memorizing a conversion table.

Python’s arbitrary-precision integers are a tradeoff. So is C asking you to care about width. Java’s primitive and reference split quietly decides which of your fields are allowed to be empty. Go buys simplicity with zero values and pays for it with ambiguity at the domain boundary. R distinguishes forms of missingness because missing observations are a normal feature of statistical work, so its problem domain is visible in its types. A verification language making constrained generation first-class tells you what its designers expected programmers to spend their days doing.

A language’s types are partly a record of what its designers thought mattered, and partly a record of what they were willing to let you avoid thinking about.

They are compressed language philosophy.

Alex survived

After all of this, Alex is still 42, still has no recorded middle name, still likes 3, 7, and 11, and remains active. Nothing about the underlying person changed. But every language forced different decisions about how those facts became computation, and the fact that changed most was the one that was not there.

Knowing that Python has int, Java has int, and Rust has i32 is useful when you are trying to get a program running. Understanding why they are not interchangeable is useful when you are trying to understand languages.

Once you start asking what a language lets you represent, what it makes you acknowledge, and what it refuses to let you say, data types stop being the boring chapter before loops. They become one of the quickest ways to see what a language believes programming should be.


What is the strangest, most useful, or most frustrating type decision you’ve encountered in a programming language? More importantly, what problem do you think the language designer was trying to solve with it? Or, if you were designing your own language, which of these decisions would you make differently?

Facebooktwitterredditlinkedinmail