4 ms·
I’ve programmed professionally in Haskell, Scala and Python in large projects across several jobs. In my experience, the compiler is rarely helpful at catching
by mlthoughts2018 8y ago
I’ve programmed professionally in Haskell, Scala and Python in large projects across several jobs.
In my experience, the compiler is rarely helpful at catching bugs. Most bugs, whether in a dynamic typing language or otherwise, are behavioral bugs that occur at runtime without generating explicit runtime errors, just incorrect but uninterrupted behavior.
I always heard people make grandiose claims about Haskell, like if you get your program to compile, it’s very likely to be correct and run without error. I’ve emphatically never found that to be true.
The types of bugs that compilers can help with are very simplistic most of the time. Requesting a method or attribute that doesn’t exist, typos, confusion about arguments passed to a function.
In dynamic typing, you also solve these same things in a super cheap and low effort way with unit tests.
Additional behavioral unit tests are where real effort becomes required, and these are needed in any paradigm.
Compilers are also not free. My experience with the Scala compiler for example was awful. It is so incredibly slow to compile that I absolutely would rather give up type checking and just use some cheap unit tests to have a much faster development cycle. When you make 1 or 2 small changes and even a smart incremental compiler setup like zinc needs to recompile 30 code units and it takes ~ 3 minutes before you can run tests, this is maddening, and the cycle repeats the whole time you’re working, so it’s this constant extremely disruptive expense.
I’ve had similar thinga happen in legacy code bases with Haskell too.
- dope99 8y agoI think most bugs are that kind that the compiler can catch. But yes absolutely a program can compile and be dead wrong. I’m writing a regex engine and lol at types saving me from all the mistakes I can make with building and transforming finite automata. I’d still much rather have a slightly slow compilation than verify types in my head every time I’m in that area of code, or writing unit tests for every configuration of code which basically just validate I haven’t returned any nulls. Typed compilation may take time, but it results in faster running programs. And unit tests also take time. I find the errors generated by a type checker come quicker and give more useful diagnostics for deep areas of the code.
- mlthoughts2018 8y agoMy experience has been the opposite. Compilers are not just a little slow, but introduce a significant slowdown for every read-edit-test loop, which adds up to such huge productivity losses that any small gains from the compiler catching typo-style / wrong arguments sorts of bugs is totally erased.
- dope99 8y agoI’m not arguing here but honestly don’t know: how can interpretation be faster than the checking step of compilation? Maybe code generation takes more time, but Rust has ‘cargo check’ which only typechecks. Parsing is basically a wash between compilation and interpretation. Dynamic types still need to be resolved for the tests to run. So why would interpretation be faster? I’m working through a compiler book and would love to know.
- ghettoimp 8y agoI think mlthoughts2018 may be saying that he finds the advantages of a good "read-edit-test loop" to be more valuable than a compiler that catches type errors? It is certainly valuable. A good REPL is completely fantastic for prototyping and debugging. Being able to change how your program works while it's still running and has all its data loaded is great compared to a classic edit-compile-run cycle where you've got to get your program's data re-loaded each time. But I don't see that this has much to do with compilers versus interpreters, or even really dynamic versus static type checking... - There are REPL-based environments with integrated compilers, and there are compiled languages with REPLs bolted on top. - Good REPL support is surely more challenging for languages with strong static typing. After all: what does it mean to redefine a type? what happens to the functions that are using the old definitions? what happens to the instances of that type that are already alive in your program? But these problems all exist in dynamic languages too, it's just easy there to sweep them under the rug by treating them as yet more run-time errors.
- mlthoughts2018 8y ago
- int_19h 8y ago> In dynamic typing, you also solve these same things in a super cheap and low effort way with unit tests. I wouldn't call it "super cheap and low effort", given that tests for stuff like "what if it's not an integer?" tend to be a large part of the overall unit test suite in dynamic languages. Just on the amount of code alone, I would say that it's far less effort to express that kind of stuff in types than in tests.
- spronkey 8y agoIf you force your types to only ever interact with primitives at certain boundaries of your app, you can test for the "what if it's not an integer" stuff at the boundaries only. I.e. the core APIs never see a magic string, an integer, or any other such untyped thing. In reality, there are always times where that falls a little flat, which is where IMO Gradual Typing comes into its own. The "core", with some assumptions about the boundaries, be statically analysed. You can add your times(int n) method safe in the knowledge that you'll get a runtime error if someone messes up the dynamic type or doesn't run the analyser. Ultimately, as I want to run all my code before I put into to production, a lack of runtime errors from my tests tells me my code is correct for the situations I designed it to handle. Now if someone comes along later and manages to invoke a state that I hadn't considered, well... all bets are off.
- mlthoughts2018 8y agoIf you’re writing tests like “what if the input is not an integer” in Python, you’re probably doing a lot more stuff wrong. Dynamic typing is useful typically because you rarely need to check these types of issues. Instead you’d have some sort of input sanitizer that is responsible for all such inputs into a large system, and would include what type of failover logic should happen upon receiving a wrong input. If you’re testing all kinds of different functions for argument checks or defensive programming conditions, it suggests a refactor to a central sanitizer pattern, and just one small set of sanitization tests. Other functions can take it for granted that the sanitizer works correctly. You often need the same thing in statically typed systems anyway for cases when calculating whether an input is valid involves conditions not encoded by the type system (possibly even runtime configurable by the user). So except for all but the most trivial kinds of type validation, this does not actually constitute a difference in effort between the two paradigms anyway.