Side-effects (aka I/O) can make your code hard to test, understand, and change. In web dev, you have 3 options:
- Use languages that don’t have side-effects; Elm for UI, and Roc for API/CLI.
- Abstract away the side-effects using Effect-TS.
- Use Functional Core, Imperative Shell in TypeScript, which requires you to manually separate pure code from side-effects.
The first, you don’t even think about it until you start doing integration testing. All your code is pure, and unit tests are only for business logic (decisions) that you can’t use types to enforce. The tradeoffs here is learning curve, small community, and race conditions.
The second, you can similarly ignore all side-effects. Effect gives you tools to abstract over I/O, and typed testing facilities to ensure things work, but like the first, you can just use them together, knowing the types ensure everything is composed together safely. The tradeoffs here is learning curve, small community, and race conditions (although the typed concurrency controls help mitigate a lot of common scenarios). The fact it works in just about all existing API/UI build systems and frameworks is a huge sell.
The third is hard. TypeScript does not have types for exceptions, no I/O type indicators to help you like Haskell, nor help to identify pure functions. You have to know, guess, and unit test your way to safe code, and constantly re-assess which code is pure and which code is not, bringing your own organization conventions. The tradeoffs here is easier learning curve, large community (small for Functional Programming, large TypeScript), and good tooling, but no good refactoring tooling nor good types for effects, and TypeScript’s gradual type system (feature + hinderance) & lack of runtime type enforcement.
Throughout my career I’ve enjoyed #1, focusing on delivering the edges where I/O is and thus non-predictable code. Much faster, more enjoyable, and easier to change code. It’s rare, though. While there has been a continued spread of Functional Programming ideas and features since I started in this industry, Functional Languages, especially in Web Dev & CRUD development, are rare. There is definitely a bell curve that quickly flat-lines where the closer you get to Functional Programming ideals, the less developers are willing to invest in learning and embracing the tooling. This has both changed in a positive direction over time, and differs in programming language communities.
Examples include Java 8 being OOP whereas Java 11 and later have many functional facilities. Early Go’s lack of generics and a rudimentary type system vs newer Go which supports some FP ideas. While Ruby is definitely OOP, they were one of the first next to Python to support functional Array methods like map, filter, and reduce.
The reality is most in-production code bases I’ve worked with do not separate side-effects from pure code, nor is pure code a goal. Given “the great FP wall” that only few make it through, you’d think pushing for something like Effect would be a better strategic goal. The issue there is many have no idea why pure code is great, why many side-effects, and thus mocks in your tests is bad. Whether an API that has exceptions all over the place making it hard to debug, or a UI that has random null pointers negatively affecting UX, there is a lot to teach here, and some of it can only be learned the hard way.
Part of me is glad I’ve climbed that mountain from various angles in hopes I can help teach others the benefits to make their development more enjoyable, and in turn the software we produce that much better for our users. On the flip-side, we could all save a lot more time just doing #1, and all produce much better software, faster.
Leave a Reply