- what are some methods you use to maintain a clean codebase?
- how does haskell help you to maintain a clean, decoupled codebase?
- what system design benefits do you receive from a statically typed, pure language such as haskell, that you wouldn’t receive in a dynamically typed, but immutable language?
Response 1
they all seem to be the same question to me. without static types, you never see any non-trivial level of generalisation. That’s because it’s humanly intractable. Dynamically-typed languages are simply compiler outputs and should never be handwritten. Those generalisations are what it means (afaic) to keep a code base clean, maintainable, and decoupled.
Response 2
Interestingly i have the opposite pov re static typing and compiler. Statically type stuff are compiler output and should not be handwritten as they build false confidence in the safety of the program.
- Separate all the things. Sadly most of this stuff was forgotten in the 90s and 00s that were the Dark Ages of architecture and design. But ADT and contracts between your Actors are what matters
This is why OTP ask you to write a client API and an implementation of callbacks on the process side.
The main problem with people coming from Haskell thinking about decoupling is that they tend to posit that a program is a static thing. And that they tend to be stuck in the thing underpinning most of CS : lack of need of debuggability and single threading
- An erlang/elixir codebase is a living and evolving thing. It helps you keep things decoupled because by default you are pushed toward a lot of small self contain unit of logic
This is reinforced by the flat namespace. Evolving your app is easy because everything is always there. If you change something, find and replace plus a bit of copy paste is enough in 99% of the cases
- to be honest... i don't see a lot of them except for really isolated stuff. Haskell programs tend to be a mess. But a self contained small mess. By being a dynamic (but strong) language based on a process and message passing paradigm, elixir/erlang gives you the ability to separate concerns behind borders by default. Plus it gives you the ability to evolve your work. It is rare that deleting code and rewriting is a problem in erlang. As soon as you respect the contract
Keep in mind you can still pattern match on types if needed
Erlang may be dynamic but it is still strongly typed
Response 3
One thing which drew me to Elixir as a solution to the inevitable tight coupling in larger code bases is Elixir being a functional language and immutibility. So often in OO languages and/or languages with mutable data structures we pass something into a function and can't reason about what has happened to that object without tracing through the entire path, which is unwieldy, often.
when objects are immutable that problem should go away
Response 4
here are a few things that I do:
- Clear definition of boundaries. This is more art than science but step back and think about what subsystems should exist in your app. Because Elixir is really just functions inside of modules, moving things around when you figure out boundaries is simpler than many languages.
- Separate impure code from pure code (db access, calling APIs, etc) as much as possible. Pure code is more deterministic, easier to test, and easier to reason about.
- Prefer pattern matching over conditionals. Because of the way our brain works, it's usually easier to comprehend patterns over conditionals.
- Don't reach for tools until you really need them. Erlang/Elixir provides a lot of powerful features like OTP, distribution, releases, meta-programming, etc. But you usually don't need them especially when starting out. I would keep it simple to start (modules & functions) and only complicate when you have a pain point.
- Delete code ruthlessly. This is is language agnostic but when I implement a new feature, I will challenge myself to to see if the number of deletions is greater than the number of additions.
Response 5 (Haskell!)
multiple technical layers
system level tests
favor specific monads with limited effects
when they represent actual values
domain-relevant or technical abstractions at all levels
the code
the outside world, along the line of hexagonal architecture. Thanks to separation of effectful and pure functions, it is easy
to test/QC/reason about the core domain without having to deal with the impurity of realworld computations. For example, I
can let concurrency out the picture and later on use STMs (or something else) to manage concurrent access, or I can model my
system with concurrency in mind (e.g. CRDTs, event streams) and still not embed effects until actual deployment
actually code in Haskell like I would code in a dynamically typed language (e.g. use simple generic immutable structures like
list, use primitives...) but I can also go for something close to proof-system.