I agree, this kind of develops implicitly. It’s so to say the goal of static typing, at least tight coupling on the type level. When I have a record with a few fields of which two are records themselves and I have a small function working on the outer record then you can’t do anything useful with this FN when seeing it posted here as long I not also give you the declarations of the (say) three records. It’s probably fair to call this coupling. If in Clojure my little function just took a hashmap then 2-3 comments in my post about the keys that it should have will be enough. You just need my function and can play with it in the repl – the corresponding data type you already have (the hashmap) on your site. Even though in Haskell one could also just use a hashmap no Haskeller would be doing it. One would go with records instead.
There is then of course coupling in other ways. For example when you make concrete calls to specific functions instead of programming against an interface. You can do this in Clojure and in Haskell as well.
One kind of coupling is related to types and this may or may not be desireable. While the other coupling is more connected to an „unfortunate” (bad) programming style.
I don’t know how much experience you have with Haskell. If you know statically typed systems such as Java for example then you also have coupling via subclassing. The subclass is coupled to the base class, but there is no opposit relation. This can lead to challenges when trying to refactor the code.
If you have concrete examples at hand I would be happy to hear about them.
My line of thinking is specific to the Haskell type system. The one that we have available today. Not in general „statically typed languages”. And I don’t think that it is always easy peasy. But I think that it pretty much always is easier than in dynamically typed languages (and here I probably mean all of them). I would like to learn about examples/cases where it would be easy to refactor in Clojure but more complicated in Haskell.
I can’t talk this away. I still believe though that this is very rare. It could be the case that deep down in some function call stack you suddenly need IO and in principle you would have to change the type signatures of the previous eight functions in the call chain. And then their callers in other parts of the code may require other changes too. This could happen, it’s not unthinkable. The good thing though is that this can in principle be automated by tools, at least to some extent. Also you can’t accidentally forget a place. After the refactoring you can be confident that the new version will still work. Besides that such sudden needs for IO can more often occur because debugging/printing is required – and here I would be pragmatic and use tracing functions which allow me to temporary get IO without having to touch anything else.
One really important aspect for me though is to use powerful abstractions and make use of type classes. In Haskell the situation is really pretty nice. A lot of code is built around Functors, Applicatives and Monads. Those are really useful abstractions. For example, think about calling bar
on an argument in a function foo
.
(defun foo1 (x)
(bar x))
But what if you don’t know that you actually get an x?
(defun foo2 (x)
(when x
(bar x)))
What if x is a list and you want to apply bar to each element?
(defun foo3 (x)
(map bar x))
What if x is a function that only returns the value that you need to put into bar?
(defun foo4 (x)
(bar (x)))
What if x is a function that can throw an exception?
(defun foo5 (x)
(try
(let [result (x)]
(bar result))
(catch Exception e
(.getMessage e))))
What if x is a hashmap and you want to apply bar to every value and create a new hashmap where the keys are the same?
(defun foo6 (x)
(zipmap (keys x) (map bar (vals x))))
You see that we need a different implementation.
In Haskell our function would look like this:
foo = fmap bar
And this one single foo would replace all implementations above. It would also work for tons more situations.
Btw, the same can be done in Clojure if people were actually using Functors. The reality here however is that most don’t. So the interesting thing here is that the typical solution in the extremly statically typed language Haskell is shorter and more reusable than the six implementations that are required in a dyanmically typed language. And this is just Functors, which are less useful than Monads.
Thanks to static typing it’s always totally clear what will happen when you call foo.
Yes, totally agree. In JS we have a weakly typed system which can reduce boilerplate code of explicit type conversions but which can introduce subtle and hard-to-find bugs. Stronger typing (i.e. Clojure) helps here because we see errors earlier. And Clojure also went steps into the direction of purity by adopting functional data structures where mutation is explicit. Those are already powerful steps against bugs. No wonder that productivity goes up as debugging time goes down. This purity aspect motivates you to restructure your code differently, often in a cleaner way. So constraints can help to make better design descisions.
Especially when we talk about a type system such as Haskell’s, where you can express quite a bit with it. For example web pages that would in principle allow XSS attacks at runtime will be compile-time errors. Or sending emails inside a DB transaction can be an error (so customers won’t get notified that something worked and then a rollback happens, ugh…).
I hope so. Maybe if you have examples let me know. I showed you how generic and reusable Functors can be. The guys who were studying abstractions for decades in their universities have identified some pretty remarkable things. Understanding that mapping a function over a list shares aspects with running computations that can fail is a pretty powerful discovery. Now we can program against such interfaces that live on a much higher level. And when you take monads into considerations that upgrades the game again. For some years libs start using Profunctors (i.e. contravariant bifunctors) and gain some nice qualities for sql programming, etc. That’s pretty nice (imo).
I don’t understand this part. I mean even if you don’t use type hints every value in Clojure always has a type. And you certainly need to know this type to work with your data.