6 ms·
Haskell is nice and all, but I'm not a huge fan of this take. I can't help but think that many of the arguments boil down to something like 'you can write types
by jrh206 6y ago
Haskell is nice and all, but I'm not a huge fan of this take. I can't help but think that many of the arguments boil down to something like 'you can write types so that the compiler checks things for you' (not a quote), whilst the author disregarded the Java/C++ compiler as "an annoyance" (a quote). The rest of the article is mostly a comparison between Haskell and PHP/Python/JavaScript, and most laid out benefits boil down to static typing.
Sure, Haskell's type system is nicer, and the error messages are, I'm sure, more helpful (although the Java/C++ ones make sense when you learn what they mean).
There is an example of domain modelling in Haskell:
type Dollars = Int
data CustomerInvoice = CustomerInvoice
{ invoiceNumber :: Int
, amountDue :: Dollars
, tax :: Dollars
, billableItems :: [String]
, status :: InvoiceStatus
, createdAt :: UTCTime
, dueDate :: Day
}
data InvoiceStatus
= Issued
| Paid
| Canceled
The syntax is nice (ish, CustomerInvoice is a bit ugly), and terse. But, I've seen this a million times in Java, and that works fine.
Quote:
Modeling domain rules in the type system like this (e.g. the status of an invoice is either Issued, Paid, or Canceled) results in these rules getting enforced at compile time, as described in the earlier section on static typing. This is a much stronger set of guarantees than encoding similar rules in class methods, as one might do in an object oriented language that does not have sum types. With the type above, it becomes impossible to define CustomerInvoice that doesn’t have an amount due, for example. It’s also impossible to define a InvoiceStatus that is anything other than one of the three aforementioned values.
All of this is table stakes in Java/C++ too.
Other brief rebuttals:
Haskell has a large number of mature, high-quality libraries
No way this beats Java. I don't know the C++ ecosystem well, but I assume C++ wins too.
Haskell enables domain-specific languages, which foster expressiveness and reduce boilerplate
Be careful what you wish for.
Haskell has a large community filled with smart and friendly people
I think at the end of the day Haskell just feels fun to write, if you're the sort of person that likes it. That's fine. But I don't think going all-in on Haskell is the right call for most companies.
- willtim 6y ago> But, I've seen this a million times in Java Perhaps when Java gets record types, sealed classes, pattern matching and other features. But right now, Domain Modelling in Java (and C++) is really painful compared to a higher-level language like Haskell.
- dgb23 6y agoThe claims in the article sound weak, because they communicate in an informal and natural way. "Haskell's type system is more expressive than X and Y" is a strong claim and can be proven by showing that X and Y need to compose run-time workarounds for a given property that can be checked statically in Haskell. "Functional Programming reduces the surface area for Bugs" is a strong claim and can be proven by showing that a single mutable reference strictly introduces a set of possible bugs that were not possible before and that these bugs cannot be checked in language X. It is kind of annoying that these discussions often seem superficial, cultural or partisan, when in fact they could be much more rigorous. Now, If we assume or find these claims to be true we can finally proceed with the real discussion: What are the costs and benefits of these properties in a given setting?
- arcturus17 6y ago> "Functional Programming reduces the surface area for Bugs" is a strong claim and can be proven Is there really a formal proof or Software Engineering paper that proves this? I was told this in my FP class in university, but it pretty much sold to me as gospel. In practice I agree with the statement - I certainly feel there's an inherent "cleanliness" to FP. But I also feel that the argument is not only about program correctness; many people ultimately conflate it with developer productivity. And here's where I feel that things fall apart a bit: I feel as if sometimes it's much quicker to do things with state, so maybe the time you save debugging is time you add elsewhere?
- nix23 6y ago>Is there really a formal proof or Software Engineering paper that proves this? Yes (page 7): https://arxiv.org/pdf/1901.10220.pdf https://arxiv.org/pdf/1901.10220.pdf
- dgb23 6y ago> Is there really a formal proof or Software Engineering paper that proves this? I wonder if there is one such formal proof as well. Intuitively it is trivial: functions a are a subset of all procedures, state can introduce unique bugs, these bugs are not found in functions, so you're dealing with a subset of all possible bugs. Another intuition is this: By introducing state you increase complexity. A procedure in isolation is not necessarily referentially transparent, but a function is. You cannot reduce the procedure with it's evaluation at any given point in time, because it is 'connected' to the surrounding program via that state. Now you'd have to show that increased complexity introduces unique bugs. I'm simply not equipped (yet) to make such claims, but I'd love to hear from experts on these matters. I know that you can formally verify stateful programs, so it is likely not an issue of what is possible. But I damn sure know it is much easier to reason informally about functions than about procedures, except if the procedure merely has local state. > And here's where I feel that things fall apart a bit: I feel as if sometimes it's much quicker to do things with state, so maybe the time you save debugging is time you add elsewhere? I can only speak for myself here, but yes certain algorithms are more intuitive if implemented imperatively. But I found that the set of these algorithms shrink over time by getting used to FP. Vice versa there are also algorithms that are much more easily written with functions. Then there is core idiom of the language you're using. If it is imperative OO, then writing functional programs can sometimes feel cumbersome and less readable. There are many factors that may or may not apply as well. For example functions are easier to compose and decompose, since they are by definition simpler. However imperative procedures are sometimes easier to read "from top to bottom", because they enable a more real-world-y mechanical/visual mental model.
- chriswarbo 6y ago> the author disregarded the Java/C++ compiler as "an annoyance" (a quote). To be fair, the context of that quote is: > Many programmers encounter statically typed languages like Java or C++ and find that the compiler feels like an annoyance. I think this is a fair statement, although it would also be fair to say "many Java and C++ programmers find their compiler errors useful". I'd guess these two camps would remain mostly the same when using Haskell. You're right that most of the article is roughly comparing a good example of static types (Haskell) against a bad example of dynamic types (PHP). > I've seen this a million times in Java, and that works fine. My biggest problem with Java (and the JVM) is the existence of `null`: it completely undermines type signatures. In the above Haskell example we "know" (see caveat below) that a `myInvoice :: CustomerInvoice` is a `CustomerInvoice`, whilst in Java a `CustomerInvoice myInvoice` might be a `CustomerInvoice` or it might be `null`; likewise `myInvoice.billableItems` is a `[String]` in Haskell, whilst in Java it might be a `List<String>` or it might be `null`; in the former case, each element might be a `String` or it might be `null`. Caveat: Haskell values are lazy by default, so errors may only get triggered when inspecting some deeply nested value; in that sense we might say that a Haskell expression of type `T` might be a `T` or might be an error (known as "bottom"). We certainly need to keep that in mind, but one nice thing about bottom is that it can't affect the behaviour of a pure function (we can't branch on it). In that sense returning a value containing errors, which are later triggered, is practically equivalent to triggering the error up-front (pure expressions have no inherent notion of "time", unlike imperative sequences of instructions). The interesting difference is that we can also use such values without triggering the errors, iff the erroneous part is irrelevant to our result ;) Having all types nullable by default makes 'proper' null-checking incredibly verbose, not to mention tricky; the alternative is to cross our fingers and hope our assumptions are right. What makes this frustrating is that such checks are exactly the sort of thing that computers can help us with, and type systems are particularly well suited for! Hence the presence of `null` cripples Java's type system in a way which can't be worked around (without essentially layering a separate, null-less type system on top to check for nulls!). Also note that the presence of null causes every domain model to collapse. Let's say we want to write a conversion method, e.g. from `CustomerInvoice` to `Document`, and we don't want to worry so much about `null`: hence we write in our javadoc that as long as the given CustomerInvoice contains no null values, this method will never return null; let's say we throw a NullPointerException in those invalid cases. Great, our users now have fewer edge-cases to worry about; they don't have to check for null, and they don't have to catch NullPointerException if their input is correct. Except, once we start implementing our method we find it needs to call some other helper method, e.g. `statusToTable`; if that method returns a null result, we would be unable to construct the `Document` value that we promised. What can we do in that case? We promised we wouldn't return `null`, so maybe we throw a NullPointerException? If we do that, those calling our method might get a NullPointerException even if they gave valid input! We might throw a different exception instead, like AssertionError, but the effect would be the same. Hence we can't guarantee to our callers that we don't return null (or some equivalent that they must deal with, like NullPointerException or AssertionError); that, in turn, means they can't provide such guarantees to their callers, and so on. At any point, we might get a null (or equivalent exception), and the whole house of cards comes crashing down. Maybe we trust that helper method doesn't return null, but how can we know? Maybe we check its documentation or source code to see whether it might return null; but we find that it calls other methods, so we have to check those, and so on. If we do this, we would also have to pin our requirements to the precise versions of the libraries that we checked. In case you couldn't tell, that process is essentially manual type checking (for a very simple system with two types: 'Null' and 'AnythingElse'). Of course, this is sometimes inherent to the problem, e.g. if a HashMap doesn't contain the entry we need then there's nothing we can do. However, most code doesn't have such constraints (except perhaps out-of-memory), but there's no way to tell that to Java (in mathematical language, Java weakens every statement to admit trivial proofs).