|
| 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