{"id":51184451,"url":"https://github.com/dfa1/refined-type","last_synced_at":"2026-06-27T09:04:04.190Z","repository":{"id":358261041,"uuid":"1240458199","full_name":"dfa1/refined-type","owner":"dfa1","description":"exploring value types and Valhalla","archived":false,"fork":false,"pushed_at":"2026-05-16T13:50:57.000Z","size":195,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-16T15:13:22.351Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Java","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/dfa1.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-16T06:41:43.000Z","updated_at":"2026-05-16T13:51:01.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/dfa1/refined-type","commit_stats":null,"previous_names":["dfa1/refined-type"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/dfa1/refined-type","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dfa1%2Frefined-type","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dfa1%2Frefined-type/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dfa1%2Frefined-type/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dfa1%2Frefined-type/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/dfa1","download_url":"https://codeload.github.com/dfa1/refined-type/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/dfa1%2Frefined-type/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34847288,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-27T02:00:06.362Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2026-06-27T09:04:03.069Z","updated_at":"2026-06-27T09:04:04.180Z","avatar_url":"https://github.com/dfa1.png","language":"Java","funding_links":[],"categories":[],"sub_categories":[],"readme":"# refined-type\n\nExperiments with [Project Valhalla](https://openjdk.org/projects/valhalla/) **value classes** as a refined-type library for Java.\n\nThe core question: *can a wrapper type that enforces a constraint at construction time approach the performance of a bare primitive?*\nValue classes say yes — no heap allocation, no object header, flat storage in arrays.\n\nInspired by: [Refined types in practice](https://kwark.github.io/refined-in-practice/#1) — Reference: [original gist](https://gist.github.com/dfa1/f6fdca0513730dc7dc7d6a5d89629709)\n\nBackground: [Rethink domain primitives with Valhalla](https://dfa1.github.io/articles/rethink-domain-primitives-with-valhalla)\n\n---\n\n## Why refined types?\n\nA `double` can hold any number. When you pass `lon` where `lat` is expected the compiler stays silent and the bug ships to production.\n\nRefined types encode the constraint in the type itself:\n\n```java\n// Before: primitive soup — caller must read the docs to know what's valid\nvoid route(double lat, double lon) { ... }\n\n// After: the type IS the documentation and the guard\nvoid route(Latitude lat, Longitude lon) { ... }\n```\n\nValidation runs **once**, at construction. Every subsequent use is guaranteed-valid, with no runtime checks in hot paths. Swapping `lat` and `lon` no longer compiles. Passing `200.0` no longer compiles either.\n\nThe same pattern scales to `String` when the constraint is structural. An ISIN (ISO 6166 securities identifier) is a 12-character code: two-letter country prefix, nine-character NSIN, and a Luhn check digit. A raw `String` enforces none of that:\n\n```java\n// Before: any string is accepted — validation is the caller's problem\nvoid settle(String isin, String currency) { ... }\n\nsettle(\"US037833100X\", \"USD\");   // bad check digit — compiles, fails at runtime or never\nsettle(\"USD\", \"US0378331005\");   // args swapped — compiles, silent wrong-currency settlement\n```\n\n```java\n// After: constraints live in the types\nvoid settle(Isin isin, CurrencyCode currency) { ... }\n\n// Build from components — check digit computed, cannot be wrong\nIsin apple = new Isin(new CountryCode(\"US\"), \"037833100\"); // → US0378331005\n\n// Domain logic belongs to the type, not scattered across callers\nCountryCode country = apple.country();   // → CountryCode(\"US\")\n\n// Swapping args does not compile; CurrencyCode and Isin are distinct types\nsettle(apple, new CurrencyCode(\"USD\"));\n```\n\n---\n\n## Requirements\n\n| Tool  | Version                                  |\n|-------|------------------------------------------|\n| Java  | 27 EA (Valhalla preview) — **required**  |\n| Maven | 3.9+                                     |\n\nDownload a [JEP 401 EA build](https://jdk.java.net/valhalla/) and set `JAVA_HOME` to its home directory before running any Maven command.\n\n```bash\njava -version   # must show openjdk 27-jep401ea3\n```\n\n---\n\n## Build\n\n```bash\nJAVA_HOME=\u003cvalhalla-path\u003e ./mvnw compile   # compile\nJAVA_HOME=\u003cvalhalla-path\u003e ./mvnw test      # ~500 tests\n```\n\nUse `bench.sh` to run JMH benchmarks — it sets `JAVA_HOME`, compiles, and launches JMH:\n\n```bash\n./bench.sh                             # all benchmarks\n./bench.sh SwissValorNumber            # filter by name (regex)\n./bench.sh --gc SwissValorNumber       # add GC alloc-rate profiler\n./bench.sh --layout SwissValorNumber   # JOL memory layout, then benchmarks\n```\n\n---\n\n## What's in the box\n\n### Marker interfaces\n\n| Interface         | Type      | Default `compareTo`           |\n|-------------------|-----------|-------------------------------|\n| `RefinedInt\u003cT\u003e`   | `int`     | `Integer.compare`             |\n| `RefinedShort\u003cT\u003e` | `short`   | `Short.compare`               |\n| `RefinedLong\u003cT\u003e`  | `long`    | `Long.compare`                |\n| `RefinedFloat\u003cT\u003e` | `float`   | `Float.compare`               |\n| `RefinedDouble\u003cT\u003e`| `double`  | `Double.compare`              |\n| `RefinedString\u003cT\u003e`| `String`  | `String.compareTo`            |\n\nEach implementation pins the self-type:\n\n```java\npublic value class Probability implements RefinedFloat\u003cProbability\u003e { ... }\n```\n\nWhy? To make something like `Probability.compareTo(Price)` a compile-time error. This is called\n[F-bounded](https://en.wikipedia.org/wiki/Bounded_quantification#F-bounded_quantification).\n\n### Unsigned integers (`unsigned` package)\n\nJava's primitives are signed. These value classes give proper unsigned semantics.\n\n#### Why unsigned integers matter\n\n**Domain correctness.** Quantities like a TCP port, a pixel component, a memory address offset, a file size, or a CRC checksum are inherently non-negative. Representing them as `int` or `long` lets `-1` or `-80` compile without complaint. A value class with a [0, 2³²−1] constructor rejects the invalid state at construction time.\n\n**C / C++ / Rust interop.** The Foreign Function \u0026 Memory (FFM) API (Java 22+, `java.lang.foreign`) lets Java call native code directly. C uses `uint8_t`, `uint16_t`, `uint32_t`, and `uint64_t` everywhere — network structs, OS syscalls, hardware registers. There is no automatic sign-extension barrier at the JNI / FFM boundary: a C `uint32_t` with value `4_000_000_000` arrives in Java as `-294_967_296` if you naively store it in `int`. Wrapping the raw bits in `UnsignedInt` makes the intent and the correct read-back (`asLong()`) explicit.\n\n```java\n// FFM snippet — reading a C uint32_t field from a MemorySegment\nint rawBits = segment.get(ValueLayout.JAVA_INT, offset);   // signed bits\nUnsignedInt count = UnsignedInt.ofBits(rawBits);           // wrap, no sign error\nSystem.out.println(count.asLong());                        // correct unsigned magnitude\n```\n\n**Network protocols and binary formats.** IPv4 addresses, UDP/TCP port numbers, DNS record TTLs, PNG chunk lengths, Ethernet frame types — all unsigned. Parsing binary protocols without an unsigned type forces the developer to remember `\u0026 0xFFFFFFFFL` at every read site. An `UnsignedInt` centralises that contract.\n\n**The JDK gap.** The JDK provides `Integer.toUnsignedLong`, `Integer.compareUnsigned`, and `Integer.divideUnsigned` — unsigned *operations* on signed carriers — but no unsigned *types*. These helpers are easy to forget or misapply. The types in this package wrap the exact same bit-patterns and delegate to those helpers, so the correctness burden shifts from the call site to the constructor.\n\n\u003e **Future:** Valhalla value classes + potential JDK unsigned primitives (discussed in the amber-dev mailing list) could make these wrappers zero-overhead. Until then, they carry a 16–24 byte object header on the heap — acceptable for correctness-critical code, suboptimal for hot loops (use arrays of primitives there).\n\n| Class           | Range            | Notes                                                     |\n|-----------------|------------------|-----------------------------------------------------------|\n| `UnsignedByte`  | [0, 2⁸ − 1]     | stored as `byte`; constructor takes `int`                 |\n| `UnsignedShort` | [0, 2¹⁶ − 1]    | stored as `short`; constructor takes `int`                |\n| `UnsignedInt`   | [0, 2³² − 1]     | stored as `int`; `value()` returns raw bits, `asLong()` returns magnitude |\n| `UnsignedLong`  | [0, 2⁶⁴ − 1]     | stored as `long`; every bit-pattern is valid              |\n\nArithmetic suite named after `BigInteger` / `BigDecimal`: `add`, `subtract`, `multiply`, `divide`, `remainder`. Overflow wraps mod 2^N; division uses `Integer.divideUnsigned` / `Long.divideUnsigned`.\n\n```java\nvar a = new UnsignedInt(3_000_000_000L);\nvar b = new UnsignedInt(1_000_000_000L);\nUnsignedInt sum = a.add(b);                           // 4_000_000_000 — no overflow\n\nUnsignedLong max     = UnsignedLong.MAX;              // 2^64-1, stored as -1L\nUnsignedLong wrapped = max.add(new UnsignedLong(1L)); // wraps to ZERO — mod 2^64\n```\n\n**Cross-type arithmetic** uses explicit widening — same contract as `Short.toUnsignedInt` in the JDK:\n\n```java\nUnsignedShort s = new UnsignedShort(1_000);\nUnsignedInt   i = new UnsignedInt(3_000_000_000L);\n\nUnsignedInt result = s.toUnsignedInt().add(i);        // widen first, then add\n```\n\n### Half-precision float (`fp` package)\n\n`Float16` — IEEE 754 binary16, stored in **2 bytes**. Range ±65 504, ~3.31 decimal digits.\n\n```java\nFloat16 a   = Float16.of(1.5f);\nFloat16 sum = a.add(Float16.of(0.5f));   // Float16(2.0)\nFloat16.MAX_VALUE.value();               // 65504.0f\nFloat16.NaN.isNaN();                     // true\n```\n\nArithmetic promotes through `float32` internally — the value-class benefit is **storage density**, not per-operation compute speed on the JVM. Ideal for ML weight arrays, sensor streams, HDR textures.\n\n### Examples (`examples` package)\n\n| Class               | Constraint                                                |\n|---------------------|-----------------------------------------------------------|\n| `Age`               | integer in `[0, 150]`                                     |\n| `AudioSample`       | signed 16-bit PCM sample                                  |\n| `Bic`               | ISO 9362 bank identifier (8- or 11-char input, canonical 11-char storage, `bankCode`/`country`/`locationCode`/`branchCode`) |\n| `CountryCode`       | ISO 3166-1 alpha-2 (two uppercase letters)                |\n| `CurrencyCode`      | ISO 4217 currency code (three uppercase letters)          |\n| `CusipNumber`       | 9-char CUSIP (US/Canadian securities), → `Isin`           |\n| `Distance`          | non-negative metres (with `Unit` enum: M/KM/MILE/NM)      |\n| `Email`             | coarse syntactic check                                    |\n| `Coordinate`        | (Latitude, Longitude) pair with haversine `Distance`      |\n| `HostName`          | RFC 1123 hostname with SSRF guards                        |\n| `Iban`              | ISO 13616 bank account number (MOD 97-10 checksum, `country`/`checkDigits`/`bban`; `of()` computes check digits) |\n| `Isin`              | ISO 6166 securities identifier (12 chars)                 |\n| `Latitude`          | decimal degrees in `[-90, 90]`                            |\n| `MacAddress`        | IEEE 802 MAC address (colon/hyphen/plain input, canonical `aa:bb:cc:dd:ee:ff`; `isMulticast`, `isLocallyAdministered`, `isBroadcast`) |\n| `Money`             | `Price` + `CurrencyCode`; `add`/`subtract`/`multiply`/`negate` enforce currency matching |\n| `Lei`               | ISO 17442 Legal Entity Identifier (20 chars, MOD 97-10 check digits) |\n| `Longitude`         | decimal degrees in `[-180, 180]`                          |\n| `Percentage`        | finite float in `[0, 100]`                                |\n| `Port`              | TCP/UDP port in `[0, 65 535]`                             |\n| `PositiveInt`       | integer strictly greater than zero — minimal example      |\n| `Price`             | strictly positive, finite float                           |\n| `Probability`       | finite float in `[0, 1]`                                  |\n| `Size`              | non-negative byte count (with `Unit` enum)                |\n| `Slug`              | URL-safe lowercase identifier                             |\n| `SwissValorNumber`  | SIX Valoren-Nummer, `[1, 999 999 999]`, → `Isin`          |\n| `Temperature`       | non-negative Kelvin; construct/convert via `Unit` (K/°C/°F) |\n| `Speed`             | non-negative float (m/s)                                  |\n| `Volume`            | non-negative, finite float                                |\n\n#### A worked example — securities identifiers\n\nNational identifiers compose into international ISINs with check digits computed by the type:\n\n```java\n// ABB Ltd — Swiss listing\nIsin abb = new SwissValorNumber(1_222_171).toIsin();   // CH0012221716\n\n// Apple Inc. — US listing\nIsin apple = new CusipNumber(\"037833100\").toIsin();    // US0378331005\n\n// Or construct directly from country + NSIN; check digit is computed\nIsin tesla = new Isin(new CountryCode(\"US\"), \"88160R101\"); // US88160R1014\n```\n\nThe wrong-type bug `swissValor.toIsin().equals(cusip)` doesn't compile; both ISINs do compare to each other, but a `SwissValorNumber` cannot be passed where a `CusipNumber` is expected.\n\n---\n\n## Value-class semantics\n\n### Equality and hash codes\n\nJEP 401 value objects have no identity — two instances with identical field values **are** the same object at the JVM level. `==`, `equals`, and `hashCode` all work correctly without any override:\n\n```java\nAge.of(30) == Age.of(30)                           // true\nAge.of(30).equals(Age.of(30))                       // true\nAge.of(30).hashCode() == Age.of(30).hashCode()      // true\n\nEmail.of(\"a@b.com\").equals(Email.of(\"a@b.com\"))     // true — String field compared by value\n\nCoordinate.of(Latitude.of(1.0), Longitude.of(2.0))\n    .equals(Coordinate.of(Latitude.of(1.0), Longitude.of(2.0)))  // true — multi-field\n```\n\nNo class in this repo overrides `equals` or `hashCode`. Only `toString` is overridden for human-readable output like `Age(30)`.\n\n### Pattern matching\n\nValue classes participate in Java's pattern-matching features:\n\n```java\nObject obj = Age.of(42);\n\n// instanceof pattern\nif (obj instanceof Age a) {\n    System.out.println(a.value());   // 42\n}\n\n// switch expression\nString label = switch (obj) {\n    case Age a -\u003e \"age: \" + a.value();\n    default    -\u003e \"other\";\n};\n```\n\nWorks for both primitive-backed types (`Age` wraps `short`) and `String`-backed types (`Email`).\n\n---\n\n## Framework integration\n\nRefined types are opaque to frameworks that expect primitives or `String`. Two opt-in adapters ship in this repo.\n\n### Jackson (`jackson` package)\n\nRegister `RefinedTypeModule` once:\n\n```java\nObjectMapper mapper = new ObjectMapper()\n        .registerModule(new RefinedTypeModule());\n```\n\n**Serializers** are shared via marker interface — one serializer covers all `RefinedInt` (or `RefinedString`, `RefinedShort`) implementations:\n\n```java\n// Age(42) → 42, Port(8080) → 8080, Email(\"a@b.com\") → \"a@b.com\"\nmapper.writeValueAsString(new Age(42));        // \"42\"\nmapper.writeValueAsString(new Email(\"a@b.com\")); // \"\\\"a@b.com\\\"\"\n```\n\n**Deserializers** are per concrete type — the constructor is the only valid factory and each type has its own constraint:\n\n```java\nmapper.readValue(\"42\", Age.class);          // Age(42)\nmapper.readValue(\"\\\"a@b.com\\\"\", Email.class); // Email(\"a@b.com\")\nmapper.readValue(\"151\", Age.class);         // throws InvalidDefinitionException (\u003e 150)\n```\n\nCurrently registered: `Age`, `Port`, `Email`, `CountryCode`, `CurrencyCode`.\n\nTo add a new type, implement `StdDeserializer\u003cYourType\u003e` and register it in `RefinedTypeModule`:\n\n```java\nclass PortDeserializer extends StdDeserializer\u003cPort\u003e {\n    PortDeserializer() { super(Port.class); }\n\n    @Override\n    public Port deserialize(JsonParser p, DeserializationContext ctx) throws IOException {\n        return new Port(p.getIntValue());\n    }\n}\n```\n\n### JPA (`jpa` package)\n\nAbstract base converters handle the `null` column ↔ `null` attribute contract.\nConcrete converters are one-liners:\n\n```java\n@Converter\npublic class AgeConverter extends AbstractRefinedShortConverter\u003cAge\u003e {\n    @Override\n    protected Age fromShort(short value) { return new Age(value); }\n}\n```\n\nApply with `@Convert` on the entity field:\n\n```java\n@Entity\npublic class Person {\n    @Convert(converter = AgeConverter.class)\n    private Age age;\n\n    @Convert(converter = EmailConverter.class)\n    private Email email;\n}\n```\n\n`NULL` in the column maps to `null` in the entity field; non-null values are reconstructed through the constructor so the constraint is re-validated on load.\n\nCurrently provided: `AgeConverter`, `PortConverter`, `EmailConverter`, `CountryCodeConverter`, `CurrencyCodeConverter`.\n\nAbstract bases available for new types: `AbstractRefinedIntConverter`, `AbstractRefinedShortConverter`, `AbstractRefinedStringConverter`.\n\n---\n\n## Benchmark results\n\nMeasured on JDK 27-jep401ea3 (Valhalla EA3), macOS Darwin 25.4.0, Apple Silicon.\nRun with `./bench.sh --gc \u003cfilter\u003e`. Each method uses `@Fork(1)` — separate JVM per variant\nto prevent JIT cross-contamination.\n\n### Construction — `SwissValorNumberConstructionBenchmark`\n\nBuilds 1 000 elements from pre-seeded raw ints. Allocation is the dominant cost.\n\n| Variant              | Time (µs/op) | Alloc (B/op) | What allocates                                                                              |\n|----------------------|-------------:|-------------:|---------------------------------------------------------------------------------------------|\n| `bareInt`            |        0.078 |        4 016 | one `int[1000]` (4 KB, `System.arraycopy`)                                                  |\n| `valueClass`         |        0.600 |        8 016 | one `SwissValorNumber[1000]` flat array (EA3 boxing overhead; JOL confirms 4 B/element at rest) |\n| `listOfValueClass`   |        2.814 |       28 040 | `ArrayList` header + `Object[1000]` refs + 1 000 × 24-byte boxed heap objects              |\n| `identityClass`      |        2.350 |       20 016 | one reference array + 1 000 × 16-byte heap objects                                          |\n\n**`listOfValueClass` vs `valueClass`** proves the generics-boxing penalty: `List\u003cSwissValorNumber\u003e`\nerases the flat-layout benefit, allocating 3.5× more bytes than the typed array (28 040 vs 8 016 B/op).\nUse typed arrays (`SwissValorNumber[]`) in hot paths; reserve `List\u003cT\u003e` for convenience APIs.\n\n**Identity class allocates 5× more bytes than bare int.** Value class allocates 2× on EA3 because\nthe constructor still boxes temporarily before writing into the flat array. In production Valhalla\n(JEP 401 fully optimised) value class should converge to bare int (~4 016 B/op).\n\n### Scan — `SwissValorNumberBenchmark`\n\nLinear scan for maximum over 100 000 pre-allocated elements. All variants are allocation-free\n(arrays built in `@Setup`); the difference is pure cache pressure.\n\n| Variant         | Time (µs/op) | Alloc (B/op) |\n|-----------------|-------------:|-------------:|\n| `bareInt`       |       11.927 |          ≈ 0 |\n| `valueClass`    |       29.170 |          ≈ 0 |\n| `identityClass` |       36.358 |          ≈ 0 |\n\n**Zero allocation in steady state — all three variants.** Value class is ~2.4× slower than bare int on\nEA3 (flat array traversal not yet fully JIT-optimised); identity class is ~3× slower from scattered\nheap objects stressing the prefetcher. In production Valhalla the value-class gap should close.\n\n### Haversine scan — `CoordinateBenchmark`\n\nSums haversine distances from 100 000 random world coordinates to Rome. `Coordinate` nests two\n`double`-backed value classes (`Latitude`, `Longitude`); the identity mirror stores two raw `double`\nfields. No bare-primitive baseline — the two-field identity class is already the simplest form.\n\n| Variant         | Time (ms/op) | Alloc (B/op) |\n|-----------------|-------------:|-------------:|\n| `valueClass`    |        2.956 |          ≈ 0 |\n| `identityClass` |        3.106 |          ≈ 0 |\n\nBoth are allocation-free. Value class is **~5% faster** — flat array layout improves spatial locality\nvs. the identity class's scattered 32-byte heap objects (JOL: 100 000 × 32 B = 3.6 MB vs. 400 KB flat).\n\n---\n\n## Design principles\n\n- **Constructor validates; the type proves it.** Throw `IllegalArgumentException` with a message naming the violated constraint. Never a silent truncation.\n- **No nulls.** Constructors reject null inputs; a refined type always holds a valid, non-null value.\n- **Fail fast, succeed forever.** Validation runs once at the boundary. Hot paths carry guaranteed-valid values.\n- **Explicit over implicit.** Cross-type widening is manual (`toUnsignedInt()`). Jackson integration is opt-in. Caching is opt-in.\n- **BigInteger naming for arithmetic.** `add`, `subtract`, `multiply`, `divide`, `remainder` — same names as `BigInteger`/`BigDecimal`.\n- **F-bounded markers.** `RefinedFloat\u003cT extends RefinedFloat\u003cT\u003e\u003e` blocks `probability.compareTo(price)` at compile time — same pattern as `java.lang.Enum\u003cE extends Enum\u003cE\u003e\u003e`.\n- **Length before regex.** String constructors check length (or reject obviously-wrong sizes) *before* calling the regex. An unbounded input can trigger catastrophic backtracking (ReDoS) even on simple patterns; a cheap length guard caps the input and makes regex evaluation linear in the bound.\n\n---\n\n## Trade-offs\n\nThis pattern is a net positive in the right place, with real costs. Don't apply it project-wide.\n\n### Where it pays\n\n- **Constraint lives in the type signature.** `void route(Latitude, Longitude)` cannot be called with swapped args, cannot accept NaN, cannot accept 200°. The compiler enforces what comments used to beg for.\n- **Validation runs once, at the boundary.** Hot paths carry a proof, not raw bytes that \"should be valid.\"\n- **Conversions belong to the type** (`SwissValorNumber.toIsin`, `AudioSample.toAmplitude`). Behavior moves toward data; the call site reads as domain prose.\n- **Value classes (when Valhalla lands)** push the perf overhead to zero — the wrapper and the bare primitive flatten the same.\n- **F-bound stops cross-type compares** like `probability.compareTo(price)` at compile time.\n\n### Costs to take seriously\n\n- **Boilerplate per type.** Constructor, `value()`, `toString`, test class. Eighteen refined types ≈ eighteen near-identical skeletons. Languages with first-class refinement (Scala 3 opaque types, Rust newtype, Kotlin value class) say this in one line.\n- **Boundary friction.** Jackson, JPA, JDBC, Bean Validation, MapStruct all expect primitives and `String`. Each refined type needs an adapter — or you pay deserialization tax at every external edge.\n- **Generic noise leaks.** `RefinedFloat\u003cT extends RefinedFloat\u003cT\u003e\u003e` is the right shape but ugly in error messages — new contributors stare at it.\n- **Sweet spot is narrow.** Big API surfaces, regulated domains (finance, geo, medical), public libraries — huge win. Internal scripts, throwaway endpoints, glue code — boilerplate eats the win.\n\n### Where it pays best\n\nFinancial, regulatory, safety-critical code where wrong-type bugs are expensive and inputs cross many layers. The CUSIP/Valor/ISIN chain in this repo is the canonical example: `toIsin()` is impossible to misuse, and passing an Apple CUSIP where a German WKN is expected does not compile.\n\n### Prior art worth checking\n\n| Language | Mechanism                                              |\n|----------|--------------------------------------------------------|\n| Scala    | `refined`, `iron`, opaque types                        |\n| Kotlin   | value classes (`@JvmInline`)                           |\n| Rust     | newtype pattern (`pub struct Latitude(f64)`)           |\n| Haskell  | `newtype` + smart constructors                         |\n| F#       | units of measure (catches lat/long swap at compile time *without* the wrapper noise) |\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdfa1%2Frefined-type","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fdfa1%2Frefined-type","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fdfa1%2Frefined-type/lists"}