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