I've written a couple of articles on the topic:
The Haskell community keeps marching forward, developing new libraries, tools, and techniques as well as creating new material for older concepts. The Haskell planetarium aggregates feeds from several communities into one page, as well as a Haskell Weekly newsletter. You might also find quite a bit of Haskell presence on the Fediverse!
Most imperative languages provide a step debugger. While the
exists, it is not particularly easy to use, especially because of Haskell's lazy evaluation, where things
might not be evaluated in the order we might intuitively expect. Because of that,
Haskellers tend to use
trace debugging and
equational reasoning. With trace debugging, we try to verify our assumptions about the code -
we use the various
trace functions as a "hack" to print variables, functions inputs, functions output
or even just say "got here" from anywhere in the code.
After finding something that does not match our assumptions, such as unexpected input or output
of a function, we try to think what piece of code could be responsible for the discrepancy or even use
trace debugging again to pinpoint the exact location, and try to use "equational reasoning" to
evaluate the offending code that betrayed our expectations. If it's easy to do, we try running
the function in
ghci with different inputs to check our assumptions as well.
Because Haskell focuses on immutability, composability, and using types to eliminate many classes of possible errors, "local reasoning" becomes possible, and trace debugging becomes a viable strategy for debugging Haskell programs.
GHC type errors are often not the most friendly error messages, but they mean well! They are just trying to help us find inconsistencies in our code - often with regard to type usage, they help us avoid making errors.
When you run into error messages, start by reading them carefully until you get used to them, and then the offending code hinted at by the error message. As you gain experience, it is likely that the most important part of an error will be the location of the offending code, and by reading the code, we can find the error without the actual error message.
Adding type signatures and annotations to test your understanding of the types also helps greatly. We can even ask GHC for the expected type in a certain place by using typed holes.
There could be various reasons. From inefficient algorithms or
unsuited data structures for the task
in terms of time complexity of the common operations, to less efficient memory representations
(this is another reminder to use
String in most cases),
and laziness issues (again, the evaluation strategy!).
The performance section in my Haskell study plan links to various resources on Haskell evaluation, profiling, and case studies.
Start with the imperative shell functional core approach, define EDSLs with the combinator
pattern for logic if needed, use capabilities such as
State locally if needed,
maybe add an environment configuration with
ReaderT, and see how it goes.
If that approach fails you, look at why it fails and examine other solutions according to your needs.
Modeling data using ADTs are usually the way to go. Often programmers coming from object-oriented background tend to look at type classes as a way to define methods similar to inheritance, but this often isn't the right approach, and ADTs with different constructors for different alternatives go a long way. Remember that even OOP people often preach for composition over inheritance.
Use functions to define behavior on data rather than trying to couple the two together.