I feel the same way going from languages with HKTs back to Java/C#...
Not sure why you think they're not as useful, it sounds like you're making the same argument as OP but just moving the bar one notch over...
I feel the same way going from languages with HKTs back to Java/C#...
Not sure why you think they're not as useful, it sounds like you're making the same argument as OP but just moving the bar one notch over...
Subjectively, I use ordinary generics all the time, but see the need for HKTs only occasionally. It's entirely possible I'm not experienced enough to see most of their possible use cases, but then I'd wager most programmers aren't.
A large portion of advanced Haskell type system features seem to be about emulating things you could do with side-effects. I guess I prefer Rust's approach to managing side-effects, or even just Scala's implied convention of: use 'var' very sparingly, and mostly locally. Yes, some guarantees get traded away, but so much simplicity is gained.
I'm not very experienced with Haskell, but I've written a fair bit of Scala and I've utterly failed to see the value in scalaz and similar libraries, despite trying them a few times. They always seem to add lots of complexity without a tangible benefit.
Coming at it from another angle, I just don't see many cases where I feel I have to repeat myself due to a shortcoming of, say, Java's or C#'s type system. If I could add one feature to either, it'd actually be support for variadic type parameters.
Both Java and C# tend to rely heavily on frameworks such as Spring to workaround issues with the expressivity of the languages. This causes problems when one needs two frameworks (they don't in general compose). In Haskell, HKTs allow one to write polymorphic programs that are parametric with respect to certain behaviours and dependencies, no dependency injection framework needed.
Please don't judge Haskell using Scala and scalaz.
I just don't understand what is so horrible and inexpressive about a static initialization block.
The only possible purpose I see to Spring is if for some reason you really need to be able to change how your dependencies are injected at runtime. (90% of Spring apologists point to this, and 99% of them never use it in practice.) Even then, I don't see how a Spring XML config file (which I have seen run to 4000+ lines, to my horror) is better than just reading some settings out of a properties file to pick an implementation in your static initializer.
Java's static initialiser blocks are too dangerous whenever one has threads.
But I think I get your general point: things like 'Control.Concurrent.Async' ('async'/'await') and 'Control.Monad.Coroutine' ('yield') are libraries that implement some and very generic type classes: 'Functor', 'Applicative', 'Monad'. This then lets you use features that are generic over those type classes ('do' syntax, 'fmap', ...).
It's been many years since I had a proper look at Haskell. Maybe it just takes more practice than I had back then to fully "get it". But I still don't see those abstractions being that useful in everyday programming. They seem to have huge potential for hard to follow code as you need to mentally unpack and remember more layers of abstraction, and the gain is not clear to me. Even the features that have trickled down to C# are not _that_ crucial I feel. The way mainstream languages pick the most useful use cases of those abstractions seems pretty OK to me.
(Also, macros and compiler plugins are another interesting avenue towards very powerful abstractions, with a different set of problems.)
As for Spring and dependency injection, I don't follow how HKTs would help there. Could you give an example? Aren't DI frameworks mostly about looking things up with reflection magic to automate, and arguably just obfuscate, the task of wiring things up in 'main'?
That's the beauty of abstraction without side effects, you don't need to unpack anything. If you know what the inputs are and the outputs are, you don't need to know how it works or what type classes are even used to transform certain things.
People use sequence
all the time in Scala, not realizing it's only able to be implemented with HKTs of Applicative and Traverse. FYI, sequence flips a list of Futures to a Future of List, or a vector of Trys to a Try of Vector, etc.
I don't buy your point about not needing to unpack side-effectless code, however. There are _always_ reasons to dig into code, be it bugs, surprising edge cases, poor documentation, insufficient performance, or even just curiosity. And those high-level abstractions tend to be visible in module interfaces too. I remember some Haskell libraries being very hard to figure out how to use if you didn't know your category theory :)
It's like spending ten years learning to speak Russian and then criticizing anyone who says that learning Russian is difficult.
Puzzling out scalaz code is difficult and requires an enormous investment in hours and practice, investment that a lot of people prefer to put into different learnings.
It's incredibly multipurpose, more so than even Spring or Guava or LINQ, and these are things that developers regularly have to invest serious time in.
The argument is just that FP libraries (like Scalaz) have a bigger payoff in the investment.
At Verizon Labs were have 20+ microservices that I have touched/looked at. Some use Akka, some use Play, some use Jetty, some use Http4s but everyone makes use of Scalaz somehow.
It depends on the people, not everybody has the inclination to dive so deep into hard core FP and they will be more productive using a different approach.
Don't make the mistake of thinking you've found the only software silver bullet that exists and that people who don't use it "don't get it", which is another attitude I've seen a lot of hardcore FP advocates embrace.
What I meant by "polymorphic programs" as an alternate to DI, is something like this:
doStuff :: HasLogger m => Input -> m Output
The effectful function "doStuff" above is polymorphic with respect to which logging implementation is used, it could even be one that uses IO. All made possible with HKTs.
Your DI example seems to be an example of my earlier point about "emulating things you could do with side-effects". No HKTs are needed when you just pass an impure side-effectful Logger object. Or, as discussed in another subthread, you could do side-effect management with Rust-style uniqueness typing, which results in a less elegant but arguably easier to use type system. It's debatable, but it seems people struggle less with the borrow checker than with advanced Haskell.
I guess the side effecting version is just to use some global registry to look up the logging implemention to use. But such code does not compose.
I agree, the side-effectful choices are either a global, some DI container, or just passing it down.
For loggers, I think global lookup from some (pluggable) logging library is justified because logging is probably the most ubiquitous cross-cutting concern ever. For pretty much everything else, I think passing as a parameter is actually the best option. It's explicit and simple, and you don't even need to explicitly pass it around _that_ much if you store it in a field of a class that plays the role of a module. Most uses of the dependency will be in non-static methods, lambdas, or inner classes.
I dislike Reader because it's similar to a DI container (or a global) in that it's more work to figure out, for a given call site, what the last value written to it was. With parameters, you just climb the call chain.
Yes, in the standard library, an immutable object can hide mutable state in e.g. a 'Mutex', and effects to the external system aren't wired through anything like monads or unique objects.
I see those as compromises Rust makes in the name of pragmatism and being a system'ey language. I don't necessarily like all of them, but I find the general uniqueness typing based approach interesting.
See articles comparing Clean and Haskell for an interesting historical perspective, including how both approaches could be used to model side-effects in a purely functional language. Haskell "won", possibly because it was seen as more generic and composable. I always felt Clean's approach had merit too, so I was really glad to see Rust bring the idea, or a closely related idea, to prominence.
Passing a unique world object around is effectively the same as composing with the IO monad, and borrowing 'f(&mut world)' is basically equivalent to 'let world = f(world)'.
Maybe someone will one day write a standard library in that style.
And I think an important take-away should be, that this perception is entirely subjective and colored by both of our experiences, preferences and the kinds of problems we work on :)