An year - already?
It’s been little more than an year of starting with F# and functional programming for real. I can finally say that I feel comfortable with the mental model. I’ve started thinking in functions rather than in terms of Object/Classes/Interfaces and then wondering "how do I do that in FP?". Frankly, this switch has been the hardest part of the transition.
It hasn’t been easy, nor was there any 'one moment' of enlightenment that stands out - rather, more a series of randomly interspersed 'aha' moments. At this point, I consider myself having moved from practitioner to journeyman - so lots to learn - but it’s easier. I figured it’ll be useful to jot down my journey.
First (non)encounter
My earliest FP run in was probably tweaking Emacs with emacslisp back in circa 2005. TBH, all the literature I’d read around lisp endowed it with these magical powers that, at least to me, were not apparent at all. The parens mania was slightly off-putting in the beginning, but I don’t remember thinking much about it. In any case, I wasn’t trying to learn FP either - just small Elisp functions to tweak my editor and that’s about that. So beyond dabbling a little in ELisp, FP wouldn’t actually cross my radar for quite a few years.
Object Orientation’s pit of disillusionment
After years of mostly Java and C# professionally and lots of OO and imperative languages on the side, it just felt weirdly empty. Irrespective of how well you think through systems at the beginning, it always seemed like a tightrope walk to keep it all structurally consistent as time went by and changes started rolling in. A constant, wearying war against devolving to a big ball of mud.
Sure we had unit tests and mocks and so on but reading code and making sense of abstractions after some time was just hard…Especially when the bulk of our interfaces were more so that we could mock it in a test somewhere versus actually model a concept in the domain.
Yeah - we primarily used interfaces for defining policy, stuck to SOLID, used inheritance for code reuse sparingly and all that - but over and over of creating single use interfaces for tests and mocking and so on just felt like grunt work. Especially keeping tests in line and dealing with object construction and dependency injection graphs was just painful.
I always thought that was the price of admission you had to pay for having a well structured system. To be honest, it was a case of Stockholm syndrome 😄
Mark Seemann’s talk Functional architecture - the pits of success does a great job of talking about some of these frustrations - esp the second half on Test Induced Damage.
Why is FP so hard to get into?
One of the things that’s worth pondering is why is FP so hard to pick up? I’ve tried many times(admittedly, with varying levels of seriousness) before it stuck.
In general, it just seems harder to grok. I’m pretty sure that if you take a novice and introduce them to OO and FP, the vast majority will pick up OO much more quickly. I think there’s a few things that contribute to that:
-
Foundations - OO doesn’t have a rigourous definition… FP fundamentals are grounded in logic/math and the expectation is that you will pick them up over time.
-
Terminology - Start with OO and you’re mostly dealing with procedural code stuck into a class… sure - for a beginner, some of those are weird and have no counterparts - like
namespace
,class
,static
- but still - explaining them in layman terms isn’t that hard. On the other side, look at the terminology that FP has - Lambda, Applicatives, Monads, Monoids etc… good luck explaining those in layman terms. This doesn’t mean that you have to know them to start… but there’s a good chance that it gets thrown around when you’re looking for resources and you end up trying to follow it before you actually need to. -
Gatekeeping - To be honest, to some extent, FP adoption is hurt by FP practitioners. There’s few resources that are a gentle beginning into FP without going into the math or the bringing up weird terms. Even after you pick up some of it, you’re liable to be sneered at if you’re not deep into category theory or think that Haskell’s purity matters above all else.
I think F# is wonderful in this respect. It’s multi paradigm by necessity of requiring interop with the rest of the .NET ecosystem and consciously tries to keep the language accessible (even if means not introducing more pure 'advanced' FP features)
Adding type-level programming of any kind can lead to communities where the most empowered programmers are those with deep expertise in certain kinds of highly abstract mathematics (e.g. category theory). Programmers uninterested in this kind of thing are disempowered. I don’t want F# to be the kind of language where the most empowered person in the discord chat is the category theorist.
— Don Syme
In defence of lack of Type classes
Influences
I remember coming across Gary Bernhardt’s Boundaries talk and it made a big impression. If you haven’t seen it, then do yourself a favor and listen to it - at the very least it’ll make you reconsider/think harder about how you structure systems.
Over the next few years, I tried to keep that in mind when building stuff - initially toy side projects and later on in small scale experiments at work. It sort of worked but I never got to the stage where it could become ingrained.
At work, like minded co-workers introduced functional patterns in our OO codebase.
Vladimir Khorikov’s Enterprise
Craftsmanship blog and
courses on Pluralsight
were influential. Our C# code got Result
and Option
types and we tried to
keep the core pure (so Results instead of throwing exceptions etc) and it was
all interesting but it didn’t feel natural and it’s scope was limited. We tended
towards Value types more and more and tried to design for immutability. And while
that helped us build better systems, however, it all seemed too little and too
clunky.
My exploration into FP continued on the side in fits and starts. Some helped, others didn’t. I took Programming languages online course sometime in 2018 - there’s two parts and both are very good if you’d like a gentle introduction. I highly recommend these. You’ll be using Racket - it’s a lisp/scheme descendant designed for teaching FP and the language used for the courses.
I also tried my hand at Haskell at some point but it was just too weird for me to get over the initial hump so it put me off the whole thing.
Diving in
I rewrote/ported my home automation backend in F# over a few weeks at the end of last year. A whole lot of fun and given that I was dealing with a solved problem, it let me just focus on learning language quirks, abstractions and structuring the code functionally. I’m proud to say that it still manages all my automation stuff but the code’s probably pretty amateurish.
Shortly afterwards, I was involved in prototyping a CQRS/ES system with a couple of folks as a side project. Given that it was a prototype, we chose F# with Orleans for the heavy lifting (aka distributed bits) and built a CQRS/ES framework. While it worked, it was also probably horribly un-idiomatic.
For all their warts though, these gave me a real taste of using a language that actually supported functional programming paradigm rather than something where it was bolted on. And boy, did that make a difference in driving home the benefits.
I got an offer to work on a SaaS backend in F# full time and took it. Looking back now, I think this has been a very interesting ride. It helps to list down some of what I consider as clear wins:
-
Immutability by default - we haven’t run into a in-process race condition yet. Reasoning about code is pretty much as easy as advertised.
-
Null safety - In a year of running, I could probably count the number of times a null reference exception on the fingers of one hand - and in each case, it was in one of the dark corners like reflection or interop etc.
-
Type Inference & the type system - OCaml (F#'s a descendant) like languages type inference is simply amazing. You don’t have to specify types - the platform will infer - so you get the best of both
-
Records and Structural equality - records are value types that follow structural equality. Records are what you get by default.
-
Purity & Isolation - No implicit state and core domain is just a bunch of Pure functions. What does this get us? Some piece of code not working like you thought it should? Copy the code into a REPL instance and pass in the input state and tweak it till you get it working - all without having to boot up your entire system. That’s immensely liberating.
-
No mocks - other than infrastructure bits, all unit tests use real code - not mocks. That means that we have far higher confidence in the tests than a mock based system could ever provide. Add to that, the fact that we’ve hardly ever had to refactor tests is just the cherry on top.
All of this comes at a cost though - F# isn’t exactly mainstream even within the .NET ecosystem. There’s a lots of disadvantages to choosing something that isn’t 'boring' and the trade-offs need to be weighed consciously.
At least for our case, the bet seems to have paid off quite well - we’re able to avoid entire classes of bugs. There’s also second order effects that go with the off-beat choice -
-
You have access to a smaller talent pool, but one that’s in general quite a high functioning one and generally more senior
-
You get more people who’re passionate, self starters and problem solvers - definitely the kind of folks you’d like in your corner
-
Wavelength match is quite natural in general - this leads to very high bandwidth communication that’s worth it’s weight in gold if you want to move fast.
-
There’s some risk of over-engineered solutions but that comes with the territory
Stuff that blew my mind
Thought I’d end this piece with a bunch of resources that left me murmuring appreciatively:
-
A Flock of Functions - Lambda calculus - Amazing talk on Lambda calculus - the underpinnings of functional programming. You won’t learn FP with this - but this talk is pretty mind-bendy.
-
The duality between objects and function closures - One of the earliest realizations is that an object instance translates to a closure with bound functions and vice versa… pretty simple and straightforward, but still it had that aha thing
-
The Y combinator -
In functional programming, the Y combinator can be used to formally define recursive functions in a programming language that does not support recursion.
— Wikipedia -
FSharp For Fun And Profit - Scott Wlaschin’s talks on youtube, the website and his books are all top notch. I pretty sure anyone taking up F# is going lands up on this site all the time. Most importantly, he puts a lot of thought into making sure that content is accessible without bringing up terminology that might trip up beginners.