Skip to content

Commit 1a2e35f

Browse files
author
MPCoreDeveloper
committed
docs: add nullable vs optional types rebuttal
1 parent 2ff7c48 commit 1a2e35f

1 file changed

Lines changed: 55 additions & 0 deletions

File tree

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Are Nullable Types Semantically Equivalent to Optional Types?
2+
3+
**Short answer: No — and here's a single real example that kills the argument.**
4+
5+
---
6+
7+
## The Claim
8+
9+
> "Types which can be nullable are semantically (and compile-time) equivalent to optional types. We already have `null` as 'absence is explicit.' Unless you want different values of absence, isn't `string?` the same as `Option<string>`?"
10+
11+
## The Killer Counter-Example
12+
13+
```csharp
14+
Dictionary<string, object> row = db.ExecuteQuery("SELECT * FROM Users WHERE Id = 2")[0];
15+
16+
string name = (string)row["Name"]; // Compiler: ✅ fine, object is non-null
17+
// Runtime: 💥 NullReferenceException
18+
```
19+
20+
The **type is non-nullable**. The **value is null**. The compiler is *happy*. The app crashes.
21+
22+
This isn't contrived — it's every ORM, every `DataReader`, every JSON deserializer, every dictionary lookup in every real application. The moment data crosses a boundary (database, network, file, reflection, interop), the compiler's nullability tracking is **erased**.
23+
24+
`Option<T>` doesn't have this failure mode. You physically cannot access the inner value without pattern-matching on `Some`/`None`. The absence is encoded in the *value*, not in a *compiler annotation that the runtime ignores*.
25+
26+
## The Precise Distinction
27+
28+
| Property | `string?` (NRT) | `Option<string>` |
29+
|---|---|---|
30+
| Where enforced | Compile-time annotation only | Runtime value — the type *is* the check |
31+
| Reflection/deserialization bypass | Yes — trivially | No — you get `None`, not a secret null |
32+
| Composable | No — manual `if (x != null)` chains | Yes — `.Map()`, `.Bind()`, `.Match()` |
33+
| Proves absence was handled | No — warnings are suppressible, not errors | Yes — won't compile without handling both arms |
34+
| Works across trust boundaries | No — external data ignores your annotations | Yes — the boundary returns `Option<T>` |
35+
36+
## Why "Different Values of Absence" Is a Red Herring
37+
38+
The argument isn't about *more kinds of absence*. It's about **where** the absence is enforced:
39+
40+
- **NRT**: The compiler *believes* the annotation. The runtime doesn't. That's a **lie** the type system tells itself.
41+
- **Option\<T\>**: The value *is* the proof. There is no gap between what the compiler knows and what the runtime does.
42+
43+
The statement *"runtime data semantics cannot always be fully proven at compile time"* means exactly this: **the compiler can annotate intent, but it cannot enforce contracts on data it has never seen** (SQL results, JSON payloads, reflection-populated DTOs). `Option<T>` closes that gap by making the proof travel *with the value*.
44+
45+
## TL;DR for LinkedIn
46+
47+
> **"Aren't nullable types just optional types?"**
48+
>
49+
> No. `string?` is a *compile-time promise* the runtime is free to break — and does, every time data comes from a database, API, or deserializer. `Option<T>` is a *runtime-enforced value* that won't let you touch the data without proving you handled absence. The gap between annotation and enforcement is where real apps crash.
50+
>
51+
> Full write-up → [Functional Null Safety in SharpCoreDB](https://github.com/MPCoreDeveloper/SharpCoreDB/blob/master/docs/FUNCTIONAL_NULL_SAFETY.md)
52+
53+
---
54+
55+
*See also: [FUNCTIONAL_NULL_SAFETY.md](./FUNCTIONAL_NULL_SAFETY.md) for tested examples, benchmarks, and the full `Option<T>` / `Fin<T>` API in SharpCoreDB v1.7.0.*

0 commit comments

Comments
 (0)