This is only a feeling. Without carefully testing the code to exercise its cases, all you know is that they are properly typed.
For instance, suppose we write a complicated function (or group of functions) which goes through a block of intermediate code (output of a compiler) and assigns registers to all the temporaries, introducing memory spills in situations where more variables are live than the available registers.
We might have a feeling that because this code compiles, it must be free of problems such as accidentally assigning the same register to two variables which have overlapping lifetimes.
That feeling is poorly supported by reality; it is the "safyness" of static typing.
Untested code is garbage. And thorough testing is difficult to impossible, so there is a bit of garbage in almost all software, unfortunately.
Statically type checked code has all of its code paths effectively tested by the compiler, but those tests have only the limited point of view of trying to show that the program contains a trivial type mismatch, a close cousin of the syntax error. These "tests" are not actually feeding values into the code and trying to make it fail or behave incorrectly with respect to its specification.
Static typing is extraordinarily useful in large company settings, where you may own a library that is used across the company, and you can quite easily understand how your library is used in other codebases and can confidently make cross-cutting emergent patches.
I'm pretty optimistic in the direction that Clojure is heading with clojure.spec. The ideal that I hope it reaches is "inductive" type safety, where you can prove your program is typesafe with a certain probability. And the tradeoffs you get by relaxing the deductive properties and timeliness of static typing are vastly simpler expressiveness, composability, testing, etc.
The type checker just ensures that a program is well-typed (i.e. free of type errors).
The better the type system the more program properties can be encoded in it and the more errors it can catch (up to the point of proving correctness of your program).
But you are right that type checking alone is no substitute for testing. They are orthogonal concepts and both should be employed to ensure that your programs behave correctly.
One nice thing of statically typed languages is that they can automatically generate some tests for you, like e.g. the QuickCheck library for Erlang and Haskell.
That means that false positives are the problem for typing systems, while false negatives are the problem for tests. But that's the biggest difference you will find.
One does even replace the other.
Is there any studies done showing that strong typing is actually a disadvantage? I've seen some stating the opposite, but nothing really stood out to me. Sorry for the long rant, but at this point I can't find anything empirically stating one is better than the other.
Interesting experience recently: I added static checks to a Lisp dialect which report warnings for all occurrences of unbound variables and functions. (Not type checking, but a very basic static check). According to static proponents, bugs of this type should exist left and right if we don't have the check.
Only one instance of an unbound variable was found in the dialect's standard library: and that was a function that was added as an afterthought and never tested. The problem reproduced 100% when calling the function in any correct manner whatsoever; the function was completely broken. If it had been called just once after having been written, the problem would have been caught.
I fixed the problem so that the warning went away and caught myself almost starting to think about commit the fix, when I realized: what am I doing? I still haven't called the function. Gee, it now passes the one feeble static check that was added, so it must be correct? Haha.
Just a small point: type declarations are part of the actual logic of the program. In type theory, types are logical propositions and values are proofs of those propositions.
Has that become a thing now? :))
[1]: http://clojure.org/about/spec [1]: https://github.com/clojure/core.typed
The REPL-driven development of Lisps makes types not as important because everything is interactively tested as you write it. Having an optional type system makes it much easier to start with dynamic types and gradually add type annotations as your architecture stabilizes.
Dynamic typing does not imply lack of mutability. Those are unrelated concepts.
Python and Ruby are dynamically typed but both are certainly mutable.
Conversely static typing actually makes it easier to reason (locally or not) about things, since you always know the type of everything.
I don't feel static typing makes it easier to reason about, at all. Reasoning about values and their transformations is more important to me than reasoning about types, and since values carry types you're actually working with more information. Most of the time the types I'm interested in are closer to concepts (sequences, mappings) than concrete classes.
Thats where having immutable dynamically typed values is better than having statically typed mutable ones.
Only for the specific value that you are reasoning about. If you want to reason about all possible values, then you are de-facto reasoning about their types.
> Most of the time the types I'm interested in are closer to concepts (sequences, mappings) than concrete classes.
But sequences and mapping are also types, right? You can abstract types to type classes (i.e. whole classes of types, e.g. all types that can be iterated over, or all types that are ordered etc.) and also reason about them.
> Thats where having immutable dynamically typed values is better than having statically typed mutable ones.
Sure, ideally you want to have immutable statically typed functions and values, since you can then do some equational reasoning and prove certain properties of your program.
True, but a variable can only ever have one value at any given time; knowing a variable is an int is good, knowing a variable is the same int value throughout its extent is better :)
> But sequences and mapping are also types, right?
Yes, but they're not concrete types and don't map to a single interface either. When they do these interfaces are compositions of smaller interfaces anyways and I prefer to reason about these smaller parts.
Which means I'm reasoning more about the shape of the data than the actual type implementing it, and that to me is closer to a value than a type. Its the same with type-classes, you're reasoning about the features of a value rather than the specific type implementing said features.
> ideally you want to have immutable statically typed functions and values
For functions we're talking about purity rather than immutability (which qualifies variables). Type systems also are very leaky abstractions; null pointer exceptions, can't limit the range of value, requiring more code which introduces its own bugs and complexity and more.
From experience I prefer a smaller dynamically typed codebase that has tests for the trickier parts over a statically typed codebase (even if tested). The productivity difference is startling and the resulting quality is about the same.
The shape of the data is its type. You have been reasoning about types all this time. Check out https://en.wikipedia.org/wiki/Structural_type_system
In a language like Ruby or Python, it's possible for a side effect to change the type of the variable you're looking at externally. This cannot happen when you're writing idiomatic Clojure.
Static typing comes at a cost however. You have to figure out how to encode the problem using types. Effectively, you have to prove to the compiler that your code is self-consistent. Proving something is often much harder than stating it.
At the same time, static typing doesn't guarantee semantic correctness. So, it doesn't actually tell you that your code is correct in any meaningful sense.
I think that Clojure Spec packaged with the 1.9 release, provides a much better way to catch errors than types. The main reason being that it focuses on ensuring semantic correctness.
For example, consider a sort function. The types can tell me that I passed in a collection of a particular type and I got a collection of the same type back. However, what I really want to know is that the collection contains the same elements, and that they're in order. This is difficult to express using most type systems out there. However, with Spec I can just write:
(s/def ::sortable (s/coll-of number?))
(s/def ::sorted #(or (empty? %) (apply <= %)))
(s/fdef mysort
:args (s/cat :s ::sortable)
:ret ::sorted
:fn (fn [{:keys [args ret]}]
(and (= (count ret)
(-> args :s count))
(empty?
(difference
(-> args :s set)
(set ret))))))
The specification will check that the arguments follow the expected pattern, and that the result is sorted, and I can do an arbitrary runtime check using the arguments and the result. In this case it can verify that the returned items match the input.Not really. When you learn the properties of your type system, you can 'prove' your way through most day-to-day programming tasks fairly easily. And when you can't prove something to the compiler, you can just fall back to telling it using its 'dynamic' escape hatch. You're certainly no worse off than you are with pure dynamic typing.
> ... Clojure Spec ... focuses on ensuring semantic correctness.
Nothing in a static type system precludes something like Clojure Spec, or at least something very much like it. You can still fall back to runtime tests that your function works according to spec. In fact that's what Haskell's QuickCheck and related generative testing systems are all about.
I am not surprised at all Bob Martin loves it. Any principled software engineer would.
[1] https://skillsmatter.com/skillscasts/2302-radical-simplicity
The feeling you get with Clojure is that, when you write and test each function (using CIDER) and get live feedback to make sure they work, you can combine them into bigger functions that most likely also run correctly.