4 ms·
The opinion piece (that's what it is) starts with an assertion that types make it easier to find bugs. I think the reality is more nuanced. When types were pus
by ktRolster 11y ago
The opinion piece (that's what it is) starts with an assertion that types make it easier to find bugs. I think the reality is more nuanced.
When types were pushed into the C language (function prototypes, etc), it fixed a lot of bugs. The compiler would prevent you from pushing a short into an int, etc. Because that was a common and hard to debug mistake, this experience increased the popularity of types.
Typing of higher level objects is less useful. When you pass an object into the wrong function, the code usually doesn't even pretend to work (unlike an integer passed in as a float parameter). Also, because objects are so different, it's rare to accidentally pass an object as something else. The only time I've seen that happen is in very convoluted, multi-layered code bases.
We can see something similar in languages like Ruby or Perl, where it's actually kind of nice that you don't have to worry about whether a variable holds a string or integer or whatever. (For some reason, Javascript's implementation of the same idea seems to be a bit more annoying, but that could just be me).
So strong type systems are not necessarily a boon. (Of course, on the other hand, we have Haskell, where the type system can help reduce bugs).
- pron 11y ago> The opinion piece (that's what it is) starts with an assertion that types make it easier to find bugs. I think the reality is more nuanced. It is, and the article ends with the recommendation that specifications (or any code that requires verification of interesting properties) should be only optionally typed, with various pluggable type systems, because every specific type system would make an interesting class of problems harder to specify than untyped code.
- dllthomas 11y agoTypes help you enforce invariants. When you need to care about representation, representational types are quite useful. But it turns out, when we don't care about information interchange or a whole lot about performance, we usually don't care about representation as long as it's handled correctly, and a lot of automatic systems are okay (or better) at handling representation. More of what we care about is domain relevant, and that's a place where enforcing invariants can be important. As an incredibly simple example, passing a "bid" price where an "ask" is expected could be an expensive error, and is unlikely to trigger an exception. A type system like Haskell can be a big win, but we can enforce surprising things in something as simple as C. I had a project where I had several threads devoted to different activities, operating on their own static data. With a little creativity, I was able to track at compile time which threads a function could be run from and detect when I accessed data from the wrong thread. Without adding any run-time overhead, and while leaving me a free hand in organizing my data and functions in both my source files and my executable. It was tremendously convenient, when changes in specifications required me to move functionality from one thread to another, to be told everything that needed to change along with that. Over the course of the project, I can count on one hand the number of concurrency issues found in testing and only one - a complicated livelock - ever made it into production.
- versteegen 11y agoThat sounds really fascinating and educational. Could you possibly provide some more details or examples of how you accomplished that in C?
- dllthomas 11y agoI'm a bit sleep deprived (new baby) but I'd like to try to respond before the reply link goes away. There's a few layers to this. I threaded knowledge of what thread a function could run on through as the first argument. It was always called `th` and took the form of an empty struct. It turns out, if you pass an empty struct by value and don't do anything but pass it along, modern C compilers seem to generate identical byte code as if you removed the argument entirely (obviously, this isn't something you could strictly rely on, but it made no difference for correctness only efficiency, and if you're at the level where you care about that scale of efficiency you should be verifying all manner of assumptions when moving between compilers!). So now at any given point in any given thread-pinned function, you've got a variable in scope called `th`. Calling another thread-pinned function on the same thread looks like `foo(th, other, args)`. If that function isn't supposed to run on this thread, you get an error about trying to pass something of type `server_thread_t` as an argument of type `main_thread_t` (or whatever). Controlling data access could be done with getters and setters. I took a slightly different approach (which I'm not convinced is better, but it didn't cause me any pain this project), which was to define a macro used to declare global data that was bound to a thread, which would mangle the name and also create a no-op checking function. I had another macro that would take the simple name, check the thread, and resolve to the thing with the mangled name (as an LVAL). Having called that macro `L`, I now can treat `L(foo)` just as if it were a global variable named `foo`, and at runtime that's all it is, but at compile time my access to it is checked. I had a few other macros for putting a "lying" `th` in scope for those places I really needed to cross threads (`CROSSTH` for "I am accessing thread X from thread Y, `BEFORETH` for "I am touching this threads data before any threads were spawned", &c). Oh, the definition of the `th` argument in functions was baked into a macro so it looked like `int foo(TH(main), int something, char else)`. `TH` also added some attributes - I know there was `unused`, there might have been others... Hopefully that gives some sense of it. Let me know if you have further questions! :)
- tome 11y ago> When you pass an object into the wrong function, the code usually doesn't even pretend to work It very much does! It pretends to work whilst, in many cases, performing buffer overruns, arbitrary code execution, and so on.
- bhntr3 11y agoHere's a nice review of empirical studies on typing (http://danluu.com/empirical-pl/ http://danluu.com/empirical-pl/) by Dan Luu.
- acconsta 11y ago>the code usually doesn't even pretend to work It does pretend to work for a while, often failing in another function, in another file, with a cryptic message. All preventable by a simple type check.
- Darwin226 11y agoThose functions still pretend to work until you actually call them. I would say that it's a pretty big plus to know that your code is faulty without the evaluation actually having to run through the buggy function.
- kbenson 11y ago> We can see something similar in languages like Ruby or Perl, where it's actually kind of nice that you don't have to worry about whether a variable holds a string or integer or whatever. (For some reason, Javascript's implementation of the same idea seems to be a bit more annoying, but that could just be me). Well, for Perl that's likely because the conversion of the variable to the desired type is controlled by the operator, and as such they make distinctions between operators that work on numeric types (the usual assortment, +,-,/,*,==,!=,<,>,<=,>= etc) and those that work on strings (".",x,eq,ne,lt,gt,le,ge, etc). This makes the intended operation obvious, so explicit casting between types is often not required. For example, in JS, what is the following doing? String concatenation or addition? What if the types are mixed? var c = a + b; In Perl, the equivalent is one of the following, depending on your purpose. my $c = $a + $b; # Addition my $c = $a . $b; # Concatenation Note there is no ambiguity. If warnings are enabled, if a string can't be cleanly converted to a numeric type, it will report as much on STDERR. This all stems from a core tenet of Perl, similar things should look similar, different things should look different. String concatenation and addition aren't really similar, as much as they may seem like it at first blush (for example, concatenation is not commutative). As for Ruby... I don't know. It uses + for string concatenation (but also concat, I believe). I suspect having actual methods to disambiguate intention helps for Ruby.