Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save heath/858a321b5fc96d3011d9b6ea4fca3cb9 to your computer and use it in GitHub Desktop.
Save heath/858a321b5fc96d3011d9b6ea4fca3cb9 to your computer and use it in GitHub Desktop.
  1. what are some methods you use to maintain a clean codebase?
  2. how does haskell help you to maintain a clean, decoupled codebase?
  3. 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.

  1. 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

  1. 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

  1. 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.
@heath
Copy link
Author

heath commented May 24, 2017

Response 5 (Haskell!)

  1. what are some methods you use to maintain a clean codebase?
    1. basic principles always apply: maintain low-coupling & high-cohesion, separation of concerns
    2. clear separation between functional components of the system, each being vertically integrated instead of spread out over
      multiple technical layers
    3. minimal code sharing, limited to common types. use common/standard libraries wherever possible, pack common dependencies as libraries
    4. standard module structure, repeated over all components of the system to ease navigation and understanding
    5. test, test, test at all levels: unit tests (including QC tests), integration tests (e.g. at boundaries like REST APIs),
      system level tests
    6. limit size of modules, components, functions (I always use a small 13'' screen and everything should fit in it)
    7. no dependencies between components so that I can work on a single one at a time
    8. type explicitly everything
    9. abstract away actual IO as much as possible. Explicit IO type should appear as late as possible (e.g. in main function),
      favor specific monads with limited effects
    10. use actual types wherever possible to represent domain concepts, avoid primitive types and above all naked strings (or Text)
      when they represent actual values
    11. limit exposition of constructors esp. for ADTs, prefer exposing functions and "smart constructors"
  2. how does haskell help you to maintain a clean, decoupled codebase?
    1. being expressive leads to more compact code which means more information fits in a single function which helps focusing
    2. thanks to types, refactoring is very easy: change what's needed to be changed and fix compiler errors
    3. thanks to types, it is easy to generalise cross-cutting concepts that appear in different contexts
    4. compositionality of functions helps defining and using small manageable chunks of logic
    5. "higher" type concepts (typeclasses, GADTs, functors and friends, type families) increase expressivity and allows introducing
      domain-relevant or technical abstractions at all levels
    6. types are very useful to model domain-related concepts and have some constraints checked by the compiler
    7. being both compiled and interpreted decreases the feedback loop when developing/tuning code: I can use the REPL to play with
      the code
  3. 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?
    1. I can have a strong and statically enforced separation between a core domain model's functions and types and connections to
      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
    2. I can leverage the power of the compiler to chose the level of correctness I want/need, from lousy to proof-like... I can
      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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment