I remember when I used to hear about so-called "functional programming" (I mean was I doing dysfunctional programming before? Weeeeell, I probably was). I would always come away with the sense that it was needlessly complex. Something only doctoral students were tortured into understanding while they were locked in the ivory tower.
However, since I mostly do functional programming today, I look back at what I could have told Past Me to make him understand that FP is not really intimidating or even reserved for people far-better educated than me. And offer some ways to approach it that setup success.
#1 Don't bother with Monad tutorials
... or other things plucked from Category Theory. You really do not have to know that stuff to start out. One day, Future You may be brimming with confidence which is ready to be destroyed by a Monad tutorial. But for today, such things are interesting "optimizations" that you can learn about later.
It might be hard to avoid getting doused in type theory in certain languages -- I know that you know that I am thinking of Haskell right now. If I were Past Me, I would stick with ML-family languages that do not have heavy type requirements: examples that come to mind are F#, OCaml/Reason, Elm. For you dynamic-type programmers in the audience, those languages also have great type inference so you can omit most type declarations but still get type checking for free.
#2 Pretend you are coming from Procedural Programming
One really common mistake when starting FP is to try to look at everything with the same pair of glasses you used for object-oriented programming. You might try to hide data structures or add methods to data. Well, that's not really the way it works most of the time. You can look at Functional Programming as being more similar to Procedural (done right) than it is to OO. The big differences being that procedural executes side effects all along, whereas FP emphasizes making decisions first and translating those decisions into side effects at the very end.
#3 Don't get obsessed with abstractions
One pervasive problem across all paradigms in the programming universe, is trying to shoehorn too many things into a Theory of Everything abstraction. FP is no different there. In fact, all the Category Theory stuff that you are so afraid of is precisely so you can use a single abstraction against otherwise very different things like a validation results and file IO -- so you can use the same functions on both. That's why I said earlier to consider all that type theory as an "optimization" that you can worry about later.
For now, be okay with observing some duplication of code (e.g. Hmm, there is List.map, Option.map, Result.map... can't quite put my finger on it, but I think some kind of pattern is happening there...) until you are pretty sure it is duplication of the exact same thing, not just something similar.
Sandi Metz said it well:
"duplication is far cheaper than the wrong abstraction"
#4 Cheat on your pure diet a little, if you must
Sometimes it can be really difficult to think about coding a problem without using side effects, because they are so ingrained in my thinking. For example, a tough one for me was generating random numbers. The languages I mentioned above have some escape hatches where you can use side effects if you just get too frustrated and need to move on for now. (Elm itself does not allow side effects, but you can still interop with the ultimate side-effect: JavaScript.)
I also allow myself to use side effects at the "edge" of a program. In a web API I define The Edge as a bad movie from 1997 the "controller". So I might generate some random numbers or get the current date/time or read from the database in the controller. But any decisions I make based on those are in pure functions. Then the decisions that were made go back to the controller, who can turn them into yet more side effects, like saving to a database or loading some more data to use in another pure function. Reminder in case you haven't seen every other post I've talked about FP: being pure makes the decision functions embarrassingly testable and refactorable. Heck I even test all the bad things that can happen as well as the happy path.
#5 Use union types
Raise your hand if you have ever tried to write a class with two nullable properties which were supposed to be mutually exclusive. Example: either some data or an error. This is precisely what union types allow you to represent in a non-awkward way. Learn about them and use them in your data structures.
Did you know that union types are representable in OO languages by using inheritance? But under the OO paradigm it is considered an anti-pattern to do "instance of" checks on an object to get at its implementation details. However in FP it is a good practice to go through all the cases of a union type and perhaps use different functions for each one. Because the emphasis in FP is on functions and immutable data, not object-based abstractions which encapsulate data and methods. Keep that in your back pocket for your journey.
I'm going to let you in on a secret. Don't let this take away from using union types, but that category theory stuff is pretty much centered around them in particular. For now, Past Me, do not taunt Happy Fun Ball always deconstruct union types with case
/ match
statements, and you will keep away from all that type theory. By the time you notice the similarity of it all, you may be ready to go deeper.
After falling in all the traps on my FP journey, hopefully I can point some out to you. ...And accurately speculate on what a better path might have looked like? Eh, accuracy is probably too much to ask. Oh well, I gave it my best go above anyway. :)
Top comments (9)
Anybody doing imperative programming can start using the functional constructs of their current language. It's the lowest learning curve to being introduced to a new paradigm.
Sure, but some of them make it really difficult to do so or don’t have features to support really common practices of the paradigm. Like in C#, I can do partial application, but it’s awkward without curried functions. Also functions can be values, but I frequently have to make long type declarations to make that happen. It’s annoying enough to be discouraging because the language doesn’t support me in it.
Why would this be a problem? You'll still be learning a functional paradigm, but comfortably in your current language. You can also use it on your current project, thus you can learn it at work. It's a great way for somebody to expose themselves to something new and spark their interest.
Partial application is easy with lambda functions. You don't need currying for that.
var
in C#,auto
in C++, and perhaps even implicit typing in Rust, also remove a lot of the need for long declarations.You definitely have a point about applying the principles to whatever language you are using. I certainly try to when I am working in our legacy systems which are in other languages. You don't have to have currying and partial application and whatnot to write pure functions and well-structured functional programs.
But at the end of the day, not having those features provides a lot of resistance to using the paradigm, and can actually be more work than using the language's default paradigm. I learned this the hard way when I tried to implement error handling in C#, like I was using in F# (which also has to handle exceptions from framework/ecosystem libraries). Some things will simply not be worth fighting the resistance (and if talking about doing it in a team using OO, won't make code review), and so you won't get to learn them.
As far as implicit typing, give it a try and see. It doesn't work well for functions (in C# anyway). Example:
var addOne = x => x + 1;
gives compile error "Cannot assign lambda expression to an implicitly typed variable." That was a great disappointment to me. I was used to being the consumer where the function was already defined likelist.Where(x => x.IsTrue)
. But when you are the one defining the functions, not just plugging them in, it gets painful quickly.I'll admit that isn't nice in C#. Maybe I was thinking more C++ where
auto
and template arguments remove a lot of the need for typing. Or in Python where the dynamic types also remove the need for typing.But those remarks are exactly why FP is so awesome!
Yeah, it's hard, but it's rather interesting and exquisite.
As FP is generally a rather niche programming paradigm ( FP languages, not FP elements like higher-order functions, currying and other examples that are commonly found nowadays in most programming languages ), I think one should strive to learn more about the delicacies of the paradigm.
It shouldn't be niche, and I think people treating it as such is part of why it's adoption is limited. The fundmanetal points of functional programming, such as immutable data, first class functions, and pure functions, are immensely useful when combined with other paradigms.
Every coder should understand the paradigm, even if they never touch one of the traditionally functional languages.
Oh I agree. I have enjoyed learning the more advanced topics. But those are often intimidating to beginners. And you really don't need to know about them to get started and get real value from the paradigm. They make for really pleasant surprises later. And FP languages make those gems more noticeable sooner. For example, I don't think I ever would have embraced functional error handling in C#, because the language makes it too much work. (And I did try.)
I agree with this. I think the advanced topics present tremendous value down the road. But are hardly of value in the beginning of someones journey. Think, explaining generics to a new C# developer. The best analogy from life I can think of is teaching how to play a card game to someone and whilst explaining the rules, you introduce strategies. The strategies are incredibly useful. But only serve to muddle the learning process. Possibly to the point of quitting.