Of course a typed language is going to have a better type system than an untyped language... When you compare to a language like Java, your argument doesn't shine.
Of course a typed language is going to have a better type system than an untyped language... When you compare to a language like Java, your argument doesn't shine.
One such example in my experience is modeling (and designing) hardware. Strict checking of lengths of bit vectors (just bit vectors!) will make mistakes like "incorrect interpretation of memory address" much harder to be made. Add another one: the "ready" bit for some bus values can be described as Maybe type. Pattern matching on value of Maybe type makes impossible to access bus values when "ready" bit is down (Nothing case). You get rid of another class of mistakes.
For this to work in more or less usable way, you have to be able to specify in type level equalities like "concatBitVectors :: (rlen ~ Add alen blen) => BitVector alen -> BitVector blen -> BitVector rlen". This is just not possible in Java. It is possible in C++, but C++ lacks algebraic types like Maybe and more. It is possible in Scala, but Scala has its own pecularities (modifiable variables are absent in hardware).
Stuff like "toString", "equals" and inequalities that you'd usually implement manually in something like Java are done for you automatically by the compiler, with a one-line directive.
That system is extensible as well, so for example you can automatically get Serde code for stuff like JSON, Avro, Protobuf etc with a one-liner.
On a more abstract level, there's a lot of stuff you can express in Haskell that's difficult or impossible on a technical level in Java. For example:
- Functions which are polymorphic in their return type, so that what they do is determined by what type the caller wants them to return.
- Function constraints - for example try to express "A function which takes two polymorphic arguments, which must both be of the same type, and which must be orderable (i.e have <, ==, >, etc defined for them), and returns the same type" in Java. In Haskell, that's just "f :: Ord a => a -> a -> a".
- Higher kinded types. These let you have (loosely speaking) polymorphic containers. For example, instead of List<A> and Set<A>, in Haskell you'd have something like Traversable t => t a, where Traversable is a particular interface you want your "container" to implement.
> - Functions which are polymorphic in their return type, so that what they do is determined by what type the caller wants them to return
You'll have to be more specific here - polymophism in the return type is clearly trivial w/o specific constraints, e.g. <T> T identity(T x) { return x; }
> - Function constraints - for example try to express "A function which takes two polymorphic arguments, which must both be of the same type, and which must be orderable (i.e have <, ==, >, etc defined for them), and returns the same type" in Java. In Haskell, that's just "f :: Ord a => a -> a -> a".
<T extends Comparable<T>> T f(T a1, T a2)
> - Higher kinded types. These let you have (loosely speaking) polymorphic containers. For example, instead of List<A> and Set<A>, in Haskell you'd have something like Traversable t => t a, where Traversable is a particular interface you want your "container" to implement.
Iterable<T>
If you're arguing that the Java version requires the types to declare that they implement Iterable, Comparable, etc, I think that's more a philosophical distinction about programmer responsibilities rather than a technical type system difference.
Haskell does have a fancier type system than Java, so you can actually present some interesting differences. However, my belief and observation is that for many smart programmers, Java's type system is already "too fancy", i.e., too hard for many people to use effectively. So I'm somewhat skeptical of the extra value brought by Haskell's type system.
Consider a function "decode :: Read a => String -> a". What this returns (and what it does) is dependent on the type that the caller expects.
> <T extends Comparable<T>> T f(T a1, T a2)
Java may have improved this somewhat since I last used it, but the general complaint from Haskellers on this is how difficult it is to say that types must be equal. Consider the fact that java `.equals` is implemented in terms of `Object`, so there's no requirement that the argument be of the same type (or even of a comparable type) to the originating object. Contrast to Haskell's `==`, which can only be called with the same type on both sides. (There is no concept of referential equality in Haskell, so no equivalent to Java's `==`).
Also, I think you'd struggle with that extends trick once you started getting more complicated constraints. e.g, try something like: "T is traversable, A is orderable and serializable to JSON, and T<A> is a monoid".
> Higher kinded types
This probably gives a better explanation than I can be bothered writing: https://stackoverflow.com/questions/35951818/why-can-the-mon...
The TLDR is that there are some concepts involving higher-kinded types (such as Monads) which are simply inexpressible in Java's type system.
A lot of this stuff is possible in other languages for varying definitions of 'possible' which usually comes down to how much code you have to write to do this, how flexible it is with code you haven't explicitly written (i.e. standard prelude types or what have you) and how much the ecosystems that exist in these languages are oriented in a manner which takes advantage of these features.
Consider nullability in Java and how pervasive it is. Java has tons of great software but the type system is lacking based on how many functions could return null but don't document it, could throw an exception but don't document it, etc... This is predominantly because actually handling nullability on the type level, or error handling on the type level is not straightforward in Java.
Java's types don't help you organize your code, don't help you make it more concise, and enable only the shallowest kind of code generalization. They are not expressive enough to prove correctness, they mostly do not help you hiding information away from the developers focus, and they mostly do not allow you to restrict code into their responsibility.
In practice, despite the features Java can support, it's typically too much work to define a new type for safety reasons. Strings are used everywhere, instead of more domain specific types. Even the designers made compareTo return an Int!
Since Haskell's type system is still more powerful than what is achievable with the .NET CLR, I'd assume this effect to be even stronger there.
It's really amazing how far sane language defaults can get you. In C#, I encounter badly designed types all the time. The main reason for this is not that it is impossible to design them right, but that the language makes it so tedious. I'm also really longing for the new features in C# 8, in the hope that they manage to remedy some of that (record types, pattern matching and non-nullable references sound like a good start).
My experience in coding functionally has shown that mutation (especially if localized and doesn't leak outside of a function) can really help functional code become a lot faster. From what you describe C# is slowly becoming an F# clone; which may be a good thing or not. Still think that if you need these features you should move to Scala/F#/Ocaml/Haskell though since I suspect these features will be added to C# in a clumsy/compromise way given its roots (e.g Scala and F# already have exhaustive pattern matching, async streams/iterators, better type inference, etc.).
100% agreed, with one caveat: You usually don't get to go "Boss, we're using F# now, 'kay?"
Some workplaces are sadly very constricted in those terms. Maybe I'll see the day when my team gets to use a more functional language, but until then, I'll take what I can get.
There's this magical thing in Haskell called the "ST Monad", where you can have a pure function (does not have IO in its type signature, does not use unsafePerformIO) that takes a normal value, returns another value, but can use mutation inside the function, and the type system guarantees that the mutation doesn't "leave" the function. So for those cases where, in C++ I would think "this function is pure enough, it doesn't print or order fish food, though it does mutate this temporary array", Haskell's compiler will actually confirm for you "yeah, you're right, it is pure enough".
The main difference I've experienced is that while java's type system forces you to be explicit about your types in a lot of places – like implementing interfaces, assigning types to variable declarations, and just in general the fact that "type inference" is limited to expressions – you get very poor checking of those types, since a lot of their complexity is hidden in the object hierarchy. This in turn, is due to java's orgins as a object oriented language relying on subtyping and inheritance for polymorphism (generics improved somewhat on this.) The result is that a lot of the complexity that haskell gets accused of, ends up in complex design patterns and/or object oriented design principles in java. This has gotten better as java has loosened some of its initial restrictions (e.g. java 8 allowing first-class functions.)
Haskell on the other hand was designed with parametric polymorphism (generics in java terms) in mind and as a functional programming language with first-class functions and no object orientation, allowing it to reap more of the benefits from the more theoretical research into type theory and category theory (yes, this gets us into monads, but I don't think they are nearly as mysterious as the internet makes them out to be.) In my opinion this has made the abstractions in haskell a lot more sturdy and more importantly statically checkable, compared to the object-oriented design principles underpinning a lot of design patterns in java. Haskell also has global type inference, which means you can avoid explicit types in many cases, especially when prototyping small, pure functions. This benefit is somewhat lessened by the fact that haskell's error messages can get very complex, but I subjectively believe that this is due to the fact the type system is checking a lot more and a lot more of what gets checked doesn't have to be given an explicit type by the programmer.
Given this I pretty clearly prefer haskell, but I've spent most of my career writing java and php. In fact, most of my criticism of java drove me to dynamic languages at first (although I would have preferred python over php.) What got me interested in haskell was the fact that I felt that there should be a way to have the freedom and speed of developing in a dynamic languages, while having the same (or better) guarantees of a statically typed language. This what led me to read about type inference.
Now, the complexeties of haskell (or even worse, haskell with ghc extensions) hardly makes my dream a reality, but I still feel like I write a lot less types while having the compiler do a lot more work for me. What has made my dream more of a reality is actually elm[0], which has similar theoretical foundations as haskell, but only has has generics (not interfaces/type-classes like java/haskell.) It's a language in which you can just write your code without types like a dynamic language, rapidly iterate on it and then add a type signature when your satisfied with it. (That's almost what I do in haskell as well, but I can run into more complex problems.)
(This characterisation is probably colored by the period I used java most heavily and might be slighty unfair, but I also limited the description of haskell to the most basic features which it has had since the 90s.)
tldr; mmm, delicious global type inference..
Haskell _allows_ you to write some true type monstrosities (cf. Lens), but almost all the useful instances of that are wrapped in libraries. Types in app code are typically very readable and expressive.
My main complaint with Haskell's type system vs Java's is actually that Haskell has too few type annotations. The inference is good enough that you usually don't need anything besides the function header, which can make it harder to read code without an IDE if you don't know what types certain functions have.
It's definitely not as nice as something like Visual Studio or IntelliJ, but it's not too bad.