Sandy's advice on functional programming

I asked Sandy this question about Functional programming...

TL;DR: Do you have a recommended curriculum of things to read/watch/build that would take someone from...

I conceptually understand functors, monoids, monads, etc but

  • struggle to spot useful opportunities to apply them in the wild (ie my own code).
  • struggle to understand code that uses these abstractions
    ...to:
    I can confidently compose multiple monads in software.

...and this was their helpful response.


My gut feeling is that if you are having problems finding uses for and applying functors/monoids/monads/etc, you might not understand them --- at least, not at a sufficiently deep level. It's one thing to be able to rattle off the definition, but a very different thing to understand them.

My "curriculum" would be as follows:

  1. pick a typeclass you want to get a feel for
  2. write down the definition
  3. give as many instances for it as you possibly can. Don't look at the existing instances when you do so. The goal is to start flexing your "what types admit an instance of this class" muscles.
  4. Implement all of those instances. If you're not 100% confident that they're right, prove the respective laws for them.
  5. After you've done as many as you can, go back and check hackage to see if you got all of the instances. Are there any you missed? If so, why? Did you not remember the type, or were you incorrect about whether it admits the instance?
  6. Look at an instance you gave for a common type. Search through your real life code for examples of the method bodies, and replace them with the typeclass vocabulary. For example, if you have some code that looks like a + b + c + d consider replacing it with getSum $ a <> b <> c <> d. This will help you spot opportunities to use these patterns in the real world.
  7. After replacing the concrete definition with the typeclass vocabulary, think about how that changes the type of the function. For example, A* pathfinding is often defined in terms of numeric weights. Is that strictly necessary? What happens when you relax those constraints?

Of these, #3, #4, and #7 are IMO the most important. Once you have, by hand, written out all of the instances for all of the classes, you'll have a pretty good feeling for opportunities in which you can replace concrete definitions with more generic vocabulary. This in turn will give you a better handle on the abstraction itself, which in turn will unlock your ability to write code directly against the abstract interface.

In practice, this often looks a lot like writing out the "obvious" implementation of a function, and then rewriting it 5-10 times until it's exactly right. As an example, https://github.com/zenovy/advent_of_code_2021/pull/2 and https://github.com/zenovy/advent_of_code_2021/pull/3 show my step-by-step transformations for a friend's code. Get in the habit of doing this to your own code, and it will quickly pay dividends.