7 ms·
Names are not type safety (2020)
- nixpulvis 1y agoIn Rust I find myself gaining a good bit of type safety without losing ergonomics by wrapping types in a newtype then implementing Deref for them. At first it might seem like a waste, but it prevents accidentally passing the wrong type of thing to a function (e.g. a user UUID as a post UUID).
- weinzierl 1y agoThis and for the use case from the article we will hopefully gain pattern types in Rust soon. They do not solve every problem that constructive data modeling does but in my opinion a large portion of what actually occurs in everyday programs. Since they are zero-cost I'd say their cost-benefit ratio is pretty good. Ada and Pascal also had handled the "encode the range in the type" nicely for decades.
- chiffaa 1y agoI want to point out that, technically, using Deref for this is an anti-pattern, as Deref is intended exclusively for smart pointers. Nothing really wrong with doing this outside of some loss in opacity (and unexpected behaviour if you're writing a library), but it's worth pointing out
- OptionOfT 1y agoNote that that language has changed: https://github.com/rust-lang/rust/commit/58645e06d9121ae37654b812d14041d0229d49c3#diff-6272bd172cf4be3f42a8e384d02cd6c72d8a9e9b7df2b228a72f02d052557162L11 https://github.com/rust-lang/rust/commit/58645e06d9121ae3765...
- nixpulvis 1y agoI don't really see the issue in providing Deref for a wrapper type like this. Could you elaborate? I'm not trying to gain full encapsulation, just trying to make sure I'm passing the right kind of wrapper, then using it transparently.
- 5pl1n73r 1y agoMy peers and I work on a language centered around "constructive data modeling" (first time I hear it called that). We implement integers, and indeed, things like non empty lists using algebraic data types, for example. You can both have a theory of values that doesn't rely on trapdoors like "int32" or "string", as well as encode invariants, as this article covers. As I understand it, the primary purpose of newtypes is actually just to work around typeclass issues like in the examples mentioned at the end of the article. They are specifically designed to be zero cost, because you want to not pay when you work around the type class instance already being taken for the type you want to make an instance for. When you make an abstract data type by not exporting the data constructors, that can be done with or without newtype.
- eru 1y agoThe alternative to newtypes is probably to go the same route as OCaml and have people explicitly bring their own instances for typeclasses, instead of allowing each type only one instance? I think OCaml calls these things modules or so. But the concepts are similar. For most cases, when there's one obvious instance that you want, having Haskell pick the instance is less of a hassle.
- 5pl1n73r 1y agoYes, I may have accidentally spilled, but I prefer ML modules to typeclasses.
- eru 1y agoI like ML-style modules in principle, but in practice I like the ergonomics of typeclasses. But while I did nearly half of my career in either OCaml or Haskell, I did all of my OCaml programming and most of my Haskell programming before the recent surge of really good autocompletion systems / AI; and I notice how much they help with Rust. So the ergonomics of ML-style modules might be perfectly acceptable now, when you have an eager assistant filling in the busy work for obvious cases. Not sure.
- kazinator 1y agoWhat if I want a type called MinusIntMaxToPlusIntMax? In other words the full range of Int? Is newtype still bad? In other words how much of this criticism has to do with newtype not providing sub-ranging for enumerable types? It seems that it could be extended to do that.
- lmm 1y agoIME this is exactly backwards: type safety is mostly about names, everything else is a nice-to-have. Yes, you can bypass your name checks if you want to, but you can bypass any type check if you want to. Most relevant type relationships in most programming are business relationships that would be prohibitively expensive to express in a full formalism if that was even possible. But putting names on them is cheap, easy, and effective. The biggest win from typed languages comes from using these basic techniques.
- b_e_n_t_o_n 1y agoHmm, IME the preferred type systems are structural - a function shouldn't care what the name is of the struct passed to it, it should just work if it has the correct fields.
- stirfish 1y ago> should just work if it has the correct fields. Correct fields by...name? By structure? I'm trying to understand.
- b_e_n_t_o_n 1y agoBy name, type, and structure. In typescript for example: let full_name = (in: { first: string, last: string }) => in.first + " " + in.last Then you can use this function on any data type that satisfies that signature, regardless of if it's User, Dog, Manager etc.
- tsimionescu 1y agoThis is very much backwards from most programming practices. It's much more common to have an operation that makes sense for many structurally different types, such as add(a, b) where a and b could be integers, floating point numbers, complex numbers, 3D vectors, 4D vectors, matrices etc.
- 1y ago
- skybrian 1y agoThe author seems concerned about compile-time range checking: did you handle the full range of inputs? Range checking can be very annoying to deal with if you take it too seriously. This comes up when writing a property testing framework. It's easy to generate test data that will cause out of memory errors - just pass in maximum-length strings everywhere. Your code accepts any string, right? That's what type signature says! In practice, setting compile-time limits on string sizes for the inputs to every internal function would be unreasonable. When using dynamically allocated memory, the maximum input size is really a system property: how much memory does the system have? Limits on input sizes need to be set at system boundaries.
- lexi-lambda 1y agoI can say that I am not particularly concerned with compile-time range checking. I agree with you that it is a massive headache that is almost always a huge waste of time. Even in dependently-typed languages, tracking ranges and bounds ends up requiring an incredible amount of bookkeeping that definitely does not seem worth the effort in the vast majority of application code. When I wrote this blog post, I used a very simple datatype because it was an extraordinarily simple example, but given many of the comments here, it seems it may have been too simple (and thus too contrived). It is only an illustration; don’t read into it too much.
- b_e_n_t_o_n 1y agoPerhaps it's because I'm not a haskeller but I'm not sure if I'm sold on encoding this into the type system. In go (and other languages for example), you would simply use a struct with a hidden Int, and receiver methods for construction/modification/access. I'm not sure I see the benefit of the type ceremony around it.
- the_af 1y agoIsn't the whole article a discussion of the kind of guarantees such an approach (which can also be done in Haskell) cannot provide?
- b_e_n_t_o_n 1y agoRight, I'm just unsure how valuable those guarantees really are. Especially if I'm extracting an Int out of the type to interface with other code.
- the_af 1y ago> Especially if I'm extracting an Int out of the type to interface with other code. Why would you do this? As soon as you go "outside" the type you lose typechecker guarantees. The whole point of the article is showing where the compiler can tell you when you're writing code that fails to consider some cases, and how use of `newtype` loses some of these guarantees.
- b_e_n_t_o_n 1y agoI don't think I'm gonna find a library that supports my OneToFive type.
- the_af 1y agoYour type comes with its own library, and the compiler makes sure you're not misusing it by failing to consider a possible case. If you need to use your OneToFive someplace that actually wants an integer, that happens at the boundary. Everything else is safer.
- andyferris 1y agoThese are possibly situations where I’d resort to a panic on the extra branch rather than complicate the return type. Providing a proof of program correctness is pretty challenging even in languages that support it. In most cases careful checking of invariants at runtime (where not possible at compile time) and crashing loudly and early is sufficient for reliable-enough software.
- valenterry 1y agoTitle should been "names are not ENOUGH for type-safety" but then no one would have read it I guess...
- auggierose 1y agoThese kinds of types are just a waste of time. It is going to be OneToSix or OneToSeven very soon... It's just an example! Well, if you cannot come up with a good example, maybe you don't have a point.
- ashton314 1y agoI take it you didn’t read the whole thing, as the next example is NonEmptyList, which is a good compelling example. It’s also not hard to think of other examples from my own work: I can imagine a URL type that only exposes constructors to create well-formed URLs. Etc etc. Really good examples will be rather domain-specific, so it’s perfectly understandable why Alexis would trust her readers to be able to imagine uses that suit their needs.
- auggierose 1y agoYes, I didn't, and I don't think NonEmptyList should have its own type either. This is just busy work and not a good example either.