9 ms·
Ad-hoc polymorphism erodes type-safety
- legobmw99 3y agoThis is an interesting point but does rely on a rather narrow definition of “type safety”. By any definition I have used, the code is type safe in those final examples. It has a bug, most definitely, but it isn’t type unsafe in the way that reading a chunk of memory intended to hold type A as if it was holding a type B is
- tlb 3y agoFully ad-hoc polymorphic languages like Smalltalk have this property, but many prefer having the compiler catch more errors.
- dcsommer 3y agoI think this claim has some merit. Perhaps the only thing missing (or perhaps intended to be self-evident) is that ad-hoc polymorphism is also useful and pragmatically, this situation doesn't come up too often. In particular, encapsulation reduces the frequency that such errors can be introduced. In the article's example, the `Setting` struct should (idiomatically) have private elements and use a method in the struct's impl block to expose the (optional) length of the field. And, to support the author's point, you wouldn't use ad-hoc polymorphism for this either. The author's concern should be considered when introducing ad-hoc polymorphism, especially when the type bounds are "loose" or commonly defined by many types.
- kzrdude 3y agoRust's iterability of Option is a good example for this because (1) it has actually bit me, but it wasn't too bad and (2) it is somewhere close to the border of where it's reasonable/useful to treat it as a sequence or not. Logically it works, but Option is not primarily a sequence. Then the Rust code in the article funnily enough not use traits for the `.iter()` call at all, that's just duck typing (no trait for .iter()).
- thinkharderdev 3y agoYeah. it's bit awkward to make `Option` implement `IntoIterator` and can certainly cause some weird and subtle bugs. But on the other hand it does make a whole lot of things work nicely. For example, you can turn a `Vec<Option<T>>` into a `Vet<T>` trivially with `foo.into_iter().flatten().collect()`. And generally it's nice to have Option and Vec composable.
- conaclos 3y agoI agree. Ironically the Rust standard library defines iter::once using Option::Some [O]. [0] https://doc.rust-lang.org/src/core/iter/sources/once.rs.html#55 https://doc.rust-lang.org/src/core/iter/sources/once.rs.html...
- gpderetta 3y agoIf instead of changing the allow list from a vector to optional vector, it was changed, for whatever reason,to a vector of vectors, wouldn't the monomorphic example compile and silently do the wrong thing? Even if you buy the thesis, the example is not terribly convincing.
- frenchy 3y agoThis is a good point, even without polymorphism there are risky refactors, but the polymorphism increases the number of potential risky refactors. On top of that, it might be a lot more obvious that turning a vector into a vector of vectors might be risky, but that you can iterate on Option won't be top-of-mind to everyone.
- twiss 3y agoI think part (but not all) of the blame in this case should fall to `length` and `.iter().count()` doing something rather different for a list and an Option/Maybe type. Does taking the length/iter of the latter really make sense? Or should one have to do `option.is_some() as usize` instead, if that's really what you want? If the same method always did the same for all types that implement it, this problem wouldn't occur. (That being said, I understand that that might be too much to ask for / can't always be avoided.)
- anderskaseorg 3y agoIt’s useful for Option to be iterable so that it works with things like .flat_map(), and it’s useful to be able to count from an arbitrary iterable. .iter().count() is a contrived way to find the length of a Vec though, since .len() is clearer and faster.
- chowells 3y agoBut you see, this behavior is the result of uniformly doing the same thing. You're asking for it to have special cases. The signature "length :: Foldable f => f a -> Int" is pretty specific. The polymorphism is parametric over `a' and ad-hoc over `f'. It has to select its behavior based only on the "container" type, rather than the "element" type. (The quoted terms are sloppy, but better for intuition than talking about deconstructing curried generative type constructor application.) Here's the thing about that. If your type is (Maybe (List Foo)), the `f' variable (container type) gets matched with `Maybe'. The `a' variable (element type) gets matched with (List Foo). So the fact that there's a list involved gets thrown away during instance resolution. And that's a good thing, because it lets you reason about your code's behavior uniformly. Just seeing the type of `length' is enough to tell you it has to work this way. Any attempt to work another way would fail to type-check. -- Ultimately I think I slightly agree with the original post, but I think it oversells the issue. Using ad-hoc polymorphism inside a definition that doesn't expose that polymorphism externally does allow a few more more incorrect implementations. That is some form of erosion of type safety, but a very minor one compared to what ad-hoc polymorphism allows you to recover in safety when writing code that's useful across many types.
- IshKebab 3y agoI mean, this is true. But traits are also very useful. Feels a bit like saying "driving is dangerous; avoid driving where possible". Also "allow-list" is so gross. Maybe pick something less divisive for future examples.
- nmjohn 3y ago> Also "allow-list" is so gross. Maybe pick something less divisive for future examples. What is gross or divisive about that?
- twic 3y agoI'm guessing this is a reference to the movement by some people to use that term instead of "whitelist" on the grounds that the latter is racist. There's a division over whether this is silly or not.
- fargle 3y agothe entire idea of having a "allow" list is itself exclusionary and divisive. why are some clients allowed and some are not? </sarc>
- IshKebab 3y agoLots of people (including me) think it sounds extremely stupid and "whitelist" was perfectly fine anyway. Reminds me of "mebi-byte", which is similarly gross (but at least that had sensible motivation).
- deredede 3y agoLots of people think whitelist is not fine, and decide to use another term whose meaning is perfectly clear and understandable. You don't like that term, but what would you have people use that's less divisive? Clearly "whitelist" does not work, since it was the initial source of the issues.
- IshKebab 3y ago
- blkhp19 3y agoIn Swift, you shouldn't use Optional like this. You should instead make a new type to represent exactly what parameter this function expects: enum AllowList { case all case some([ClientIdentifier]) }
- thelopa 3y agoI would argue that you should do something similar in Haskell, too. It would ensure that practically everything accessing the allow list fails to compile unless it had been adapted to the new model. Haskell’s strength is the compiler and its ability to pedantically enforce type errors. Use it! Let it help you!
- diarrhea 3y agoSo far, in Rust, that has always felt unidiomatic and I haven't seen this pattern around a lot. It always felt like reimplementing Option/Result but worse: "Why use this when something conceptually similar exists in the stdlib?" But that is exactly the point: it's only conceptually similar, but in this case deceivingly so. In Rust, we make abundant use of newtypes; this enum is also a new type. Only once it is treated as such does actual type safety kick in. I take it the same principle applies to Haskell. Good to know there's some merit to it.
- pxeger1 3y agoThe claim > Using ad-hoc polymorphism can cause code to silently do the wrong thing after a refactor because of a type error, in a way that monomorphic code would not have doesn't mean languages with ad-hoc polymorphism are less type-safe overall. Ad-hoc polymorphism allows you to write stronger-typed code more easily.
- taeric 3y agoThis feels less of polymorphism eroding type safety as it is aliased terms eroding it. Stated differently, programming/abstraction doesn't protect you from synonyms. That is, it means different things to iterate over all allowed values of a list than it does to iterate over the existence of something. That they can be abstractly described in the same way feels powerful, of course, but such is the case of powerful tools?
- tlb 3y agoYes, and common synonyms like `length` can hose you when you convert from a string to a list of strings. They shouldn’t really have the same name, especially in a Unicode world where there are several different lengths for a string.
- cmrdporcupine 3y agoYes it's in the same category of "we know better now, hopefully?" as people overloading the + operator for string concatenation. Refining for clarity of categories is hard. I'm not sure 'type polymorphism' is the problem here as much as it is false conceptual polymorphism.
- cryptonector 3y agoYes, this is about aliases. When you see `length` in a language like Haskell you don't know what length exactly it refers to without looking at the type of its argument. But if you see `list_of_foo_length` then you know there can be only one `list_of_foo_length` and where to look for it. This makes things like source code indexers have to know how to parse the language and how to understand its types even. Compare to the lowly ctags/etags/cscope of C world! Clearly monomorphic APIs are easier to deal with in some ways, though they also clutter things up a bit.
- tialaramex 3y agoThe Rust example provided isn't showing what the author seems to claim it's showing, which is interesting. You see, that function named iter() isn't some trait feature, it's just a convention. So this is a coincidence, of exactly the sort which could arise regardless of a feature like traits. Option::iter() is a function defined on the Option type, and [T]::iter() is a function defined on slice types [T]. The Vec is getting coerced into a slice to call that iter() function in the case where there's a Vec here. These functions do similar things because of the convention, but they could in principle have completely unrelated behaviour -- it's just a convention -- and this would still compile. For example suppose we instead change allow_list to my custom Doodad type. Doodad can cheerfully provide a method named iter() which gives you a Debretts (Debrett's publishes a notable guide to British Peerages) and my Debretts type has a method count() which tells you how many counts and countesses there are currently for some reason (zero, England doesn't use this title). That compiles just fine, not because "Ad-hoc polymorphism erodes type safety" but because we just wrote iter().count() and we get whatever that does, if we wanted to say "I assert iter gives me an Iterator over items of type ClientIdentifier", we can say that, but we did not.
- karmakaze 3y agoI was expecting/hoping to learn something new, and instead only came to this same realization. The most I could say is that interfaces/etc can sometimes give a false sense of (type) safety. On finding the cause of such a fault, I don't shake my fist at interfaces, I say "Doh! I should know better." > Some responders seemed surprised by this idea, and I thought this was common knowledge but could not find any blog posts about it either. I figured maybe it was worth writing out, and some people agreed, so here we are: The idea is both surprising and common knowledge. Surprising to think that it's a special kind of type weakness, rather than a common one where actual cases are not surprising at all. Nice writing, more substantial content would be great! If we take away all the big words, use Java instead of Haskell/Rust and start with the example that breaks, it would be a much shorter post that no one would care to read. I don't know that the great writing and build-up is worthwhile, other than a pleasant if somewhat disappointing read.
- WirelessGigabit 3y agoIsn't the idea with traits that you can pass in anything you want that implements the trait? I think a larger problem here is that the function takes Settings and not a &Vec Option<&Vec>, that way you'd notice immediately. Or you split the code and add type constraints: struct ClientIdentifier {} struct Settings { allow_list: Vec<ClientIdentifier> } struct Settings2 { allow_list: Option<Vec<ClientIdentifier>> } fn allow_list_count(settings: Settings) -> usize { // clear you're iterating over a slice let iter: std::slice::Iter<'_, _> = settings.allow_list.iter(); // and the count is len iter.count() } fn allow_list_count_2(settings: Settings2) -> usize { // clear that you're iterating over Option<T> let iter: std::option::Iter<'_, _> = settings.allow_list.iter(); // which is 0 (None) or 1 (Some(T)) iter.count() }