In OCaml (or any similar language) 95% of what you "unit test" for in Javascript just goes away.
If people cared about software quality, they would use strongly typed languages before even thinking about "unit tests". What people actually want is a) to hack it out quickly and b) to have a lot of busywork to do as a form of job security. Hence 50,000 lines of "unit tests" per 10,000 lines of application...
Probably more accurate to say strong AND static typing. There are plenty of languages that are statically typed but have very weak type enforcement in the compiler and where idiomatic usage tends to use Any types and reflection and non-hygienic macros and many other type shenanigans, allowing tons of bugs to seep through. OCaml is not one of these languages.
It is good to maintain an understanding about the difference.
On the other hand, many of the most egregious problems are due to weak typing and not as static advocates, well, advocate, static vs. dynamic.
Having a really good type inference engine helps make statically typed languages much more productive - but lisps and the like have real advantages too.
This is very patronizing, you're not better than some other developer because you fell in love with OCaml or similar strongly-typed languages.
I don't think this kind of criticism is healthy, you're trying to live on top of your ivory tower calling out everyone else who isn't like you as lazy and looking for job security, you really do think there's no other reason for people using languages that you aren't so fond of?
Honest question, because this kind of message is just unnecessary aggression.
It really depends on what kind of software you build.
There are certain kinds of software for which strongly-typed languages (in the sense of ML, not C/Java) are heavily underused. For those cases, parent's rant is quite appropriate and mostly justified.
For example, we really, really want to have our browsers written in a language like Rust, rather than C or C++. Same with web servers, and public-facing web applications.
Exactly, I can agree completely with this kind of statement and I'm also one expecting to see more ML-family languages being used for what they are good at.
Only he doesn't - he just makes claims. You happen to agree so you are satisfied - but the test is, would you like that post if it stated something you don't already agree with?
But I'm certain that you have written tests to assert that an object implements a method or that a null isn't propagated in some case or another (or if you haven't written those, you likely have some bugs somewhere).
I totally agree with the sentiment up thread that it's super condescending to think you're a better programmer if you use a language with a good static type system, but from a personal perspective, it's so much more pleasant to let the compiler check this trivial stuff so that I can focus on testing actual logic.
You wouldn't write a unit test to assert an object implements a method. You would test if the method works as expected. This will of course also fail if the method is not implemented at all.
Here's a concrete example: say you are writing a test for a method that takes an object and asks that object some question, eg. `object.is_something()`. You can either explicitly assert that `object` implements `is_something` or just let it throw the normal error if it doesn't. Either way, you should test the functionality of the method when it receives an object that does not implement that method, because the behavior in that case is visible to callers, may come to be relied upon, and may cause a regression if changed.
I find that lots of tests end up being of this sort, that I'm uncomfortable if I don't write them, that they clutter up test files and obscure the more interesting tests, and that they are exactly what type checking catches automatically.
Here is a list of basic errors in Python. Only builtin ones, I'm not talking about ones from other libs. And I'm talking of error categories, as we all know one exception can be used in a lot of different contexts:
And ALL those errors can also be triggered with something a type system would no catch : runtime class generation, unexpexted user input, corrupted database, wrong data header, etc)
So basically, relying only on typing will cover about 10% of the error types, most of which are caught with linters such as flake8 and a code intelligence tools such as Jedi. You are making a very weak case.
> And ALL those errors can also be triggered with something a type system would no catch : runtime class generation, unexpexted user input, corrupted database, wrong data header, etc)
Runtime class generation and unexpected user input can actually be handled in types. Any language with Type:Type, generative modules, first-class modules, and other variations all handle various forms of runtime type generation. Something like lightweight static capabilities [1] can ensure runtime input conforms to expectations encoded in types.
Furthermore, you are way underselling types with that restricted list. NameError, SyntaxError, ImportError wouldn't occur in any typed languages at runtime. LookupError, AttributeError, AssertionError, ArithmeticError wouldn't occur in some of them.
Finally, your breakdown doesn't cover how frequent any of these errors occur. For example, even if typing were to solve only TypeError, if TypeError consists of 90% of runtime errors, that would be a huge win.
> Runtime class generation and unexpected user input can actually be handled in types. Any language with Type:Type, generative modules, first-class modules, and other variations all handle various forms of runtime type generation. Something like lightweight static capabilities [1] can ensure runtime input conforms to expectations encoded in types.
How would you if you can't deternmine type in advance, check for the type it will be ? I don't understand.
> Furthermore, you are way underselling types with that restricted list. NameError, SyntaxError, ImportError wouldn't occur in any typed languages at runtime.
Absolutly not. Those are not linked to typing in any way, and any decent Python editor catch them.
> LookupError, AttributeError, AssertionError, ArithmeticError wouldn't occur in some of them.
AssertionError ? Serioulsy ? Do you even know what it does in Python ?
And LookupError, unless you got constant sized containers, which has nothing to do with types, you can't check that.
ArithmeticError ? Come on ! Are you numbers constants ? You need to check the inputs so that they belong to the domain of your problem, at that's it. Nothing to do with types.
> Finally, your breakdown doesn't cover how frequent any of these errors occur. For example, even if typing were to solve only TypeError, if TypeError consists of 90% of runtime errors, that would be a huge win.
Yes but it's not. My last week have 90% keyerror, and empty values. The input is usually the source of errors.
Types are useful, and they come at a cost. Whether your want to use them or not is a technical choice to make. But selling types the way it's been done on this thread is dishonest. Or you are all here working with algo problems and very little user input. Which are the easy program to code. The hard part would be the algo, not the code. If you are dealing with a user app, a web site, a video game, typing will not save you from the 90% of the bugs, and you DO need unit tests.
Your view of what static type checking is or isn't seems to be limited to your understanding of types (and what can be expressed with them) in Python. There's a world of languages outside Python, even for languages with dynamic typing!
> How would you if you can't deternmine type in advance, check for the type it will be ? I don't understand.
Compilers do this all the time. Just consider all those programs that dynamically generate code based on objects that they haven't seen in advance, like object-relational mappers. Those can all be statically typed and ensure that they generate correct code [1].
> Absolutly not. Those are not linked to typing in any way, and any decent Python editor catch them.
It's all part of compilation, and furthermore, we can type check code generation so that names errors don't even occur in runtime generated code. Sorry, but all of these errors are related to type checking.
> AssertionError ? Serioulsy ? Do you even know what it does in Python ?
Just google "static contract checking" to find plenty of work, including compilers already available for Haskell. Heck, code contracts have been widely deployed on .NET for years now.
> And LookupError, unless you got constant sized containers, which has nothing to do with types, you can't check that.
Please read the lightweight static capabilities paper I already provided. It demonstrates using the Haskell and OCaml type systems to statically assure that all array bounds are in range, even for dynamically sized structures. So yes, you can check that, which doesn't even go into dependent typing where checking such dynamic properties is the whole point.
> ArithmeticError ? Come on ! Are you numbers constants ? You need to check the inputs so that they belong to the domain of your problem, at that's it. Nothing to do with types.
You don't seem to realize that types are simply logical propositions about a program. They can be ANY proposition about a program, including that indexing is within bounds, that a concurrent program has no race conditions or deadlocks [2], that programs terminate, that HTML forms correctly serialize/deserialize values from/to structures [1], and more.
Like most dynamic typing enthusiasts, you don't have a proper appreciation for the true scope of static typing. You are correct that typing has its costs that aren't always warranted, but you are incorrect about where you draw that line because I don't think you fully understand how powerful static typing truly is.
I think there's a reasonable confusion here - people always talk about "static type checking" when what they really mean is a superset that might be more clearly referred to as "static checking" or maybe even "static analysis". It's true that type systems often improve static analysis capabilities, but I don't see why some useful analyses (like catching SyntaxError and probably NameError) couldn't be done for a language like Python. After all, the editors have basically already implemented it.
"relying only on typing will cover about 10% of the error types". It's not about the percentage of error types. Its about the percentage of errors. A very high percentage of unit tests in a dynamic language are written to catch type errors. That work is a waste of time when a type system could check it for you. The reason for this is that type errors are involved in all code. A FileExistsError only applies if I am working with files. (Plus with types you get the added benefit of inline documentation and better tooling.)
> A very high percentage of unit tests in a dynamic language are written to catch type errors.
I'm not sure where you got that information. You don't actually write tests to catch types. You write tests to check behavior. While it is possible that actual type errors come out of the woodwork while doing that, that is not the reason why the test was written in the first place; and after fixing such a type error, the test itself is still valid (again because it is making sure that the code exhibits the desired behavior).
I don't think I have EVER written a test just to check for types (in Python). Neither do I pepper my code with 'assert isinstance(x, some_class)' because it just goes against the grain of a dynamically typed language.
Really? What language allows you to encode business logic into its type system?
I'm talking about things like, what happens if you press this button; is this calculation correct; does this lookup give me the desired results; does X get stored in the database correctly (or retrieved from it); etc. I don't know of any type system that lets you check these things.
(Also, I assume s/strongly/statically; Python is a strongly typed language. Granted, not everybody agrees about the terminology...)
You're moving the goalposts now; those are integration not unit tests. But the type system certainly can validate/enforce that the column you select from the DB into an integer, really is an integer in the DB too. OCaml does this with Postgres. It can also make sure the end result of a calculation is of the correct dimensions, that you're not adding cms to inches, etc etc.
A lot can be said with types beyond simply checking things like "is this a String or an Int?".
Depending on the power of your type checker, you can say (with varying degrees of convenience) things like "this integer will always be positive", "this is always going to be a non-empty list", "these two functions can be composed", "I've exhausted all possible values for this variable; I know it because the compiler told me so", "it is safe to call this function, and I know it won't touch the database or do any kind of I/O", and many more.
Whenever you called a function and it wrote to a file when you didn't want that, that's also a type error!
It's also about the compiler guaranteeing to enumerate every code path in and out of every function. A human writing tests is limited by what they think will happen, but the compiler knows.
Most of these would be caught at compile time in a language like Elm or (to a lesser extent) Haskell. A common pattern in some of these languages with actually strong type systems is to encode these failure possibility points in the type of the associated functions.
Also, in Elm as an example, there is no runtime class generation, the type of user input that is possible is checked at compile time, and you're forced to explicitly handle many of these potential failures. If you want to crash the program if you get unexpected user input, you literally need to type "Debug.crash" and then you pretty much deserve what you get.
If you're actually interested in maintainable code you should concentrate on reducing the state space of your program wherever possible, then using exhaustive (if possible) or property testing to fill in most of the rest of the gaps. Unit tests can help too, but I really think of them as less useful than all of the above.
You can not catch a keyerror at run time. You can not check for user input with a type system. Those have nothing to do with types. Input and output are out of your control, they need check sanitizing, and dynamic language are actually great at that.
because unit tests won't help you to catch these, either (because they are either thrown directly on program start, or depend of specific system circumstances rather than bugs in your code).
Finally, I would argue that most instances of this exception are caught by a type system:
| +-- KeyError
Why? Because most of the time the key is not user input, but a hard coded string, and accessing non-existing properties of an object/struct is caught by most type systems on compile time.
Well, doh :) Type system or unit tests won't catch that. Tooling, tooling, tooling.
> Finally, I would argue that most instances of this exception are caught by a type system:
>
> | +-- KeyError
No, no no !
KeyError should be super mega tested:
- keys can be any mutable in Python, not string;
- keys are very often generated on the fly, not constants;
- dict are mutable, you can add or remove keys.
Well, in many typed languages, a lookup on a map will return an option type. If the key is in the map, it returns Some(value), but if it isn't, it returns None.
By encoding the possibility of a missing key in the return type, you force the programmer to deal with it in the program.
So in fact, having a good type system can help deal with those kinds of errors as well.
> Well, doh :) Type system or unit tests won't catch that. Tooling, tooling, tooling.
You need neither unit tests or a type system to catch those. And you also don't need any tooling. The Python interpreter will throw these exceptions as soon as your program starts.
(Unless, of course, you rely on stuff like heavy dynamic importing at runtime, but that's really rare. I usually see this only for web servers in debug mode, where they auto-reload the app after a changed source file. But then my above comment applies: The faulty program crashes right away, you can't miss that.)
The bugs are way more subtle than that. You're lucky if your bad programming is caught somewhere at runtime. In most cases, Python and other weakly-typed languages will do everything they can to fit the data you're providing with what the code is expecting. This means converting strings to numbers and the other way around, plus a lot of other oddities you would not always expect (see PHP's intval for a crash course in bad language design). All that results in bugs that are far from obvious, and that don't always raise neat exceptions.
EDIT: I stand corrected, I don't know enough about Python in particular. The argument is still valid for other languages with weak types.
You clearly haven't done much Python. It doesn't do implicit convertion. Byte are separated from strings and you need to decode explicitly. You can't add '1' and 1.
Python is dynamically type. It is NOT weakly typed.
> In most cases, Python and other weakly-typed languages will do everything they can to fit the data you're providing with what the code is expecting. This means converting strings to numbers ...
Python is strongly typed, as well as dynamically typed.
In [11]: 1 + '1'
---------------------------------------------------------------------------
TypeError Traceback (most recent call last)
<ipython-input-11-861a99da769e> in <module>()
----> 1 1 + '1'
TypeError: unsupported operand type(s) for +: 'int' and 'str'
The ones you write: this month I'm working on medical data, we got tests where we scramble the database to test the reaction of our software of course. Error reporting, cleaning, logging. You mean you don't do that ?
I don't remember when was the last time I've made a type related screw up. In my experience almost all bugs in my code (and there is plenty of them of course) are either something completely stupid like a typo, or some edge-cases not covered properly by code logic (which only proper testing and QA can catch). Back in the days of working in C/C++ things were different, I'd quite often forget to do some required casting. Of course, compiler would warn me about it, but it would still kill my flow, so I honestly never regretted moving to the land of dynamically typed languages. To me it significantly reduced the number of errors since you take one layer of complexity out and thus have less of dull errors you can possibly make. Not the other way around, as you imply.
I am not saying you are wrong, and I think the OP of this thread overstated the benefits of static typing, but here are a couple of counterpoints:
> either something completely stupid like a typo...
Typos are a bigger problem in languages that silently instantiate a new variable in response. If it's on the rhs, would you prefer your IDE to warn you about using an unititialized variable, or let it go? Your coding flow is paid for by more failures (some of which will be WTFs) in testing (and testing is part of coding these days, right?) That may be the right trade-off, in which case you may be able to disable warnings in your IDE.
>...or some edge-cases not covered properly by code logic (which only proper testing and QA can catch).
The 'only' suggests that they are inevitable, and they are in a statistical sense, but whenever my code logic fails to cover an edge case, I ask myself why I overlooked it. Sometimes it is not something I could have anticipated even if I had thought more about what I was doing, but often it is.
Just as you said, statistically speaking being stupid from time to time is an inevitable part of the process :) When I manage to anticipate every angle correctly then usually there is no bugs, so it's not that relevant for this discussion. I was talking about those "other" moments, and usually, yes you are completely right, it's just me not being focused or not looking from the right angle on the problem. Thing that really sucks in programming is that computers almost never make mistakes on their own, it's always us who screw up the code :P
> I don't remember when was the last time I've made a type related screw up
Maybe the languages you are using aren't capable of encoding the kind of screw ups you do make as type errors? Typos are trivially covered by most static type checkers, but like you say that's not a very interesting kind of error (except, of course, with languages that silently introduce fresh variables. Those can introduce hard to find errors...). More interestingly, they can also handle other problems like dereferencing nulls, not considering all possible patterns of a value (think "case" conditions), calling a function on the wrong kind of value, etc.
I'd agree that Haskell/OCaml/... will get rid of some unit tests that are to be written in other languages, but you still need to test if the logic and semantics of your code are correct.
Sure, but removing entire classes of things you have to check is a really good win. Combine that with the power of fuzzers derived from legal values of the type, you can knock out a lot of bugs from your programs.
I've been using flow type which has made the transition from F# tolerable, so none of my assertions are type checks but still a lot of null checks because of no language support of option types.
In a proper type system a la Haskell, ML, Rust, etc. you can encode significantly more into the type system than you can with JavaScript's rather anemic offerings, even if it were statically typed. It feels almost like contact programming in that you must handle everything out else the code won't compile.
then you have never worked in anything but a small focussed team.
The majority of development is performed by people in corporate environments, 9->5. They simply know that they "have to" write unit tests.
No reasoning is performed, simply churn out the code to satisfy the boss and walk out at 5pm.
Sorry, you are correct; what I wrote was simply stupid. I have, in fact, encountered one method with a cyclomatic complexity of 20+, which had a single test (it verified that the result was not None).
I just tried to put that code behind me and yes, those "tests" were there because they were mandatory.
If people cared about software quality, they would use strongly typed languages before even thinking about "unit tests". What people actually want is a) to hack it out quickly and b) to have a lot of busywork to do as a form of job security. Hence 50,000 lines of "unit tests" per 10,000 lines of application...