13 ms·
Use Your Type System
- zeroCalories 1y agoI 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_ 1y agoAlso 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 1y agoAllocating objects for every single property can turn pretty bad in Java. A strong enough type system would be a lot more useful.
- pclowes 1y agoI 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 1y agoFYI: 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 /Users/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_ 1y agoThere seem to be two competing nomenclatures around strong/weak typing where people mean static/dynamic instead.
- jnpnj 1y agoyes, untyped names != untyped objects
- josephg 1y agoSome 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 1y agoIt'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.
- jamezzzboy 1y ago[flagged]
- jjice 1y agoDoes 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 1y agoI'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.html https://doc.rust-lang.org/rust-by-example/generics/new_types...
- frou_dh 1y ago"newtype", or kind of (but not exactly) the opposite of "Primitive Obsession"
- marcosdumay 1y ago"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 1y agoStrongly Typed Identifier https://en.wikipedia.org/wiki/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 1y agoUsing 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.
- Noumenon72 1y agoDoes 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 1y agoHighly recommend the Early Access book Data-Oriented Programming with Java by Chris Kiehl as another resource.
- goostavos 1y agoHey, I'm that guy! Thanks for the shout out!
- fedeb95 1y agoThis 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 1y agoThis 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 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 1y agoMy 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.html https://lukasschwab.me/blog/gen/deriving-safe-id-types-in-go... https://lukasschwab.me/blog/gen/safe-incompatibility.html https://lukasschwab.me/blog/gen/safe-incompatibility.html
- taeric 1y agoHard 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 1y agoMaybe 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")`.
- Maxion 1y agoThis stuff gets unbearable very fast. We have custom types for geometries at my work. We also use a bunch of JS libraries for e.g. coordinate conversions. They output as [number, number, number], whereas our internal type are number[].
- stellalo 1y agoIdeally 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?
- chriswarbo 1y ago"Phantom types" are useful for what you describe: that's where we add a parameter to a type (i.e. making it generic), but we don't actually use that parameter anywhere. I used this when dealing with cryptography in Scala, where everything is just an array of bytes, but phantom types prevented me getting them mixed up. https://news.ycombinator.com/item?id=28059019 https://news.ycombinator.com/item?id=28059019
- recursivedoubts 1y agoType 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 https://grugbrain.dev/#grug-on-type-systems
- 3836293648 1y agoTo be fair, you probably needed to know that anyway? Or else you would've just passed invalid data into functions.
- recursivedoubts 1y agoI cannot recall ever passing an invalid UUID (or long id) into a function due to statically-knowable circumstances.
- happytoexplain 1y agoThe 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 1y agoMore 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 1y ago
- ho_schi 1y agoI'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 1y agoThis 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 1y agoThank 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 1y agoI was doing this and used it for a year in https://github.com/bbkane/warg/ 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 1y agoYep 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 1y agoThis 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 https://go.dev/play/p/4eNQOJSmGqD
- skybrian 1y agoThis 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.
- drpixie 1y agoArrggg- that's the best reason, so far, to avoid Go. Almost nothing is a number. A length is not a number, an age is not a number, a phone number is not a number - sin(2inches) is meaningless, 30years^2 is meaningless, phone#*2 is meaningless, and 2inches+30years is certainly meaningless - but most of our languages permit us to construct, and use, and confuse these meaningless things.
- jshxr 1y agoUnfortunately, 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.
- deleted 1y ago[deleted]
- deleted 1y ago[deleted]
- paldepind2 1y agoI 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 1y agoI'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 1y agoAn 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 1y agoWith 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 1y agoSetting 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 1y agoThat'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.
- tyleo 1y agoIn 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 1y agoThere are libraries for that, such as Vogen https://github.com/SteveDunn/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.
- tyleo 1y agoThis seems like overkill. I’d prefer the few lines of code above to a whole library.
- SideburnsOfDoom 1y agoIs it "overkill" if it's already written and tested? Once you have several of these types, and they have validation and other concerns then the cost-benefit might flip. FYI, In modern c#, you could try using "readonly record struct" in order to get lots of equality and other concerns generated for you. It's like a "whole library" but it's a compiler feature.
- tyleo 1y agoYes: more code to compile, more stuff to learn, more complexity. I gave like a 5-line-of-code example, I don’t understand why I’d want to replace that with a library.
- 1y ago
- MantisShrimp90 1y agoIm 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 1y agoA 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.
- rmunn 1y agoA wise person once told me that if you ever find yourself saying "if only everyone would just do X...", then you should stop right there. Never, ever, in the history of the world has everyone done X. No matter how good an idea X is, there will always be some people who say "No, I'm going to do Y instead." Maybe they're stupid, maybe they're evil, maybe they're just ignorant... or maybe, just maybe, X was not the best thing for their particular needs and Y was actually better for them. This is an important concept to keep in mind. It applies to programming, it applies to politics, it applies to nearly every situation you can think of. Any time you find yourself wishing that everyone would just do X and the world would be a better place, realize that that is never going to happen, and that some people will choose to do Y — and some of them will even be right to do so, because you do not (and cannot) know the specific needs of every human being on the planet, so X will not actually be right for some of them.
- jerf 1y agoThis 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 1y agoCode review time: + int doTheThing(bool,bool,int,int); Die a little bit inside.
- wvenable 1y agoI 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.
- IshKebab 1y agoYeah I agree with wvenable. It's definitely safer but most languages have very poor support for doing it ergonomically. In fact I have yet to find one that makes it genuinely nice.
- mk_chan 1y agoI’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/MetaValueType.h https://github.com/Mk-Chan/libchess/blob/master/internal/Met... https://github.com/Mk-Chan/libchess/blob/master/Square.h https://github.com/Mk-Chan/libchess/blob/master/Square.h
- gpderetta 1y agoBut it can be a bit easier! You could make Square itself an enum class and overload the operators for it directly.
- vemv 1y agoMost 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 1y ago> 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 1y agoThere 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 1y agoI'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 1y agoIt 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 1y agoAn 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.
- rhubarbtree 1y ago
- fellowniusmonk 1y agoComplex 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".
- sparkie 1y agoArguably, you can get rid of "primitive types" entirely. xtc-lang's "Turtles type system" does so, where all the built-in types are defined in the standard library, where the definitions are infinitely recursive, but we have a fixpoint which we can use as if it were "primitive". > The Ecstasy type system is called the Turtles Type System, because the entire type system is bootstrapped on itself, and -- lacking primitives -- solely on itself. An Int, for example, is built out of an Array of Bit, and a Bit is built out of an IntLiteral (i.e. 0 or 1), which is built out of a String, which is an Array of Char, and a Char is built out of an Int. Thus, an Int is built out of many Ints. It's turtles, the whole way down. [1]:https://xtclang.blogspot.com/2019/06/an-introduction-to-ecstasy-type-system.html https://xtclang.blogspot.com/2019/06/an-introduction-to-ecst...
- kapilkaisare 1y agoThis is the antidote to primitive obsession[0]. [0]: https://wiki.c2.com/?PrimitiveObsession https://wiki.c2.com/?PrimitiveObsession
- lolive 1y agoPrimitive object types in Java (String, Float, …) are final. That blocks you from doing such tricks, as far as I understand.
- viktorcode 1y agoThe 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 1y agoTotally 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 https://kotlinlang.org/docs/inline-classes.html).
- skwee357 1y agoThis is one of the reasons I adore Rust. Creating custom types in Rust geel very native and effortless.
- tonymet 1y agoThis 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 1y agoThe equivalent in Python is:- from typing import NewType UserId = NewType("UserId", int)
- sdeframond 1y agoActually, 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)
- chaz6 1y agoIt will not stop you doing this at runtime of course. It is only intended for type checking.
- recursivedoubts 1y agothey 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
- deleted 1y ago[deleted]
- frankus 1y agoSwift 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 1y agoYeah, 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 1y agoBoth 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 1y agoRust has the newtype idiom which works as proper type alias most of the time
- ameliaquining 1y agoIn what precise way are you envisioning that this would be different from a wrapper struct?
- frankus 1y agoPretty much only less boilerplate. Definitely questionable if it's worth the added complexity. And also it could probably be a macro.
- jwpapi 1y agoI 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 1y agoThere 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 1y agoThis in an usage of Value Objects as defined in DDD https://en.m.wikipedia.org/wiki/Value_object https://en.m.wikipedia.org/wiki/Value_object Also relevant https://refactoring.guru/smells/primitive-obsession https://refactoring.guru/smells/primitive-obsession
- mosferatu 1y agoCame 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 1y agoIn 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 1y agoIn Python 3.12 syntax, you can use type UserIs = UUID
- 12_throw_away 1y ago`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 https://typing.python.org/en/latest/spec/aliases.html
- beders 1y agoIt 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 1y agoThis 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/ https://mpusz.github.io/mp-units/latest/ [2] https://github.com/kwon-young/units https://github.com/kwon-young/units
- ryandrake 1y agoI 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 1y agothis was very much my intent with https://raku.land/zef:librasteve/Physics::Measure https://raku.land/zef:librasteve/Physics::Measure
- yawaramin 1y agoSee https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/units-of-measure https://learn.microsoft.com/en-us/dotnet/fsharp/language-ref...
- esafak 1y agohttps://nim-by-example.github.io/types/distinct/ https://nim-by-example.github.io/types/distinct/ https://kotlinlang.org/docs/inline-classes.html https://kotlinlang.org/docs/inline-classes.html https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3320.htm https://www.open-std.org/jtc1/sc22/wg14/www/docs/n3320.htm https://doc.rust-lang.org/rust-by-example/generics/new_types.html https://doc.rust-lang.org/rust-by-example/generics/new_types...
- somethingsome 1y agoI'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?
- deleted 1y ago[deleted]
- tomtom1337 1y agoI 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 1y agoThat'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 1y agoWhy 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.
- cat-whisperer 1y agoI'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 1y agoIsn't this just the newtype pattern?
- dajonker 1y agoIt is. To some it is more fun to reinvent the wheel than to study history
- sparkie 1y agoWell, "newtype pattern" is itself a reinvention of a decades old concept. Presumably "newtype pattern" gets its name from the `newtype` keyword in Haskell, which was introduced in Haskell 1.3 (1996). To the best of my knowledge, the earliest specific term for the concept is painted types (Simonyi, 1976)[0], but I believe this was a new term for a concept that was already known. Simonyi himself quotes (Hoare, 1970)[1]. Hoare doesn't provide a specific term like painted type for the type being defined, but he describes new types as being built from constintuent types, where a singular constituent type is known as the base type. Simonyi uses the term underlying type rather than base type. Hoare eludes to painted types by what they contain - though he doesn't explicitly require that the new type has the same representation as it's base type - so they're not necessarily equivalent, though in practice they often are. Simonyi made this explicit, which is what we expect from newtype in Haskell - and specifically why we'd specifically use `newtype` instead of `data` with a single (base) constituent. If you're aware of any other early references, please share them. --- [0]:https://web.archive.org/web/20170313211616/http://www.parc.com/content/attachments/meta-programming-csl-76-7.pdf#page=42 https://web.archive.org/web/20170313211616/http://www.parc.c... > These examples show that the idea of types is independent of how the objects belonging to the type are represented. All scalar quantities appearing above - column numbers, indices and so forth, could be represented as integers, yet the set of operations defined for them, and therefore their types, are different. We shall denote the assignment of objects to types, independent of their representations, by the term painting. When an object is painted, it acquires a distinguishing mark (or color) without changing its underlying representation. A painted type is a class of values from an underlying type, collectively painted a unique color. Operations on the underlying type are available for use on painted types as the operations are actually performed on the underlying representation; however, some operations may not make sense within the semantics of the painted type or may not be needed. The purpose of painting a type is to symbolize the association of the values belonging to the type with a certain set of operations and the abstract objects represented by them. [1]:https://www.cs.cornell.edu/courses/cs4860/2018fa/lectures/Notes-on-Data-Structuring_Hoare.pdf#page=11 https://www.cs.cornell.edu/courses/cs4860/2018fa/lectures/No... > In most cases, a new type is defined in terms of previously defined constituent types; the values of such a new type are data structures, which can be built up from component values of the constituent types, and from which the component values can subsequently be extracted. These component values will belong to the constituent types in terms of which the structured type was defined. If there is only one constituent type, it is known as the base type.
- William_BB 1y agoWhat 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 1y agoThere 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.
- lcfcjs6 1y ago[dead]
- m0llusk 1y agoThis 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 1y ago> 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 1y agoGo 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 1y agoSeparate types for each model id is an extremely tedious way of avoiding bugs that can easily be prevented by a single test.
- mcapodici 1y agoThere 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 1y agoPersonally 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.
- Warwolt 1y agoTypes give you static proof where tests only give partial inductive evidence. I cannot _fathom_ why people would prefer tests over types where types do the job, outside anything but sheer ignorance.
- socalgal2 1y agoMy 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.
- stillpointlab 1y agoI've used the approach described for uuids on a project and I liked it. We were using typescript so we went further using template literal types [1] type UserId = `user:${uuid}`; type OrgId = `org:${uuid}`; This had the benefit that we could add validation (basic begins with kind of logic) and it was obvious upon visual inspection (e.g. in logs/debugging). 1. https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html https://www.typescriptlang.org/docs/handbook/2/template-lite...
- oehpr 1y agoI assume you used these against a relational database? Did you commit those ids with the prefix still attached? or did you `.split()[1]` or something? I think it's a pretty good idea. I'm just wondering how this translated to other systems.
- stillpointlab 1y agoWe were using Mongo and stored the ids with the prefix in the DB as the primary key. Pretty much everywhere we were passing them around as strings, never as 128 bit int, so there was no integrity checking outside of the app layer. The only drawback was marshalling the types when they come out of the db layer. Since the db library types were string we had to hard cast them to the correct types, really my only pain. That isn't such a big deal, but it means some object creation and memory waste, like: // pseudo code: const results = dbclient.getObjectsByFilter( ... ); return results.map(result => ({ id: result.id as ObjectId, ... })); We normally didn't do it, but it would be at that time you could have some `function isObjectId(id:string) : id is ObjectId { id.beginsWith("object:"); }` wrapper for formal verification (and maybe throw exceptions on bad keys). And we were probably doing some type conversions anyway (e.g. `new Date(result.createdAt)`). If we were reading stuff from the client or network, we would often do the verification step with proper error handling.
- spartanatreyu 1y agoTypescript's template literal types are awesome when creating values that have to match a specific format. But depending on the format it can sometimes be tricky to narrow a string back down to that format. We have type guards to do that narrowing. (see: https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates https://www.typescriptlang.org/docs/handbook/2/narrowing.htm..., but their older example is a little easier to read: https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types https://www.typescriptlang.org/docs/handbook/advanced-types....) If writing the check is too tricky, sometimes it can just be easier to track the type of a value with the value (if you can be told the type externally) with tagged unions (AKA: Discriminated unions). See: https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes-func.html#discriminated-unions https://www.typescriptlang.org/docs/handbook/typescript-in-5... And if the formats themselves are generated at runtime and you can use the "unique" keyword to make sure different kinds of data are treated as separate (see: https://www.typescriptlang.org/docs/handbook/symbols.html#unique-symbol https://www.typescriptlang.org/docs/handbook/symbols.html#un...). You can combine `unique symbol` with tagged unions and type predicates to make it easier to tell them apart.
- UltraSane 1y agoYou can do this in Python using NewType from typing import NewType UserId = NewType('UserId', int) some_id = UserId(524313)
- bern4444 1y agoThis seems like a conclusion derived from the ideas in parse don't validate[1]. The goal is to encode the information you learn while parsing your data into your type system. This unlocks so many capabilities: better error handling, making illegal states unrepresentable, better compiler checking, better autocompletion etc. [1]https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/ https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-va...
- jhhh 1y agoThe problem with this post is that the author is conflating two different things. Using a type system to capture the units of a measurement or metric is straightforwardly better than having them be implied. Stripping a numeric value and unit down to just a value involves an obvious loss of information. That situation is wholly different than just wrapping your UUID in some bespoke type which doesn't provide any extra information. They just look the same because you're mechanically doing something similar (ie. wrapping some primitive or standard type in something else). Not to mention unless you want to make your wrappers monads you're going to have to unwrap them at some point anyway at which point you can still transpose the actual type when you have to call any external library/function. I would love to know what the test suites of these 'many bugs in real systems' projects looked like. I suspect the test suite coverage wasn't very good.
- Mawr 1y ago> I would love to know what the test suites of these 'many bugs in real systems' projects looked like. I suspect the test suite coverage wasn't very good. The same argument gets brought up in favor of dynamic typing. The point of typing is that you don't need all those repetitive tests. Moreover, the coding feedback loop gets shorter since there's no need to wait until the tests run to find out a string was passed in instead of an int (or UserID).
- denkmoon 1y agoIf I had a dollar for every stringly typed class or method I've come across I could retire. Why not have all your db UUID primary keys as strings :(
- fny 1y agoThis is also an incredibly useful technique with LLMs. If you alias types (e.g. str to DateStr) the LLM can better infer which functions to select and how to compose them
- Ey7NFZ3P0nzAe 1y agoShoutout to the absolutely awesome beartype for python. With simple decorators it adds runtime type checking with virtually no performance cost!! https://beartype.readthedocs.io/en/latest/ https://beartype.readthedocs.io/en/latest/ Or see their page about performance: https://beartype.readthedocs.io/en/latest/faq/#faq-realtime https://beartype.readthedocs.io/en/latest/faq/#faq-realtime
- throwaway290 1y agoyou never got a TypeError before? Python already has runtime type checking
- Ey7NFZ3P0nzAe 1y agoIf you have a TypeError it's already too late. Decorating everything with beartype means I can catch things way in advance. It also forces me to specify types to all my functions which really helps LLM. See for example in wdoc, my advanced personal RAG library: https://github.com/thiswillbeyourgithub/wdoc/blob/main/wdoc/utils/typechecker.py https://github.com/thiswillbeyourgithub/wdoc/blob/main/wdoc/...
- throwaway290 1y agoIn advance but at runtime? I don't get the point still...
- Ey7NFZ3P0nzAe 1y agoLet me give you an example from just a minute ago: in plenty of cases np.array works on arrays as well as on lists. So you can have for example: ``` import numpy as np def func(inputA: np.ndarray, inputB: np.ndarray) -> np.ndarray: return np.concat(inputA, inputB) ``` But then you want to modify the code for some reason way later and do this: ``` def func(inputA: np.ndarray, inputB: np.ndarray) -> np.ndarray: if len(inputB.shape) == 1: return np.concat(inputA, inputB) else: return inputB ``` Now imagine that inputB is directly received from some library you imported. So pyright might not be able to check its type and inputB is actually a List. Then you will never get a crash in the version 1. But its types are wrong as inputB is a List. The version 2 on the other hand will crash as List don't have a shape attribute. Notice also how func returns inputB, propagating the wrong type. Sure that means the code still works until you modify version 1, but any developpers or LLM that reads func would get the wrong idea about how to modify such code. Also this example is trivial but it can become much much more complicated of course. This would not be caught in pyright but beartype would. I'm basically using beartype absolutely everywhere I can and it really made me way more sure of my code. If you're not convinced I'm super curious about your reasoning! PS: also try/except in python are extremely slow so figuring out types in advance is always AFAIK a good idea performance wise.
- arch_btw 1y agoWait until you find out about Julia, the typesystem is brilliant. It encourages this kind of thing to allow you to give more info about what the number represents. It ensures there are no downsides because you can define functions to run on families of types.
- mckravchyk 1y agoI'm using typed IDs in TypeScript with template literal types. I.e. `type UserId = `user_${string}` I've written a blog post about it a while ago [1] One can argue that this pollutes runtime, however, it is a feature to me. When the ID pops up in the logs, it is instantly obvious what the object is, error messages are more meaningful. You can notice that Stripe uses such approach in their API. [1] https://www.velopen.com/blog/adding-type-safety-to-object-ids-typescript/ https://www.velopen.com/blog/adding-type-safety-to-object-id...
- SergeAx 1y agoThis is how we implemented DDD in several teams: everything is either an entity, a value object, or an aggregate root. Generic integers and strings exist only in the infrastructure/interface layers between users or external systems and the domain kernel.