The question I'm interested in finding the answer to is specifically what important things you can do in Clojure but not Haskell because of Haskell's static types.
If you want to add static typing, you're looking for a contract library. Some are runtime, some use the macro system to get compile-time checking. (In fact, core.typed looks almost exactly like Haskell type definitions.)
If you miss Haskell's pattern matching, you can find it at core.match.
You want monads instead of variadic functions? Nothing easier.
Laziness is a lambda away.
Clojure, and so many similar dynamic languages have the power to give you the safety of static typing, if and when you want it. But they don't require it of you.
In some cases this can be bad, when you don't think through your data structures enough.
In other cases, it can be good because you don't need to think as much as you put things together, and can go back and expand later.
I think every language has a time and a place, for the right purpose.
You are making a false equivalence here. This thread started with the implication that the flexibility of dynamic languages is more useful than anything static typing can give, and questioning the grounds for that is not the same as asserting the opposite.
If Haskell's type system paths the way for the programmer to implement, innovate, and create, then there is no reason for change.
The same for Clojure, or any other.
I merely answered a question on Haskell's capabilities with a personal anecdote. Haskell has been hard for the teams I worked with, replacing it with Clojure enabled others to reach their potential.
But, as for my personal belief on whether static or dynamic typing is better? Different projects have different requirements, as do the hands that craft them.
Also an interesting question, but, again, not equivalent to the one that I asked! joncampbelldev very specifically said that Clojure gives him more flexibility than Haskell. I'm asking for specific examples, otherwise how else can I improve Haskell or its documentation to be more appealing to fans of dynamic languages?
Dynamic types give their power in what they can change. I do not know how easily this can be done with Haskell, but I expect a different pattern, or a more convoluted approach would be needed, but something like this:
(defun greet
([:bob] "Hey bob")
([x] (str "Greetings, " x)))
So far, something familiar. But where's the dynamism? Let's grab a macro, so that we can do this: (addmatches! greet :chef-matches {:before :beginning}
([:emeril] "Love the zest")
([:child] "First, we baste the chicken!"))
Which we can have later use in other conditionals, as well as the corresponding macro removematches! (These macros come from a library, but are not difficult to implement).Why would one ever want such a thing? It allows for the creation of a self-modifying parser, for one. Removes a lot of boilerplate for another.
greet :: String -> String
greet "bob" = "Hey bob"
greet x = "Greeting, " ++ x
addmatches :: (String -> String) -> String -> String
addmatches f = \case
"emeril" -> "Love the zest"
"child" -> "First, we baste the chicken!"
x -> f x
If so, you'd have to design it slightly differently to do removematches in Haskell, I guess.And this:
greet :: String -> String
Wouldn't really be writable by a human, because: x -> f x
Can represent any two expressions, for example: (addmatches! greet :chef-matches {:before :beginning}
((list? x) :sym) (comment Boolean -> Symbol)
(list (apply greet x)) (comment Function -> Recursive...))
addmatches! isn't a separate function. Basically, at compile time, a macro is given the raw untyped tokens from Clojure's parser, and can run all of Clojure's features against them. The *match! macros don't exist at runtime.https://github.com/tomjaguarpaw/haskell-opaleye/blob/master/...
For someone who knows Haskell idioms this is extremely readable compared to functionally equivalant code in another language.
Welcome to the world of Haskell! Sum types aren't even available in C, C++, C#, Java, Python, Ruby, JavaScript, ..., so I think you're just proving my point for me.
https://github.com/databrary/databrary/blob/master/Databrary...
with gems like
https://github.com/databrary/databrary/blob/master/Databrary...
and
https://github.com/databrary/databrary/blob/master/Databrary...
and
https://github.com/databrary/databrary/blob/master/Databrary...
and
https://github.com/databrary/databrary/blob/master/Databrary...
That said... looking at your first example, it's really not that bad. It could probably be rewritten to be quite a bit clearer, but even knowing nothing about the library or surrounding context, it doesn't take me that much thinking to work out what's going on:
We have a dictionary implemented as a sorted list of pairs, and a list of records we want to update that dictionary with (after some transformation).
If there are no records, we give back the dictionary unmodified. If there is no dictionary, we build one out of the records. If there is both a dictionary and a bunch of records, we do a recursive merge of the two - compare the heads of each list, cons the smaller on the result of the appropriate recursive call.
That's exactly what I was asked to do!
https://hackage.haskell.org/package/turtle-1.3.6/docs/src/Tu...
However, I'm not a haskell pro, so I don't know if I'm just missing the obvious here.
One domain is deserialization of json or xml in a way that's agnostic to the structure of any extra data that I'm not explicitly referencing. For example, if I want to take a structured message (json or xml), do computation on parts of that message (so I need to decode these parts to "normal" data structures), and create a modification of the original message where my results are added but everything else is unchanged.
The last time I had to do this it was a bit painful as the serialization/deserialization libraries (I don't even recall which I used in the end. aeson? I tried a bunch.) expected me to know/define the whole structure if I wanted to properly serialize the updated data afterwards, and would break whenever the an updated api started returning a different message structure than expected (e.g. added new fields).
Perhaps I didn't find the right tools/approach to do this, but it's a rather common need and doing the same is quite trivial in python/javascript/etc.
Even quite short parts of code can have an unexpected (at least unexpected to me) explosions of thunks, and it's quite difficult (at least to me) to find out where/how strictness markers should be added so that the code wouldn't generate the totally unneeded gigabytes of temporary data that I see in the memory profiler.
It's quite easy to reason about the correctness and results of Haskell code execution, but it's hard to reason how exactly that code will be executed in ways that have an extreme effect on performance - not tenfold, but e.g. exponential relative to dataset size.
I think they are directly linked. If I have to worry about arbitrary types, my code can never be robust for the environment it runs in without raising some generic exception. This is the lazy approach. In a dynamic language, everything can be boxes to a static list of flexible types and is by default, meaning my code is succinct. This doesn't address the question (which is a strawman), but is the heart of the debate about static vs dynamic typing. Getting things done faster (with some guarantees) vs many guarantees (but not all) at a slower rate of progress (because you have to essentially duplicate code at a higher level, that the dynamic interpreter is already doing).
"The spec changed, and I now have to be able to handle geese, where I originally typed for ducks."
There are ways and tools to minimize the impact, but it's still a lot extra overhead to think about and deal with.
EDIT: Attempting to answer my own question, perhaps it occurs when you define a product type with a constructor
data Foo = Foo bar baz
and then you want to add a new field to Foo data Foo = Foo bar baz quux
All your explicit uses of Foo in construction and pattern matching have now broken.Since existential types can be monomorphised and compiled to binary code allowing type-safe plugins.
Assembly, C have tagged unions & function pointers. Java has visitors, instanceOf, reflection. C++ has visitors, boost::variant, dynamic casts, mach7. Haskell has ADTs, pattern matching, existential quantification, Dynamic. Rust has pattern matching, traits, Any. Python, javascript, lua, lisp are dynamic, it's easy enough to create both.
I also didn't yet know Dynamic, thanks. I'm not that fluent in fp.