[HN Gopher] Use Your Type System
___________________________________________________________________
Use Your Type System
Author : ingve
Score : 198 points
Date : 2025-07-24 14:57 UTC (8 hours ago)
(HTM) web link (www.dzombak.com)
(TXT) w3m dump (www.dzombak.com)
| zeroCalories wrote:
| I generally agree, but I think the real strength in types come
| from the way in which they act as documentation and help you
| refactor. If you see a well laid out data model in types you
| supercharge your ability to understand a complex codebase. Issues
| like the one in the example should have been caught by a unit
| test.
| sam_lowry_ wrote:
| Also validation. In Java, you can have almost seamless
| validation on instantiation of your very objects. That's why
| having a class for IBAN instead of String containing IBAN is
| the right way to do.
| codr7 wrote:
| Allocating objects for every single property can turn pretty
| bad in Java.
|
| A strong enough type system would be a lot more useful.
| pclowes wrote:
| I like this. Very much falls into the "make bad state
| unrepresentable".
|
| The issues I see with this approach is when developers stop at
| this first level of type implementation. Everything is a type and
| nothing works well together, tons of types seem to be subtle
| permutations of each other, things get hard to reason about etc.
|
| In systems like that I would actually rather be writing a weakly
| typed dynamic language like JS or a strongly typed dynamic
| language like Elixir. However, if the developers continue pushing
| logic into type controlled flows, eg:move conditional logic into
| union types with pattern matching, leverage delegation etc. the
| experience becomes pleasant again. Just as an example (probably
| not the actual best solution) the "DewPoint" function could just
| take either type and just work.
| arrowsmith wrote:
| FYI: Ruby is strongly typed, not loosely. > 1
| + "1" (irb):1:in 'Integer#+': String can't be coerced
| into Integer (TypeError) from (irb):1:in '<main>'
| from <internal:kernel>:168:in 'Kernel#loop' from /User
| s/george/.rvm/rubies/ruby-3.4.2/lib/ruby/gems/3.4.0/gems/irb-1.
| 14.3/exe/irb:9:in '<top (required)>' from
| /Users/george/.rvm/rubies/ruby-3.4.2/bin/irb:25:in
| 'Kernel#load' from
| /Users/george/.rvm/rubies/ruby-3.4.2/bin/irb:25:in '<main>'
| kitten_mittens_ wrote:
| There seem to be two competing nomenclatures around
| strong/weak typing where people mean static/dynamic instead.
| josephg wrote:
| Some people mistakenly call dynamic typing "weak typing"
| because they don't know what those words mean. PSA:
|
| Static typing / dynamic typing refers to whether types are
| checked at compile time or runtime. "Static" = compile time
| (eg C, C++, Rust). "Dynamic" = runtime (eg Javascript,
| Ruby, Excel)
|
| Strong / weak typing refers to how "wibbly wobbly" the type
| system is. x86 assembly language is "weakly typed" because
| registers don't have types. You can do (more or less) any
| operation with the value in any register. Like, you can
| treat a register value as a float in one instruction and
| then as a pointer during the next instruction.
|
| Ruby is strongly typed because all values in the system
| have types. Types affects what you can do. If you treat a
| number like its an array in ruby, you get an error. (But
| the error happens at runtime because ruby is dynamically
| typed - thus typechecking only happens at runtime!).
| 0x457 wrote:
| It's strongly typed, but it's also duck typed. Also, in
| ruby everything is an object, even the class itself, so
| type checking there is weird.
|
| Sure it stops you from running into "'1' + 2" issues, but
| won't stop you from yeeting
| VeryRawUnvalidatedResponseThatMightNotBeAuthorized to a
| function that takes
| TotalValidatedRequestCanUseDownstream. You won't even
| notice an issue until:
|
| - you manually validate
|
| - you call a method that is unavailable on the wrong
| object.
| jnpnj wrote:
| yes, untyped names != untyped objects
| ameliaquining wrote:
| I recall a type theorist once defined the terms as follows
| (can't find the source): "A strongly typed language is one
| whose type system the speaker likes. A weakly typed
| language is one whose type system the speaker dislikes."
|
| Related Stack Overflow post:
| https://stackoverflow.com/questions/2690544/what-is-the-
| diff...
|
| So yeah I think we should just give up these terms as a bad
| job. If people mean "static" or "dynamic" then they can say
| that, those terms have basically agreed-upon meanings, and
| if they mean things like "the type system prohibits
| [specific runtime behavior]" or "the type system allows
| [specific kind of coercion]" then it's best to say those
| things explicitly with the details filled in.
| dpryden wrote:
| I think you might be thinking of
| https://cdsmith.wordpress.com/2011/01/09/an-old-article-
| i-wr...
|
| It says:
|
| > I give the following general definitions for strong and
| weak typing, at least when used as absolutes:
|
| > Strong typing: A type system that I like and feel
| comfortable with
|
| > Weak typing: A type system that worries me, or makes me
| feel uncomfortable
| johnfn wrote:
| irb(main):001:0> a = 1 => 1 irb(main):002:0>
| a = '1' => "1"
|
| It doesn't seem that strong to me.
| dgb23 wrote:
| In the dynamic world being able to redefine variables is a
| feature not a bug (unfortunately JS has broken this), even
| if they are strongly typed. The point of strong typing is
| that the language doesn't do implicit conversions and other
| shenanigans.
| Spivak wrote:
| Well yeah, because variables in what you consider to be a
| strongly typed language are allocating the storage for
| those variables. When you say int x you're asking the
| compiler to give you an int shaped box. When you say x = 1
| in Ruby all you're doing is saying is that in this scope
| the name x now refers to the box holding a 1. You can't
| actually store a string in the int box, you can only say
| that from now on the name x refers to the string box.
| 9rx wrote:
| The types are strong. The variables are weak.
| folkrav wrote:
| It would be weak if that was actually mutating the first
| "a". That second declaration creates a new variable using
| the existing name "a". Rust lets you do the same[1].
|
| [1] https://doc.rust-lang.org/book/ch03-01-variables-and-
| mutabil...
| johnfn wrote:
| Rust lets you do the same because the static typing keeps
| you safe. In Rust, treating the second 'a' like a number
| would be an error. In ruby, it would crash.
| 0x457 wrote:
| These are two entirely different a's you're storing
| reference to it in the same variable. You can do the same
| in rust (we agree it statically and strongly typed,
| right?):
|
| let a = 1;
|
| let a = '1';
|
| Strongly typing means I can do 1 + '1' variable names and
| types has nothing to do with it being strongly typed.
| pclowes wrote:
| Oops, I meant weakly typed as in JS or strongly typed as in
| Ruby. But decided to switch the Ruby example to Elixir and
| messed up the sentence
| js2 wrote:
| Good luck with this fight. I've had it on HN most recently 7
| months ago, but about Python:
|
| https://news.ycombinator.com/item?id=42367644
|
| A month before that:
|
| https://news.ycombinator.com/item?id=41630705
|
| I've given up since then.
| arrowsmith wrote:
| I already wrote about wrt Elixir:
| https://arrowsmithlabs.com/blog/elixir-is-dynamically-and-
| st...
| 9rx wrote:
| We've been reading comments like that since the internet
| was created (and no doubt in books before that). Why give
| up now?
| josephg wrote:
| Yep. For this reason, I wish more languages supported bound
| integers. Eg, rather than saying x: u32, I want to be able to
| use the type system to constrain x to the range of [0, 10).
|
| This would allow for some nice properties. It would also enable
| a bunch of small optimisations in our languages that we can't
| have today. Eg, I could make an integer that must fall within
| my array bounds. Then I don't need to do bounds checking when I
| index into my array. It would also allow a lot more peephole
| optimisations to be made with Option.
|
| Weirdly, rust already kinda supports this _within_ a function
| thanks to LLVM magic. But it doesn 't support it for variables
| passed between functions.
| steveklabnik wrote:
| In my understanding Rust may gain this feature via "pattern
| types."
| mcculley wrote:
| Ada has this ability to define ranges for subtypes. I wish
| language designers would look at Ada more often.
| tylerhou wrote:
| Academic language designers do! But it takes a while for
| academic features to trickle down to practical languages--
| especially because expressive-enough refinement typing on
| even the integers leads to an undecidable theory.
| idbehold wrote:
| >But it takes a while
|
| *Checks watch*
|
| We're going on 45 years now.
| spookie wrote:
| Well, ada is practical
| geysersam wrote:
| Aren't most type systems in widely used languages Turing
| complete and (consequently) undecidable? Typescript and
| python are two examples that come to mind
|
| But yeah maybe expressive enough refinement typing leads
| to hard to write and slow type inference engines
| ninetyninenine wrote:
| This can be done in typescript. It's not super well known
| because of typescripts association with frontend and
| JavaScript. But typescript is a language with one of the most
| powerful type systems ever.
|
| Among the popular languages like golang, rust or python
| typescript has the most powerful type system.
|
| How about a type with a number constrained between 0 and 10?
| You can already do this in typescript. type
| onetonine = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9
|
| You can even programmatically define functions at the type
| level. So you can create a function that outputs a type
| between 0 to N. type Range<N extends
| number, A extends number[] = []> = A['length'] extends
| N ? A[number] : Range<N, [...A, A['length']]>;
|
| The issue here is that it's a bit awkward you want these
| types to compose right? If I add two constrained numbers say
| one with max value of 3 and another with max value of two the
| result should be max value of 5. Typescript doesn't support
| this by default with default addition. But you can create a
| function that does this. // Build a tuple of
| length L type BuildTuple<L extends number, T extends
| unknown[] = []> = T['length'] extends L ? T :
| BuildTuple<L, [...T, unknown]>; // Add two
| numbers by concatenating their tuples type Add<A
| extends number, B extends number> = [...BuildTuple<A>,
| ...BuildTuple<B>]['length']; // Create a union: 0
| | 1 | 2 | ... | N-1 type Range<N extends number, A
| extends number[] = []> = A['length'] extends N ?
| A[number] : Range<N, [...A, A['length']]>;
| function addRanges< A extends number, B
| extends number >( a: Range<A>, b:
| Range<B> ): Range<Add<A, B>> { return (a + b)
| as Range<Add<A, B>>; }
|
| The issue is to create these functions you have to use tuples
| to do addition at the type level and you need to use
| recursion as well. Typescript recursion stops at 100 so
| there's limits.
|
| Additionally it's not intrinsic to the type system. Like you
| need peanno numbers built into the number system and built in
| by default into the entire language for this to work
| perfectly. That means the code in the function is not type
| checked but if you assume that code is correct then this
| function type checks when composed with other primitives of
| your program.
| lucianbr wrote:
| Complexity is bad in software. I think this kind of thing
| does more harm than good.
|
| I get an error that I can't assign something that seems to
| me assignable, and to figure out why I need to study
| functions at type level using tuples and recursion. The
| cure is worse than the disease.
| ninetyninenine wrote:
| It can work. It depends on context. Like let's say these
| types are from a well renowned library or one that's been
| used by the codebase for a long time.
|
| If you trust the type, then it's fine. The code is safer.
| In the world of of the code itself things are easier.
|
| Of course like what you're complaining about, this opens
| up the possibility of more bugs in the world of types,
| and debugging that can be a pain. Trade offs.
|
| In practice people usually don't go crazy with type level
| functions. They can do small stuff, but usually nothing
| super crazy. So type script by design sort of fits the
| complexity dynamic you're looking for. Yes you can do
| type level functions that are super complex, but the
| language is not designed around it and it doesn't promote
| that style either. But you CAN go a little deeper with
| types then say a language with less power in the type
| system like say Rust.
| giraffe_lady wrote:
| Typescript's type system is turing complete, so you can do
| basically anything with it if this sort of thing is fun to
| you. Which is pretty much my problem with it: this sort of
| thing can be fun, _feels_ intellectually stimulating. But
| the added power doesn 't make coding easier or make the
| code more sound. I've heard this sort of thing called the
| "type puzzle trap" and I agree with that.
|
| I'll take a modern hindley milner variant any day.
| Sophisticated enough to model nearly any _type information_
| you 'll have need of, without blurring the lines or
| admitting the temptation of encoding complex logic in it.
| reactordev wrote:
| Typescript's type system can run Doom.
|
| https://youtu.be/0mCsluv5FXA
| giraffe_lady wrote:
| (derogatory)
| ninetyninenine wrote:
| >Which is pretty much my problem with it: this sort of
| thing can be fun, feels intellectually stimulating. But
| the added power doesn't make coding easier or make the
| code more sound.
|
| In practice nobody goes too crazy with it. You have a
| problem with a feature almost nobody uses. It's there and
| Range<N> is like the upper bound of complexity I've seen
| in production but that is literally extremely rare as
| well.
|
| There is no "temptation" of coding complex logic in it at
| all as the language doesn't promote these features at
| all. It's just available if needed. It's not well known
| but typescript types can be easily used to be 1 to 1 with
| any hindley milner variant. It's the reputational baggage
| of JS and frontend that keeps this fact from being well
| known.
|
| In short: Typescript is more powerful then hindley
| milner, a subset of it has one to one parity with it, the
| parts that are more powerful then hindley milner aren't
| popular and used that widely nor does the flow of the
| language itself promote there usage. The feature is just
| there if you need it.
|
| If you want a language where you do this stuff in
| practice take a look at Idris. That language has these
| features built into the language AND it's an ML style
| language like haskell.
| giraffe_lady wrote:
| I have definitely worked in TS code bases with overly
| gnarly types, seen more experienced devs spend an entire
| workday "refactoring" a set of interrelated types and
| producing an even gnarlier one that more closely modeled
| some real world system but was in no way easier to reason
| about or work with in code. The advantage of HM is the
| inference means there is no incentive to do this, it
| feels foolish from the beginning.
| vmchale wrote:
| ATS does this. Works quite well since multiplication by known
| factors and addition of type variables + inequalities is
| decidable (and in fact quadratic).
| nikeee wrote:
| I proposed a primitive for this in TypeScript a couple of
| years ago [1].
|
| While I'm not entirely convinced myself whether it is worth
| the effort, it offers the ability to express "a number
| greater than 0". Using type narrowing and intersection types,
| open/closed intervals emerge naturally from that. Just check
| `if (a > 0 && a < 1)` and its type becomes `(>0)&(<1)`, so
| the interval (0, 1).
|
| I also built a simple playground that has a PoC
| implementation: https://nikeee.github.io/typescript-
| intervals/
|
| [1]: https://github.com/microsoft/TypeScript/issues/43505
| mnahkies wrote:
| Related
| https://github.com/microsoft/TypeScript/issues/54925
|
| My specific use case is pattern matching http status codes
| to an expected response type, and today I'm able to work
| around it with this kind of construct
| https://github.com/mnahkies/openapi-code-
| generator/blob/main... - but it's esoteric, and feels
| likely to be less efficient to check than what you propose
| / a range type.
|
| There's runtime checking as well in my implementation, but
| it's a priority for me to provide good errors at build time
| scottgg wrote:
| The generic magic for this is called "dependant types" I
| believe - generics that can take values as well as types as
| parameters. Idris supports these
| ameliaquining wrote:
| The full-blown version that guarantees no bounds-check
| errors at runtime requires dependent types (and
| consequently requires programmers to work with a proof
| assistant, which is why it's not very popular). You could
| have a more lightweight version that instead just crashes
| the program at runtime if an out-of-range assignment is
| attempted, and optionally requires such fallible
| assignments to be marked as such in the code. Rust can do
| this today with const generics, though it's rather clunky
| as there's very little syntactic sugar and no implicit
| widening.
| librasteve wrote:
| in raku, that's spelled subset OneToTen of
| Int where 1..10:
| someone_19 wrote:
| You can do this quite easily in Rust. But you have to
| overload operators to make your type make sense. That's also
| possible, you just need to define what type you get after
| dividing your type by a regular number and vice versa a
| regular number by your type. Or what should happen if when
| adding two of your types the sum is higher than the maximum
| value. This is quite verbose. Which can be done with generics
| or macros.
| MoreQARespect wrote:
| I've recently been following red-green-refactor but instead of
| with a failing test, I tighten the screws on the type system to
| make a production-reported bug cause the _type checker_ to fail
| before making it green by fixing the bug.
|
| I still follow TDD-with-a-test for all new features, all edge
| cases and all bugs that I can't trigger failure by changing the
| type system for.
|
| However, red-green-refactor-with-the-type-system is usually
| quick and can be used to provide hard guarantees against entire
| classes of bug.
| pclowes wrote:
| I like this approach, there are often calls for increased
| testing on big systems and what they really mean is increased
| rigor. Don't waste time testing what you can move into the
| compiler.
|
| It is always great when something is so elegantly typed that
| I struggle to think of how to write a failing test.
|
| What drives me nuts is when there are testing left around
| basically testing the compiler that never were "red" then
| "greened" makes me wonder if there is some subtle edge case I
| am missing.
| eyelidlessness wrote:
| As you move more testing responsibilities to the compiler,
| it can be valuable to test the compiler's responsibilities
| for those invariants though. Otherwise it can be very hard
| to notice when something previously guaranteed statically
| ceases to be.
| eyelidlessness wrote:
| I found myself following a similar trajectory, without
| realizing that's what I was doing. For a while it felt like I
| was bypassing the discipline of TDD that I'd previously found
| really valuable, until I realized that I was getting a lot of
| the test-first benefits before writing or running any code at
| all.
|
| Now I just think of types as the test suite's first line of
| defense. Other commenters who mention the power of types for
| documentation and refactoring aren't wrong, but I think
| that's _because types are tests_ ... and good tests, at
| almost any level, enable those same powers.
| MoreQARespect wrote:
| I dont think tests and types are the same "thing" per se -
| they work vastly better in conjunction with each other than
| alone and are weirdly symmetrical in the way that theyre
| bad substitutes for each other.
|
| However, Im convinced that theyre both part of the same
| class of thing, and that "TDD" or red/green/refactor or
| whatever you call it works on that class, not specifically
| just on tests.
|
| Documentation is a funny one too - I use my types to
| generate API and other sorts of reference docs and tests to
| generate how-to docs. There is a seemingly inextricable
| connection between types and reference docs, tests and how-
| to docs.
| eyelidlessness wrote:
| Types are a kind of test. Specifically they're a way to
| assert certain characteristics about the interactions
| between different parts of the code. They're frequently
| assertions you'd want to make another way, if you didn't
| have the benefit of a compiler to run that set of
| assertions for you. And like all tests, they're a means
| to gain or reinforce confidence in claims you could make
| about the code's behavior. (Which is their symmetry with
| documentation.)
| kazinator wrote:
| Also known as "make bad state unexperimentable".
| jevndev wrote:
| The "Stop at first level of type implementation" is where I see
| codebases fail at this. The example of "I'll wrap this int as a
| struct and call it a UUID" is a really good start and pretty
| much always start there, but inevitably someone will circumvent
| the safety. They'll see a function that takes a UUID and they
| have an int; so they blindly wrap their int in UUID and move
| on. There's nothing stopping that UUID from not being actually
| universally unique so suddenly code which relies on that
| assumption breaks.
|
| This is where the concept of "Correct by construction" comes
| in. If any of your code has a precondition that a UUID is
| actually unique then it should be as hard as possible to make
| one that isn't. Be it by constructors throwing exceptions,
| inits returning Err or whatever the idiom is in your language
| of choice, the only way someone should be able to get a UUID
| without that invariant being proven is if they really *really*
| know what they're doing.
|
| (Sub UUID and the uniqueness invariant for whatever
| type/invariants you want, it still holds)
| munificent wrote:
| _> This is where the concept of "Correct by construction"
| comes in._
|
| This is one of the basic features of object-oriented
| programming that a lot of people tend to overlook these days
| in their repetitive rants about how horrible OOP is.
|
| One of the key things OO gives you is _constructors_. You can
| 't get an instance of a class without having gone through a
| constructor that the class itself defines. That gives you a
| way to bundle up some data and wrap it in a layer of
| validation that can't be circumvented. If you have an
| instance of Foo, you have a firm guarantee that the author of
| Foo was able to ensure the Foo you have is a meaningful one.
|
| Of course, writing good constructors is hard because data
| validation is hard. And there are plenty of classes out there
| with shitty constructors that let you get your hands on
| broken objects.
|
| But the language itself gives you direct mechanism to do a
| good job here if you care to take advantage of it.
|
| Functional languages can do this too, of course, using some
| combination of abstract types, the module system, and factory
| functions as convention. But it's a _pattern_ in those
| languages where it 's a language feature in OO languages.
| (And as any functional programmer will happily tell you, a
| design pattern is just a sign of a missing language feature.)
| lock1 wrote:
| I find regular OOP language constructor are too
| restrictive. You can't return something like
| Result<CorrectObject,ConstructorError> to handle the error
| gracefully or return a specific subtype; you need a static
| factory method to do something more than guaranteed
| successful construction w/o exception.
|
| Does this count as a missing language feature by requiring
| a "factory pattern" to achieve that?
| henry700 wrote:
| The natural solution for this is a private constructor
| with public static factory methods, so that the user can
| only obtain an instance (or the error result) by calling
| the factory methods. Constructors need to be constrained
| to return an instance of the class, otherwise they would
| just be normal methods.
|
| Convention in OOP languages is (un?)fortunately to just
| throw an exception though.
| Conscat wrote:
| In languages with generic types such as C++, you
| generally need free factory functions rather than static
| member functions so that type deduction can work.
| 0x457 wrote:
| This is why constructors are dumb IMO and rust way is the
| right way.
|
| Nothing stops you from returning
| Result<CorrectObject,ConstructorError> in
| CorrectObject::new(..) function because it's just a
| regular function struct field visibility takes are if you
| not being able to construct incorrect CorrectObject.
| hombre_fatal wrote:
| I don't see this having much to do with OOP vs FP but maybe
| the ease in which a language lets you create nominal types
| and functions that can nicely fail.
|
| What sucks about OOP is that it also holds your hand into
| antipatterns you don't necessarily want, like adding
| behavior to what you really just wanted to be a simple data
| type because a class is an obvious junk drawer to put
| things.
|
| And, like your example of a problem in FP, you have to be
| eternally vigilant with your own patterns to avoid
| antipatterns like when you accidentally create a system
| where you have to instantiate and collaborate multiple
| classes to do what would otherwise be a simple
| `transform(a: ThingA, b: ThingB, c: ThingC): ThingZ`.
|
| Finally, as "correct by construction" goes, doesn't it all
| boil down to `createUUID(string): Maybe<UUID>`? Even in an
| OOP language you probably want `UUID.from(string):
| Maybe<UUID>`, not `new UUID(string)` that throws.
| munificent wrote:
| _> Even in an OOP language you probably want
| `UUID.from(string): Maybe <UUID>`, not `new UUID(string)`
| that throws._
|
| One way to think about exceptions is that they are a
| pattern matching feature that privileges one arm of the
| sum type with regards to control flow and the type system
| (with both pros and cons to that choice). In that sense,
| every constructor is `UUID.from(string):
| MaybeWithThrownNone<UUID>`.
| 9rx wrote:
| The best way to think about exceptions is to consider the
| term literally (as in: unusual; not typical) while
| remembering that programmers have an incredibly
| overinflated sense of ability.
|
| In other words, exceptions are for cases where the
| programmer screwed up. While programmers screwing up
| isn't unusual at all, programmers like to think that they
| don't make mistakes, and thus in their eye it is unusual.
| That is what sets it apart from environmental failures,
| which are par for the course.
|
| To put it another way, it is for signalling at runtime
| what would have been a compiler error if you had a more
| advanced compiler.
| reactordev wrote:
| Union types!! If everything's a type and nothing works
| together, start wrapping them in interfaces and define an uber
| type that unions everything everywhere all at once.
|
| Welcome to typescript. Where generics are at the heart of our
| generic generics that throw generics of some generic generic
| geriatric generic that Bob wrote 8 years ago.
|
| Because they can't reason with the architecture they built,
| they throw it at the type system to keep them in line. It works
| most of the time. Rust's is beautiful at barking at you that
| you're wrong. Ultimately it's us failing to design flexibility
| amongst ever increasing complexity.
|
| Remember when "Components" where "Controls" and you only had
| like a dozen of them?
|
| Remember when a NN was only a few hundred thousand parameters?
|
| As complexity increases with computing power, so must our
| understanding of it in our mental model.
|
| However you need to keep that mental model in check, use it. If
| it's typing, do it. If it's rigorous testing, write your tests.
| If it's simulation, run it my friend. Ultimately, we all want
| better quality software that doesn't break in unexpected ways.
| tossandthrow wrote:
| This can usually be alleviated by structural types instead of
| nominal types.
|
| You can always enforce nominal types if you really need it.
| jjice wrote:
| Does anyone know the term for this? I had "Type Driven
| Development" in my head, but I don't know if that's a broadly
| used term for this.
|
| It's a step past normal "strong typing", but I've loved this
| concept for a while and I'd love to have a name to refer to it by
| so I can help refer others to it.
| jshxr wrote:
| I've seen it being referred to as "New Type Pattern" or "New
| Type Idiom" in quite some places. For example in the rust-by-
| example book [1].
|
| [1] https://doc.rust-lang.org/rust-by-
| example/generics/new_types...
| frou_dh wrote:
| "newtype", or kind of (but not exactly) the opposite of
| "Primitive Obsession"
| marcosdumay wrote:
| "Type driven development" is usually meant to say you will
| specify your system behavior in the types. Often by writing the
| types first and having the actual program determined by them.
| Some times so completely determined that you can use some
| software (not an LLM) to write it. (The name is a joke about
| the other TDD.)
| peterldowns wrote:
| Strongly Typed Identifier
|
| https://en.wikipedia.org/wiki/Strongly_typed_identifier
|
| > The strongly typed identifier commonly wraps the data type
| used as the primary key in the database, such as a string, an
| integer or universally unique identifier (UUID).
| bcrosby95 wrote:
| Using basic types for domain concepts is called 'primitive
| obsession'. It's been considered code smell for at least 25
| years. So this would be... not being primitive obsessed. It
| isn't anything driven development.
|
| Different people draw the line in different places for this.
| I've never tried writing code that takes every domain concept,
| no matter how small, and made a type out of it. It's always
| been on my bucket list though to see how it works out. I just
| never had the time or in-the-moment inclination to go that far.
| Romario77 wrote:
| I think often times it's enough to have enums for known ints,
| for example and have some parameter checking for ranges when
| known.
|
| Some languages like C++ made a contracts concept where you
| could make these checks more formal.
|
| As some people indicated the auto casting in many languages
| could make the implementation of these primitive based types
| complicated and fragile and provide more nuisance than it
| provides value.
| bcrosby95 wrote:
| Yep! I recently started playing with Ada and they make
| tightly specifying your types based upon primitives pretty
| easy. You also have some control over auto conversion based
| upon the specifics of how you declare them.
| b450 wrote:
| The method in the article is very close to the idea of a
| "branded type". Though maybe there's a distinction someone can
| point out to me.
| SideburnsOfDoom wrote:
| You're correct that this is far from a new idea.
|
| Relevant terms are "Value object" (1) and avoiding "Primitive
| obsession" where everything is "stringly typed".
|
| Strongly typed ids should be Value Objects, but not all value
| objects are ids. e.g. I might have a value object that
| represents an x-y co-ordinate, as I would expect an object with
| value (2,3) to be equal to a different object with the same
| value.
|
| 1) https://martinfowler.com/bliki/ValueObject.html
|
| https://en.wikipedia.org/wiki/Value_object
| Izkata wrote:
| It's taking the original idea behind Hungarian notation (now
| called "Apps Hungarian notation" to distinguish from "Systems
| Hungarian notation" which uses datatype) and moving it into the
| type system.
|
| To keep building on history, I'd suggest Hungarian types.
| gfairbanks wrote:
| The overall idea of using your type system to enforce
| invariants is called typeful programming [1]. The first few
| sentences of that paper are:
|
| "There exists an identifiable programming style based on the
| widespread use of type information handled through mechanical
| typechecking techniques. This typeful programming style is in a
| sense independent of the language it is embedded in; it adapts
| equally well to functional, imperative, object-oriented, and
| algebraic programming, and it is not incompatible with
| relational and concurrent programming."
|
| [1] Luca Cardelli, Typeful Programming, 1991.
| http://www.lucacardelli.name/Papers/TypefulProg.pdf
|
| [2] https://news.ycombinator.com/item?id=18872535
| Noumenon72 wrote:
| Does this apply in Java where adding a type means every ID has to
| have a class instance in the heap? ChatGPT says I might want to
| wait for Project Valhalla value types.
| nsm wrote:
| Highly recommend the Early Access book Data-Oriented Programming
| with Java by Chris Kiehl as another resource.
| goostavos wrote:
| Hey, I'm that guy! Thanks for the shout out!
| fedeb95 wrote:
| This works very well and I'd whish I'd convince my team members
| to use more this technique.
|
| Moreover: you can separate types based on admitted values and
| perform runtime checks. Percentage, Money, etc.
| kccqzy wrote:
| This pattern is exactly the pattern I recommended two weeks ago
| in a thread about a nearly catastrophic OpenZFS bug
| https://news.ycombinator.com/item?id=44531524 in response to
| someone saying we should use AI to detect this class of bugs. I'm
| glad there are still people who think alike and opt for simpler,
| more deterministic solutions such as using the type system.
| peterldowns wrote:
| My friend Lukas has written about this before in more detail, and
| describes the general technique as "Safety Through
| Incompatibility". I use this approach in all of my golang
| codebases now and find it invaluable -- it makes it really easy
| to do the right thing and really hard to accidentally pass the
| wrong kinds of IDs around.
|
| https://lukasschwab.me/blog/gen/deriving-safe-id-types-in-go...
|
| https://lukasschwab.me/blog/gen/safe-incompatibility.html
| taeric wrote:
| Hard not to agree with the general idea. But also hard to ignore
| all of the terrible experiences I've had with systems where
| everything was a unique type.
|
| In general, I think this largely falls when you have code that
| wants to just move bytes around intermixed with code that wants
| to do some fairly domain specific calculations. I don't have a
| better way of phrasing that, at the moment. :(
| hombre_fatal wrote:
| Maybe I know what you mean.
|
| There are cases where you have the data in hand but now you
| have to look for how to create or instantiate the types before
| you can do anything with it, and it can feel like a scavenger
| hunt in the docs unless there's a cookbook/cheatsheet section.
|
| One example is where you might have to use createVector(x, y,
| z): Vector when you already have { x, y, z }. And only then can
| you createFace(vertices: Vector[]): Face even though Face is
| just { vertices }. And all that because Face has a method to
| flip the normal or something.
|
| Another example is a library like Java's BouncyCastle where you
| have the byte arrays you need, but you have to instantiate like
| 8 different types and use their methods on each other just to
| create the type that lets you do what you wish was just
| `hash(data, "sha256")`.
| stellalo wrote:
| Ideally though, the compiler lowers all domain specific logic
| into simple byte-moving, just after having checked that types
| add up. Or maybe I misunderstood what you meant?
| recursivedoubts wrote:
| Type systems, like any other tool in the toolbox, have an 80/20
| rule associated with them. It is quite easy to overdo types and
| make working with a library extremely burdensome for little to no
| to negative benefit.
|
| I know what a UUID (or a String) is. I don't know what an
| AccountID, UserID, etc. is. Now I need to know what those are
| (and how to make them, etc. as well) to use your software.
|
| Maybe an elaborate type system worth it, but maybe not
| (especially if there are good tests.)
|
| https://grugbrain.dev/#grug-on-type-systems
| 3836293648 wrote:
| To be fair, you probably needed to know that anyway? Or else
| you would've just passed invalid data into functions.
| recursivedoubts wrote:
| I cannot recall ever passing an invalid UUID (or long id)
| into a function due to statically-knowable circumstances.
| happytoexplain wrote:
| The point is that you might pass a _semantically invalid_
| user ID. Not that you might pass an invalid UUID.
|
| I generally agree that it's easy to over-do, but can be
| great if you have a terse, dense, clear
| language/framework/docs, so you can instantly learn about
| UserID.
| ThunderSizzle wrote:
| More specifically, if all entities have a GUID, it's not
| impossible to accidentally map entity A ID to entity B ID
| accidentally, especially when working with relationships.
| Moving the issue to the compiler is nicer than the query
| returning 0 results and the developer staring endlessly
| for the subtle issue.
| petesergeant wrote:
| > I know what a UUID (or a String) is. I don't know what an
| AccountID, UserID, etc. is. Now I need to know what those are
| (and how to make them, etc. as well) to use your software.
|
| Yes, that's exactly the point. If you don't know how to acquire
| an AccountID you shouldn't just be passing a random string or
| UUID into a function that accepts an AccountID hoping it'll
| work, you should have acquired it from a source that gives out
| AccountIDs!
| recursivedoubts wrote:
| And that's my point: I'm usually getting AccountIDs from
| strings (passed in via HTTP requests) so the whole thing
| becomes a pointless exercise.
| petesergeant wrote:
| Do you validate them? I assume you do. Feels like a great
| time to cast them too
| Kranar wrote:
| You just accept raw strings without doing any kind of
| validation? The step that performs validation should encode
| that step in the form of a type.
| dgb23 wrote:
| I think the example is just not very useful, because it
| illustrates a domain separation instead of a computational one,
| which is almost always the wrong approach.
|
| It is however useful to return a UUID type, instead of a
| [16]byte, or a HTMLNode instead of a string etc. These
| discriminate real, computational differences. For example the
| method that gives you a string representation of an UUID
| doesn't care about the surrounding domain it is used in.
|
| Distinguishing a UUID from an AccountID, or UserID is
| contextual, so I rather communicate that in the aggregate. Same
| for Celsius and Fahrenheit. We also wouldn't use a specialized
| type for date times in every time zone.
| ElectricalUnion wrote:
| > I know what a UUID (or a String) is.
|
| I now know I never know whenever "a UUID" is stored or
| represented as a GUIDv1 or a UUIDv4/UUIDv7.
|
| I know it's supposed to be "just 128 bits", but somehow, I had
| a bunch of issues running old Java servlets+old Java
| persistence+old MS SQL stack that insisted, when "converting"
| between java.util.UUID to MS SQL Transact-SQL uniqueidentifier,
| every now and then, that it would be "smart" if it flipped the
| endianess of said UUID/GUID to "help me". It got to a point
| where the endpoints had to manually "fix" the endianess and
| insert/select/update/delete for both the "original" and the
| "fixed" versions of the identifiers to get the expected results
| back.
|
| (My educated guess it's somewhat similar to those problems that
| happens when your persistence stack is "too smart" and tries to
| "fix timezones" of timestamps you're storing in a database for
| you, but does that wrong, some of the time.)
| tshaddox wrote:
| > I don't know what an AccountID, UserID, etc. is. Now I need
| to know what those are (and how to make them, etc. as well) to
| use your software.
|
| Presumably you need to know what an Account and a User are to
| use that software in the first place. I can't imagine a
| reasonable person easily understanding a getAccountById
| function which takes one argument of type UUID, but having
| trouble understanding a getAccountById function which takes one
| argument of type AccountId.
| kjksf wrote:
| UserID and AccountID could just as well be integers.
|
| What he means is that by introducing a layer of indirection
| via a new type you hide the physical reality of the
| implementation (int vs. string).
|
| The physical type matters if you want to log it, save to a
| file etc.
|
| So now for every such type you add a burden of having to undo
| that indirection.
|
| At which point "is it worth it?" is a valid question.
|
| You made some (but not all) mistakes impossible but you've
| also introduced that indirection that hides things and needs
| to be undone by the programmer.
| buerkle wrote:
| foo(UUID, UUID); foo(AccountId, UserId);
|
| I'd much rather deal with the 2nd version than the first. It's
| self-documenting and prevents errors like calling "foo(userId,
| accountId)" letting the compiler test for those cases. It also
| helps with more complex data structures without needing to
| create another type. Map<UUID, List<UUID>>
| Map<AccountId, List<UserId>>
| chamomeal wrote:
| Before I finished the first full paragraph of your comment, I
| thought "I bet this is the grug guy"
|
| Love love love hypermedia systems, by the way. In 2018, took a
| Java class in college and really liked it, asked my prof how to
| get a job in programming, and he said "idk learn a JavaScript
| framework.
|
| I googled "what is a javascript framework", watched a Udemy
| course on react, and got a job doing react and node. I never
| really learned anything else.
|
| Your book honestly shaved off a large chunk of my development
| cynicism. I love html! I can do whatever I want, with whatever
| tools I want! No more bundlers for me, baby. Except at work,
| but everything at work kinda sucks, and I don't have to
| configure any of it.
| ho_schi wrote:
| I'm not familiar with Go. Please correct me, but this reads like
| _object oriented programming_ i.e. OOP for every kind of data?
|
| Coming from C++, this kind of types with classes make sense. But
| also are a maintenance task with further issues, were often
| proper variable naming matters. Likely a good balance is the key.
| Jtsummers wrote:
| This isn't an OO thing at all. In C, to contrast with Go, a
| typedef is an alias. You can use objects (general sense) of the
| type `int` interchangeably with something like `myId` which is
| created through `typedef int myId`.
|
| That is, this is perfectly acceptable C: int x
| = 10; myId id = x; // no problems
|
| In Go the equivalent would be an error because it will not,
| automatically, convert from one type to another just because it
| happens to be structurally identical. This forces you to be
| explicit in your conversion. So even though the type happens to
| be an int, an arbitrary int or other types which are
| structurally ints cannot be accidentally converted to a myId
| unless you somehow include an explicit but unintended
| conversion.
| ho_schi wrote:
| Thank you for answer :)
|
| This helped me! Especially because you started with typedef
| from C. Therefore I could relate. Others just downvote and
| don't explain.
| bbkane wrote:
| I was doing this and used it for a year in
| https://github.com/bbkane/warg/, but ripped it out since Go auto-
| casts underlying types to derived types in function calls:
| Type userID int64 func Work(u userID) {...}
| Work(1) // Go accepts this
|
| I think I recalled that correctly. Since things like that were
| most of what I was doing I didn't feel the safety benefit in many
| places, but had to remember to cast the type in others (iirc,
| saving to a struct field manually).
| mattbee wrote:
| Yep in the same way it would allow `var u userID = 1` it allows
| `Work(1)` rather than insisting on `var u userID = userID(1)`
| and `Work(userID(1))`.
|
| I teach Go a few times a year, and this comes up a few times a
| year. I've not got a good answer why this is consistent with
| such an otherwise-explicit language.
| alphazard wrote:
| This is a little misleading. Go will automatically convert a
| numeric literal (which is a compile time idea not represented
| at runtime) into the type of the variable it is being assigned
| to.
|
| Go _will not_ automatically cast a variable of one type to
| another. That still has to be done explicitly.
| func main() { var x int64 = 1
| Func(SpecialInt64(x)) // this will work Func(x) // this
| will not work } type SpecialInt64 int64
| func Func(x SpecialInt64) { }
|
| https://go.dev/play/p/4eNQOJSmGqD
| skybrian wrote:
| This only happens for literal values. Mixing up variables of
| different types will result in a type error.
|
| When you write 42 in Go, it's not an int32 or int64 or some
| more specific type. It's automatically inferred to have the
| correct type. This applies even for user-defined numeric types.
| jshxr wrote:
| Unfortunately, this can be somewhat awkward to implement in
| certain structural typed languages like TypeScript. I often find
| myself writing something along the lines of type
| UserID = string & { readonly __tag: unique symbol }
|
| which always feels a bit hacky.
| paldepind2 wrote:
| I never understood why people are so keen to do that in
| TypeScript. With that definition a `UserID` can still be
| silently "coerced" to a `string` everywhere. So you only get
| halfway there to an encapsulated type.
|
| I think it's a much better idea to do: type
| UserID = { readonly __tag: unique symbol }
|
| Now clients of `UserID` no longer knows anything about the
| representation. Like with the original approach you need a bit
| of casting, but that can be neatly encapsulated as it would be
| in the original approach anyway.
| mcflubbins wrote:
| I've actually seen this before and didn't realize this is exactly
| what the goal was. I just thought it was noise. In fact, just
| today I wrote a function that accepted three string arguments and
| was trying to decide if I should force the caller to parse them
| into some specific types, or do so in the function body and throw
| an error, or just live with it. This is exactly the solution I
| needed (because I actually don't NEED the parsed values.)
|
| This is going to have the biggest impact on my coding style this
| year.
| abraxas wrote:
| An adjacent point is to use checked exceptions and to handle them
| appropriate to their type. I don't get why Java checked
| exceptions were so maligned. They saved me so many headaches on a
| project where I forced their use as I was the tech lead for it.
| Everyone hated me for a while because it forced them to deal with
| more than just the happy path but they loved it once they got in
| the rhythm of thinking about all the exceptional cases in the
| code flow. And the project was extremely robustness even though
| we were not particularly disciplined about unit testing
| dherls wrote:
| With Java, there are a lot of usability issues with checked
| types. For example streams to process data really don't play
| nicely if your map or filter function throws a checked
| exception. Also if you are calling a number of different
| services that each have their own checked exception, either you
| resort to just catching generic Exception or you end up with a
| comically large list of exceptions
| Jtsummers wrote:
| Setting aside the objections some have to exceptions generally:
| Checked exceptions, in contrast to unchecked, means that if a
| function/method deep in your call stack is changed to throw an
| exception, you may have to change many function (to at least
| denote that they will throw that exception or some exception)
| between the handler and the thrower. It's an objection to the
| ergonomics around modifying systems.
|
| Think of the complaints around function coloring with async,
| how it's "contagious". Checked exceptions have the same
| function color problem. You either call the potential thrower
| from inside a try/catch or you declare that the caller will
| throw an exception.
| abraxas wrote:
| That's a valid point but it's somewhere on a spectrum of
| "quick to write/change" vs "safe and validated" debate of
| strictly vs loosely typed systems. Strictly typed systems are
| almost by definition much more "brittle" when it comes to
| code editing. But the strictness also ensures that
| refactoring is usually less perilous than in loosely typed
| code.
| gpderetta wrote:
| And as with async, the issue is a) the lack of the ability to
| write generic code that can abstract over the async-ness or
| throw signature of a function and b) the ability to type
| erase asyncness (by wrapping with stackful coroutines) or
| throw signature (by converting to unchecked exceptions).
|
| Incidentally, for exceptions, Java had (b), but for a long
| time didn't have (a) (although I think this changed?),
| leading to (b) being abused.
| someone_19 wrote:
| Unhappy way is a part of contract. So yes, that is what I
| want. If a function couldn't fail before but can after the
| update - I want to know about it.
|
| In fact, at each layer, if you want to propagate an error,
| you have to convert it to one specific to that layer.
| default-kramer wrote:
| I think checked exceptions were maligned because they were
| overused. I like that Java supports both checked and unchecked
| exceptions. But IMO checked exceptions should only be used for
| what Eric Lippert calls "exogenous" exceptions [1]; and even
| then most of them should probably be converted to an unchecked
| exception once they leave the library code that throws them.
| For example, it's always possible that your DB could go offline
| at any time, but you probably don't want "throws SQLException"
| polluting the type signature all the way up the call stack.
| You'd rather have code assuming all SQL statements are going to
| succeed, and if they don't your top-level catch-all can log it
| and return HTTP 500.
|
| [1] https://ericlippert.com/2008/09/10/vexing-exceptions/
| codr7 wrote:
| Sometimes I feel like I actually wouldn't mind having any
| function touching the database tagged as such. But checked
| exceptions are such a pita to deal with that I tend to not
| bother.
| abraxas wrote:
| It's fine to let exceptions percolate to the top of the call
| stack but even then you likely want to inform the user or at
| least log it in your backend why the request was
| unsuccessful. Checked exceptions force both the handling of
| exceptions and the type checking if they are used as
| intended. It's not a problem if somewhere along the call
| chain an SQLException gets converted to "user not permitted
| to insert this data" exception. This is how it was always
| meant to work. What I don't recommend is defaulting to
| RuntimeException and derivatives for those business level
| exceptions. They should still be checked and have their own
| types which at least encourages some discipline when handling
| and logging them up the call stack.
| yardstick wrote:
| In my experience, the top level exception handler will
| catch all incl Throwable, and then inspect the exception
| class and any nested exception classes for things like SQL
| error or MyPermissionsException etc and return the
| politically correct error to the end user. And if the
| exception isn't in a whitelist of ones we don't need to
| log, we log it to our application log.
| materiallie wrote:
| Put another way: errors tend to either be handled "close by"
| or "far away", but rarely "in the middle".
|
| So Java's checked exceptions force you to write verbose and
| pointless code in all the wrong places (the "in the middle"
| code that can't handle and doesn't care about the exception).
| alex_smart wrote:
| >you probably don't want "throws SQLException" polluting the
| type signature all the way up the call stack
|
| A problem easily solved by writing business logic in pure
| java code without any IO and handling the exceptions
| gracefully at the boundary.
| bcrosby95 wrote:
| I think most complaints about checked exceptions in Java
| ultimately boil down to how verbose handling exceptions in Java
| is. Everytime the language forces you to handle an exception
| when you don't really need to makes you hate it a bit more.
|
| First, the library author cannot reasonably define what is and
| isn't a checked exception in their public API. That really is
| up to the decision of the client. This wouldn't be such a big
| deal if it weren't so verbose to handle exceptions though: if
| you could trivially convert an exception to another type, or
| even declare it as runtime, maybe at the module or application
| level, you wouldn't be forced to handle them in these ways.
|
| Second, to signature brittleness, standard advice is to create
| domain specific exceptions anyways. Your code probably
| shouldn't be throwing IOExceptions. But Java makes converting
| exceptions unnecessarily verbose... see above.
|
| Ultimately, I love checked exceptions. I just hate the
| ergonomics around exceptions in Java. I wish designers focused
| more on fixing that than throwing the baby out with the
| bathwater.
| lock1 wrote:
| If only Java also provided Either<L,R>-like in the standard
| library...
|
| Personally I use checked exceptions whenever I can't use
| Either<> and avoid unchecked like a plague.
|
| Yeah, it's pretty sad Java language designer just completely
| deserted exception handling. I don't think there's any kind
| of improvement related to exceptions between Java 8 and 24.
| alex_smart wrote:
| Ok please help me understand, what is the difference
| between - R method() throws L, and - Either<L, R> method()
|
| To me they seem completely isomorphic?
| worldsayshi wrote:
| Don't you mean "isosemantic"? Since the same concept is
| represented with different syntax.
| cloogshicer wrote:
| There _is_ a major difference at the call site.
|
| try/catch has significantly more complex call sites
| because it affects control flow.
| hiddew wrote:
| That is why I am happy that rich errors
| (https://xuanlocle.medium.com/kotlin-2-4-introduces-rich-
| erro...) are coming to Kotlin. This expresses the possible
| error states very well, while programming for the happy path
| and with some syntactic sugar for destucturing the errors.
| Hackbraten wrote:
| For anyone who dislikes checked exceptions due to how clunky
| they feel: modern Java allows you to construct custom Result-
| like types using sealed interfaces.
| wvenable wrote:
| I rarely have more than handful of try..catch blocks in any
| application. These either wrap around an operation that can be
| retried in the case of temporary failure or abort the current
| operation with a logged error message.
|
| Checked exceptions feel like a bad mix of error returns and
| colored functions to me.
| tyleo wrote:
| In C#, I often use a type like: readonly struct
| Id32<M> { public readonly int Value { get; } }
|
| Then you can do: public sealed class MFoo { }
| public sealed class MBar { }
|
| And: Id32<MFoo> x; Id32<MBar> y;
|
| This gives you integer ids that can't be confused with each
| other. It can be extended to IdGuid and IdString and supports new
| unique use cases simply by creating new M-prefixed "marker" types
| which is done in a single line.
|
| I've also done variations of this in TypeScript and Rust.
| SideburnsOfDoom wrote:
| There are libraries for that, such as Vogen
| https://github.com/SteveDunn/Vogen
|
| The name means "Value Object Generator" as it uses Source
| generation to generate the "Value object" types.
|
| That readme has links to similar libraries and further reading.
| rjbwork wrote:
| Have you used this in production? It seems appealing but
| seems so anti-thetical to the common sorts of engineering
| cultures I've seen where this sort of rigorous thinking does
| not exactly abound.
| SideburnsOfDoom wrote:
| Sadly I have not. I have played with it and it seems to
| hold up quite well.
|
| I want it for a case where it seems very well suited - all
| customer ids are strings, but only very specific strings
| are customer ids. And there are other string ids around as
| well.
|
| IMHO Migration won't be hard - you could allow casts
| to/from the primitive type while you change code.
| Temporarily disallowing these casts will show you where you
| need to make changes.
|
| I don't know yet how "close to the edges" you would have to
| go back to the primitive types in ordered for json and db
| serialisation to work.
|
| But it would be easier to get in place in a new "green
| field" codebase. I pitched it as a refactoring, but the
| other people were well, "antithetical" is a good word.
| vborovikov wrote:
| Source generators hide too many details from the user.
|
| I prefer to have the generated code to be the part of the
| code repo. That's why I use code templates instead of
| source generators. But a properly constructed ID type has a
| non-trivial amount of code: https://github.com/vborovikov/p
| wsh/blob/main/Templates/ItemT...
| tyleo wrote:
| This seems like overkill. I'd prefer the few lines of code
| above to a whole library.
| default-kramer wrote:
| I've done something like that too. I also noticed that enums
| are even lower-friction (or were, back in 2014) if your IDs are
| integers, but I never put this pattern into real code because I
| figured it might be too confusing:
| https://softwareengineering.stackexchange.com/questions/3090...
| gpderetta wrote:
| FWIW, I extensively use strong enums in C++[1] for exactly
| this reason and they are a cheap simple way to add strongly
| typed ids.
|
| [1] enum class from C++11, classic enums have too many
| implicit conversions to be of any use.
| TuxSH wrote:
| > classic enums have too many implicit conversions
|
| They're fairly useful still (and since C++11 you can
| specify their underlying type), you can use them as
| namespaced macro definitions
| TuxSH wrote:
| > classic enums have too many implicit conversions
|
| They're fairly useful still (and since C++11 you can
| specify their underlying type), you can use them as
| namespaced macro definitions
|
| Kinda hard to do "bitfield enums" with enum class
| MantisShrimp90 wrote:
| Im on the opposite extreme here in that I believe typing
| obsession is the root of much of our problems as an industry.
|
| I think Rich Hickey was completely right, this is all information
| and we just need to get better at managing information like we
| are supposed to.
|
| The downside of this approach is that these systems are
| tremendously brittle as changing requirements make you comfort
| your original data model to fit the new requirements.
|
| Most OOP devs have seen atleast 1 library with over 1000 classes.
| Rust doesn't solve this problem no matter how much I love it. Its
| the same problem of now comparing two things that are the same
| but are just different types require a bunch of glue code which
| can itself lead to new bugs.
|
| Data as code seems to be the right abstraction. Schemas give
| validation a-la cart while still allowing information to be
| passed, merged, and managed using generic tools rather than
| needing to build a whole api for every new type you define in
| your mega monolith.
| dajonker wrote:
| A lot of us programmer folk are indefinitely in search of that
| one thing that will finally let us write the perfect, bug-free,
| high performance software. We take these concepts to the
| extreme and convince ourselves that it will absolutely work as
| long as we strictly do it the Right Way and only the Right Way.
| Then we try to convince to our fellow programmers that the
| Right Way will solve all of our problems and that it is the
| Only Way. It will be great, it will be grand, it will be
| amazing.
| jerf wrote:
| This technique makes me sad.
|
| Not because it's a bad idea. Quite the contrary. I've sung the
| praises of it myself.
|
| But because it's like the most basic way you can use a type
| system to prevent bugs. In both the sense used in the article,
| and in the sense that it is something you have to do to get the
| even more powerful tools brought to bear on the problem that type
| systems often.
|
| And yet, in the real world, I am constantly explaining this to
| people and constantly fighting uphill battles to get people to do
| it, and not bypass it by using primitives as much as possible
| then bashing it into the strict type at the last moment, or even
| just trying to remove the types.
|
| Here on HN we debate the finer points of whether we should be
| using dependent typing, and in the real world I'm just trying to
| get people to use a Username type instead of a string type.
|
| Not always. There are some exceptions. And considered over my
| entire career, the trend is positive overall. But there's still a
| lot of basic explanations about this I have to give.
|
| I wonder what the trend of LLM-based programming will result in
| after another few years. Will the LLMs use this technique
| themselves, or will people lean on LLMs to "just" fix the
| problems from using primitive types everywhere?
| gpderetta wrote:
| Code review time: + int
| doTheThing(bool,bool,int,int);
|
| Die a little bit inside.
| wvenable wrote:
| I think if it were better supported in the majority of strictly
| typed programming languages then it would be used more. Most
| languages make it a big hassle.
| mk_chan wrote:
| I've been using hacks to do this for a long time. I wish it was
| simpler in C++. I love C++ typing but hate the syntax and
| defaults. It's so complicated to get started with.
|
| https://github.com/Mk-Chan/libchess/blob/master/internal/Met...
| https://github.com/Mk-Chan/libchess/blob/master/Square.h
| gpderetta wrote:
| But it can be a bit easier! You could make Square itself an
| enum class and overload the operators for it directly.
| vemv wrote:
| Most static type systems that I know of disappear at runtime. You
| literally cannot "use" them once deployed to production.
|
| (Typescript's Zed and Clojure's Malli are counterexamples.
| Although not official offerings)
|
| Following OP's example, what prevents you from getting a
| AccountID parsed as a UserID at runtime, in production? In
| production it's all UUIDs, undistinguishable from one another.
|
| A truly safe approach would use distinct value prefixes - one per
| object type. Slack does this I believe.
| Jtsummers wrote:
| > Most static type systems that I know of disappear at runtime.
| You literally cannot "use" them once deployed to production.
|
| That's part of the point of being static. If we can statically
| determine properties of the system and use that information in
| the derived machine code (or byte code or whatever), then we
| may be able to discard that information at runtime (though
| there are reasons not to discard it).
|
| > Following OP's example, what prevents you from getting a
| AccountID parsed as a UserID at runtime, in production? In
| production it's all UUIDs, undistinguishable from one another.
|
| If you're receiving information from the outside and converting
| it into data in your system you have to parse and validate it.
| If the UUID does not correspond to a UserID in your database or
| whatever, then the attempted conversion should fail. You'd have
| a guard like this: if
| user_db.contains(UserID(uuid)) { return UserID(uuid)
| } // signal an error or return a None, zero value, null,
| etc.
| vemv wrote:
| There are infinitely many runtime properties that are simply
| impossible to determine statically.
|
| Static typing is just a tool, aiming to help with a subset of
| all possible problems you may find. If you think it's an
| absolute oracle of every possible problem you may find,
| sorry, that's just not true, and trivially demonstrable.
|
| Your example already is a runtime check that makes no
| particular use of the type system. It's a simple "set
| contains" check (value-oriented, not type-oriented) which
| also is far more expensive than simply verifying the string
| prefix of a Slack-style object identifier.
|
| Ultimately I'm not even saying that types are bad, or that
| static typing is bad. If you truly care about correctness,
| you'd use all layers at your disposition - static and
| dynamic.
| alphazard wrote:
| I've seen experienced programmers do this a lot. It's the kind of
| thing that someone thinks is annoying, without realizing that it
| was preventing them from doing something incorrect.
| rhubarbtree wrote:
| It can be annoying though.
|
| I think Rich Hickey has a point that bugs like this almost
| certain get caught by running the program. If they make it into
| production it usually results in an obscure edge case.
|
| I'm sure there are exceptions but unless you're designing for
| the worst case (safety critical etc) rather than average case
| (web app), types come with a lot of trade offs.
|
| I've been on the fence about types for a long time, but having
| built systems fast at a startup for years, I now believe
| dynamic typing is superior. Folks I know who have built similar
| systems and are excellent coders also prefer dynamic typing.
|
| In my current startup we use typescript because the other team
| members like it. It does help replace comments when none are
| available, and it stops some bugs, but it also makes the
| codebase very hard to read and slows down dev.
|
| A high quality test suite beats everything else hands down.
| lurking_swe wrote:
| An engineer getting up to speed on a 10 year old web app that
| uses dynamic types will likely have a very different opinion.
|
| No types anywhere, so making a change is SCARY! And all the
| original engineers have usually moved on. Fun times. Types
| are a form of forced documentation after all, and help catch
| an entire class of bugs. If you're really lucky, the project
| has good unit tests.
|
| I think dynamic typing is wonderful for making software
| quickly, and it can be a force multiplier for startups. I
| also enjoy it when creating small services or utilities. But
| for a large web app, you'll pay a price eventually. Or more
| accurately...the poor engineer that inherits your code in 10
| years will pay the price. God bless them if they try to do a
| medium sized refactor without types lol. I've been on both
| ends of the spectrum here.
|
| Pros and cons. There's _always_ a tradeoff for the business.
| fellowniusmonk wrote:
| Complex types don't exist. Schemas do.
|
| There is no duck, just primitive types organized duck-wise.
|
| The sooner you embrace the truth of mereological nihilism the
| better your abstractions will be.
|
| Almost everything at every layer of abstraction is structure.
|
| Understanding this will allow you to still use types, just not
| abuse them because you think they are "real".
| kapilkaisare wrote:
| This is the antidote to primitive obsession[0].
|
| [0]: https://wiki.c2.com/?PrimitiveObsession
| lolive wrote:
| Primitive object types in Java (String, Float, ...) are final.
| That blocks you from doing such tricks, as far as I understand.
| viktorcode wrote:
| The idea is to just wrap them in a unique type per intended use
| case, like AccountID, SessionID, etc. and inside the may
| contain a single field with String.
| hiddew wrote:
| Totally agree. And even the overhead of construction/destruction
| can be avoided in runtime for languages with inline types (e.g.
| https://kotlinlang.org/docs/inline-classes.html).
| skwee357 wrote:
| This is one of the reasons I adore Rust. Creating custom types in
| Rust geel very native and effortless.
| tonymet wrote:
| This is an accidental benefit of golang for those coming from
| python , perl or php. At first making structs and types is a
| pain. But within few hundred lines it's a blessing .
|
| Being forced to think early on types has a payoff at the medium
| complexity scale
| chaz6 wrote:
| The equivalent in Python is:- from typing import
| NewType UserId = NewType("UserId", int)
| sdeframond wrote:
| Actually, not really. In this case UserId is still an integer,
| which means any method that takes an integer can also take a
| UserId. Which means your co-workers are likely to just use
| integer out of habit.
|
| Also, you can still do integer things with them, such as
|
| > nonsense = UserId(1) + UserId(2)
| recursivedoubts wrote:
| they constantly try to escape from the darkness outside &
| within by dreaming of type systems so perfect that
| no one will need to be good but the strings that are will
| shadow the abstract datatype that pretends to be
| frankus wrote:
| Swift has a typealias keyword but it's not really useful for this
| since two distinct aliased types with the same underlying type
| can be freely interchanged. Wrong code may look wrong but it will
| still compile.
|
| Wrapper structs are the idiomatic way to achieve this, and with
| ExpressibleByStringLiteral are pretty ergonomic, but I wonder if
| there's a case for something like a "strong" typealias
| ("typecopy"?) that indicates e.g. "this is just a String but it's
| a particular _kind_ of String and shouldn 't be mixed with other
| Strings".
| titanomachy wrote:
| Yeah, most languages I've used are like this. E.g. rust/c/c++.
|
| I guess the examples in TFA are golang? It's kind of nice that
| you don't have to define those wrapper types, they do make
| things a bit more annoying.
|
| In C++ you have to be extra careful even with wrapper classes,
| because types are allowed to implicitly convert by default. So
| if Foo has a constructor that takes a single int argument, then
| you can pass an int anywhere Foo is expected. Fine as long as
| you remember to mark your constructors as explicit.
| ameliaquining wrote:
| Both clang-tidy and cpplint can be configured to require all
| single-argument constructors (except move, copy, and
| initializer-list constructors) to be marked explicit, in
| order to avoid this pitfall.
| ghosty141 wrote:
| Rust has the newtype idiom which works as proper type alias
| most of the time
| ameliaquining wrote:
| In what precise way are you envisioning that this would be
| different from a wrapper struct?
| frankus wrote:
| Pretty much only less boilerplate. Definitely questionable if
| it's worth the added complexity. And also it could probably
| be a macro.
| dataflow wrote:
| This sounds elegant in theory but very thorny in practice even
| with a standards change, at least in C++ (though I don't
| believe the issues are that particular to the language). Like
| how do you want the equivalent of std::cout <<
| your_different_str to behave? What about with third-party
| functions and extension points that previously took strings?
| jandrewrogers wrote:
| Isn't that where C++20 concepts come in?
| qcnguy wrote:
| Haskell has this, it's called newtype.
|
| In OOP languages as long as the type you want to specialize
| isn't final you can just create a subclass. It's cheap (no
| additional wrappers or boxes), easy, and you can specialize
| behavior if you want to.
|
| Unfortunately for various good reasons Java makes String final,
| and String is one of the most useful types to specialize on.
| buerkle wrote:
| But then you are representing two distinct types as the same
| underlying type, String. MyType extends
| String; void foo(String s); foo(new MyType()); //
| is valid
|
| Leading to the original problem. I don't want to represent
| MyType as a String because it's not.
| qcnguy wrote:
| It has to work that way or else you can't use the standard
| library. What you want to block is not:
| StringUtils.trim(String foo);
|
| but myApp.doSomething(AnotherMyType amt);
|
| The latter is saying "I need not any string but a specific
| kind of string".
| jwpapi wrote:
| I think the rule of thumb here is to avoid every kind of runtime
| check that can be checked at compile time.
|
| But if you have a function that works with different types you
| should make it more reusable.
|
| It's a good marker to yourself or to a review agent
| Izkata wrote:
| There was a post a decade or more ago, I think written with Java,
| that used variables like "firstname", "lastname", "fullname", and
| "nickname" in its example, including some functions to convert
| between them. Does this sound familiar to anyone?
|
| The examples were a bit less contrived than this, encoding
| business rules where you'd want nickname for most UI but real
| name for official notifications, and the type system prevented
| future devs from using the wrong one when adding new UI or
| emails.
| davidelettieri wrote:
| This in an usage of Value Objects as defined in DDD
| https://en.m.wikipedia.org/wiki/Value_object
|
| Also relevant https://refactoring.guru/smells/primitive-obsession
| mosferatu wrote:
| Came here to say this. This is an old thing. I'm guessing next
| we'll rediscover "Stringly Typed"?
|
| That refactoring guru raccoon reminds me of Minix for some
| reason.
| presz wrote:
| In TypeScript you can enable this by using BrandedTypes like
| this: type UserId = string & { readonly __tag:
| unique symbol };
|
| In Python you can use `NewType` from the typing module:
| from typing import NewType from uuid import UUID
| UserId = NewType("UserId", UUID)
| movpasd wrote:
| In Python 3.12 syntax, you can use type
| UserIs = UUID
| 12_throw_away wrote:
| `type UserId = UUID` creates a TypeAlias, not the same thing
| (from a type checker's point of view) as a NewType [1].
|
| [1] https://typing.python.org/en/latest/spec/aliases.html
| beders wrote:
| It is tempting, maybe a good first step, but often not expressive
| enough.
|
| Especially and particularly attributes/fields/properties in an
| enterprise solution.
|
| You want to associate various metadata - including at runtime -
| with a _value_ and use that as attribute/field/property in a
| container.
|
| You want to be able to transport and combine these values in
| different ways, especially if your business domain is subject to
| many changes.
|
| If you are tempted to use "classes" for this, you will sign up
| for significant pain later down the road.
| kwon-young wrote:
| This reminds me of the mp-units [1] library which aims to solve
| this problem focusing on the physical quantities. The use of
| strong quantities means that you can have both safety and complex
| conversion logic handled automatically, while having generic code
| not tied to single set of units.
|
| I have tried to bring that to the prolog world [2] but I don't
| think my fellow prolog programmers are very receptive to the idea
| ^^.
|
| [1] https://mpusz.github.io/mp-units/latest/
|
| [2] https://github.com/kwon-young/units
| ryandrake wrote:
| I remember a long, long time ago, working on a project that
| handled lots of different types of physical quantities:
| distance, speed, temperature, pressure, area, volume, and so
| on. But they were all just passed around as "float" so you'd
| every so often run into bugs where a distance was passed where
| a speed was expected, and it would compile fine but have subtle
| or obvious runtime defects. Or the API required speed in km/h,
| but you passed it miles/h, with the same result. I always
| wanted to harden it up with distinct types so we could catch
| these problems during development rather than testing, but I
| was a junior guy and could never articulate it well and justify
| the engineering effort, and nobody wanted to go through the
| effort of explicitly converting to/from primitive types to
| operate on the numbers.
| librasteve wrote:
| this was very much my intent with
| https://raku.land/zef:librasteve/Physics::Measure
| mabster wrote:
| I had kind of written off using types because of the complexity
| of physical units, so I will be having a look at that!
|
| My biggest problem has been people not specifying their units.
| On our own code end I'm constantly getting people to suffix
| variables with the units. But there's still data from clients,
| standard library functions, etc. where the units aren't
| specified!
| somethingsome wrote:
| I'm curious about what you think about something,
|
| Supoose you make two simple types one for Kelvin K and the other
| for Fahrenheit F or degrees D.
|
| And you implement the conversions between them in the types.
|
| But then you have something like
|
| d: D = 10;
|
| For i=1...100000: k=f_Take_D_Return_K(d)
| d=g_Take_K_Return_D(k)
|
| end
|
| Then you will implicitly have many many automatic conversions
| that are not useful. How to handle this? Is it easily catched by
| the compiler when the functions are way more complex?
| tomtom1337 wrote:
| I interpret your question as <<given that I am doing many
| conversions between temperature, because that makes it easier
| to write correct code, then I worry that my code will be slow
| because I am doing many conversions>>.
|
| My response is: these conversions are unlikely to be the slow
| step in your code, don't worry about it.
|
| I do agree though, that it would be nice if the compiler could
| simplify the math to remove the conversions between units. I
| don't know of any languages that can do that.
| somethingsome wrote:
| That's exactly the problem, in the software I have in mind,
| the conversions are actually very slow, and I can't easily
| change the content of the functions that process the data,
| they are very mathematical, it would take much time to
| rewrite everything.
|
| For example, it's not my case but it's like having to convert
| between two image representations (matrix multiply each
| pixel) every time.
|
| I'm scared that this kind of 'automatic conversion' slowness
| will be extremely difficult to debug and to monitor.
| tomtom1337 wrote:
| Why would it be difficult to monitor the slowness? Wouldn't
| a million function calls to the from_F_to_K function be
| very noticeable when profiling?
|
| On your case about swapping between image representations:
| let's say you're doing a FFT to transform between real and
| reciprocal representations of an image - you probably _have
| to_ do that transformation in order to do the the work you
| need doing on reciprocal space. There's no getting around
| it. Or am I misunderstanding?
|
| Please don't take my response as criticism, I'm genuinely
| interested here, and enjoying the discussion.
| somethingsome wrote:
| I have many functions written by many scientists in a
| unique software over many years, some expect a data
| format the others another, it's not always the same
| function that is called, but all the functions could have
| been written using a unique data format. However, they
| chose the data format when writing the functions based on
| the application at hand at that moment and the possible
| acceleration of their algorithms with the selected data
| structure.
|
| When I tried to refactor using types, this kind of
| problems became obvious. And forced more conversions than
| intended.
|
| So I'm really curious because, a part from rewriting
| everything, I don't see how to avoid this problem. It's
| more natural for some applications to have the data
| format 1 and for others the data format 2. And forcing
| one over the other would make the application slow.
|
| The problem arises only in 'hybrid' pipelines when new
| scientist need to use some existing functions some of
| them in the first data format, and the others in the
| other.
|
| As a simple example, you can write rotations in a
| software in many ways, some will use matrix multiply,
| some Euler angles, some quaternions, some geometric
| algebra. It depends on the application at hand which one
| works the best as it maps better with the mental model of
| the current application. For example geometric algebra is
| way better to think about a problem, but sometimes Euler
| angles are output from a physical sensor. So some
| scientists will use the first, and the others the second.
| (of course, those kind of conversions are quite trivial
| and we don't care that much, but suppose each conversion
| is very expensive for one reason or another)
|
| I didn't find it a criticism :)
| cat-whisperer wrote:
| I've been using this technique in Rust for years, really helps
| catch bugs early and makes code more readable. Wish more
| languages had similar type systems.
| Warwolt wrote:
| Isn't this just the newtype pattern?
| dajonker wrote:
| It is. To some it is more fun to reinvent the wheel than to
| study history
| William_BB wrote:
| What do you think about this but in C++ (e.g. with explicit
| constructors)? Has anyone had any experience with it? Did it
| succeed or fail?
| zzo38computer wrote:
| There are benefits of such things, especially if it can be
| handled by the compiler so that it does not make the code
| inefficient. In some cases it might even automatically convert
| the type, but often it is better to not do so. Furthermore, there
| may be an operator to ignore the type and use the representation
| directly, which must be specified explicitly (in order to avoid
| bugs in the software involving doing it by mistake).
|
| In the example, they are (it seems) converting between Celsius
| and Fahrenheit, using floating point. There is the possibility of
| minor rounding errors, although if you are converting between
| Celsius and Kelvin with integers only then these rounding errors
| do not occur.
|
| In some cases, a function might be able to work with any units as
| long as the units match.
|
| > Public and even private functions should often avoid dealing in
| floats or integers alone
|
| In some cases it makes sense to use those types directly, e.g.
| many kind of purely mathematical functions (such as checking if a
| number is prime). When dealing with physical measurements, bit
| fields, ID numbers, etc, it does make sense to have types
| specifically for those things, although the compiler should allow
| to override the requirement of the more specific type in specific
| cases by an explicit operator.
|
| There is another article about string types, but I think there is
| the problem of using text-based formats, that will lead to many
| of these problems, including needing escaping, etc.
| m0llusk wrote:
| This can solve a lot of problems, but also introduce awkward
| situations where it is hard to make a square shape or panel
| because the width measure must first be converted explicitly into
| a height measure in order to be used as such which might be
| considered correct but also expensively awkward and pedantic.
| manoDev wrote:
| > In any nontrivial codebase, this inevitably leads to bugs when,
| for example, a string representing a user ID gets used as an
| account ID
|
| Inevitably is a strong word. I can't recall the last time I've
| seen such bug in the wild. > or when a critical
| function accepts three integer arguments and someone mixes up the
| correct order when calling it.
|
| Positional arguments suck and we should rely on named/keyword
| arguments?
|
| I understand the line of reasoning here, but the examples are
| bad. Those aren't good reasons to introduce new types. If you
| follow this advice, you'll end up with an insufferable codebase
| where 80% LoC is type casting.
|
| Types are like database schemas. You should spend a lot of time
| thinking about semantics, not simply introduce new types because
| you want to avoid (hypothetical) programmer errors.
|
| "It is better to have 100 functions operate on one data structure
| than to have 10 functions operate on 10 data structures."
| Splizard wrote:
| Go is a great language because it has distinct types by default,
| it's not about "making invalid states unrepresentable", it's
| about recording relationships about a particular type of value
| and where it can be used ie. it doesn't matter that UserID is
| just a string, what matters, is that now you can see what string
| values are UserIDs without making assumptions based on naming
| conventions.
| abujazar wrote:
| Separate types for each model id is an extremely tedious way of
| avoiding bugs that can easily be prevented by a single test.
| mcapodici wrote:
| There are other benefits over a test.
|
| The compiler tests the type is correct wherever you use it. It
| is also documentation.
|
| Still have tests! But types are great.
|
| But sadly, in practice I don't often use a type per ID type
| because it is not idiomatic to code bases I work on. It's a
| project of its own to move a code base to be like that if it
| wasn't in the outset. Also most programming languages don't
| make it ergonomic.
| dilap wrote:
| Personally I like it, and it catches bugs right away,
| especially when there are multiple possible ids, e.g.
| func AddMessage(u UserId, m MessageId)
|
| If it's just func AddMessage(userId,
| messageId string)
|
| it's very easy to accidentally call as
| AddMessage(messageId, userId)
|
| and then _best-case_ you are wasting time figuring out a test
| failure, and worst case trying to figure out the bug IRL.
|
| V.S. an instant compile error.
|
| I have seen errors like this many times, both written by myself
| and others. I think it's great to use the type system to
| eliminate this class of error!
|
| (Especially in languages like Go that make it very low-friction
| to define the newtype.)
|
| Another benefit if you're working with any sort of static data
| system is it makes it very easy to validate the data -- e.g.
| just recursively scan for instances of FooId and make sure they
| are actually foo, instead of having to write custom logic or
| schema for everywhere a FooId might occur.
| socalgal2 wrote:
| My team recently did this to some C++ code that was using mixed
| numeric values. It started off as finding a bug. The bug was
| fixed but the fixer wanted to add safer types to avoid future
| bugs. They added them, found 3 more bugs where the wrong values
| were being used unintentionally.
___________________________________________________________________
(page generated 2025-07-24 23:00 UTC)