Skip to content

Appendix a5 — Bridges from other languages

This appendix is a reference set for linking the concepts of the main volume to knowledge of other languages. The Little volume proceeds presupposing only Ruby, but for readers with experience in Java, Kotlin, TypeScript, Go, PHP (Hack/PHPStan), and the like, we can build a bridge that lands as “ah, the same story as that.” Conversely, even without knowing these languages, the main volume’s argument is complete — this is a “nice to have if you know it” bonus.

It assumes readers who came here via a “for details see appendix a5” pointer from Little Part 4 (Union), Part 5 (narrowing), Part 6 (HashShape), and Part 7 (subtyping). Look up only the items you need, and return to the main volume.


Section titled “a5-1. Null safety — catching the “NPE” in the types (related to Part 5)”

If you write Java, Part 5’s User | nil should look familiar. find_user returns “if found User, if not nil” — the same setup as Java’s “User or null,” with Ruby’s nil corresponding to Java’s null. And call x.name while x is still nil, and in Ruby it’s a NoMethodError, in Java a NullPointerException (the NPE) — the same accident.

Narrowing is the machine that catches that accident ahead of time, at the type level. In the else-branch of if x.nil?, it tightens the type to “the x here is no longer nil” — the same as the habit, in Java, of writing if (x != null) { … } and then touching a field, except the type checker traces it for you automatically. You carry around “a Union containing nil” and strip nil where you pass the guard. Call .name somewhere the stripping isn’t complete (nil still remains in the type), and that’s “where the NPE comes out.”

For someone who took NullPointerException as “something that happens to crash at run time,” this is where the view shifts — null was a bug expressible in types, and preventable in types. This is the core of the idea called null safety, and Kotlin’s User? and TypeScript’s User | null are the same notion. Rigor / chibirigor hold nil as just an ordinary Union member and strip it with narrowing — standing at the doorway of null safety without adding any special syntax.


Section titled “a5-2. Nominal and structural subtyping (related to Part 7)”

If you write Java, hearing “subtype” you’ll picture inheritanceclass Dog extends Animal lets you assign Animal a = new Dog();. That is indeed one kind of subtyping, restatable as “the Dog box fits in the Animal box” (small box → big box). implements (interface implementation) is subtyping too.

But subtyping isn’t only inheritance and implementation. Recall Part 6 saying “a hash with more keys is a subtype of a hash with fewer keys” — {name:, age:} was a subtype of {name:}. Here there’s neither extends nor implementsno inheritance relation at all. It’s decided by shape alone: as long as the structure (the keys held) is there, it’s a subtype — this is called structural subtyping. Java is basically a nominal subtyping world (“decided by name, by inheritance declaration”), so this is where intuition easily slips. It might click to say Go’s interfaces and TypeScript’s object types are structural.

To sum up, subtyping has two lineages, “decided by inheritance (nominal)” and “decided by structure (structural),” and Rigor / chibirigor handle both together in one judgment: ‘does it fit in the box.’ Part 7’s accepts is its doorway (the formal treatment of when to use nominal vs. structural is in Seasoned Part 2).


Section titled “a5-3. The lineage of structural hash types — Hack → PHPStan → Rigor (related to Part 6)”

“A structural hash type that remembers keys and value types” (the main volume’s HashShape) isn’t Rigor’s invention; it’s the product of a history in which type checkers hit the same problem whenever they handled a dynamic hash.

  • Hack (Facebook): a language that added static types to PHP. It introduced the type shape('name' => string, 'age' => int) and adopted the design “spell out the keys, but allow extras (open).” A design conscious of coexisting with options hashes from the start.
  • PHPStan / Psalm: PHP’s checkers hit the same problem and introduced the same type with the notation array{name: string, age: int}. The vocabulary follows Hack, and some can state open/closed explicitly.
  • Rigor: raises types from Ruby’s RBS { name: String, age: Integer }, and likewise adopts open. It receives with “at least.”

In all three tools, a naive join (a wide type like Hash[Symbol, String | Integer]) loses the per-key information, so a type that remembers keys individually was needed. chibirigor’s HashShape is the minimal implementation of this lineage.


Section titled “a5-4. Untagged unions vs. “tagged variants” (related to Part 4)”

The Union built in Part 4 (an untagged union like Integer | String) is, in the world of the reference books, the rarer starting point. 『しくみ』deliberately avoided general union types as “too large an impact on the type system” (touching only lightly on tagged variants in a ch. 5 exercise), and what TAPL holds is also a tagged variant (a type that attaches a tag to a value to tell which type it is) — a different thing from the untagged union we build.

A tagged variant carries “a label that distinguishes, at run time, which type the contents are,” but Ruby’s values carry no such label — whether x is Integer or String you can only check on the spot with is_a?. That’s exactly why, dealing with Ruby, we essentially need an untagged union that doesn’t rely on a label, and the narrowing (Part 5) that tightens it by case.


Section titled “a5-5. The difference in direction from exhaustiveness checks — Java/C#‘s missing arm (related to Part 5)”

The “unreachable arm” reporting touched on in Part 5 is a design the reverse of Java’s and C#‘s exhaustiveness checks. Java’s and C#‘s switch / pattern matching enforce exhaustiveness, and if the cases don’t cover every pattern, the compiler stops you with a “missing arm.” Rigor (and chibirigor) don’t ask about “missing”; instead they report an “unreachable arm.” When you write if x.is_a?(String) even though x : Integer, that branch never executes — they find it and tell you “this is a superfluous branch.” chibirigor can actually emit this diagnostic too, with check --unreachable (opt-in; by default it keeps “stay quiet about working code” and stays silent. A runnable worked example is in appendix a1-3x).

Java / C#Rigor / chibirigor
What it reportsmissing arm (an arm not covered)unreachable arm (an arm never taken)
Stance toward working codestops you until you write itstays quiet about what works
Who loses outa developer who knows “that pattern won’t come”a bug that “thinks it won’t come but actually does”

This is an expression of Rigor’s value of prioritizing fewer false positives (don’t frighten working code) over soundness (cover every pattern).

© 2026 TypedDuck. Licensed under CC BY-SA 4.0.