The app and the runtime - interpreted effects
Demonstration of managed effects in Elixir
-
Some thoughts and logic on how to do good software architecture and design
-
The major thing we're trying to do here is separate the DATA from the RUNTIME.
-
The DATA layer is a large army of functions, that take data in, and put data out.
They are the "brains" of the application, and make all the decisions of WHEN x DO y -
This layer is supported by the RUNTIME layer. This is the shell of the application
that acts as the "body", simply taking instructions from the DATA layer and executing
them in the real world. -
This is designed to be spiritually similar to these ideas:
- The Elm Architecture
- Functional Core, Imperative Shell
- Hexagonal Architecture/Ports and Adapters
-
This approach has 2 key advantages:
- Logically complex code is simple to debug and test in isolation
- Code that interacts with the real world can be debugged and tested without any
app logic being involved. This allows it to be largely abstracted away from app
developers needing to worry about the internals.
-
The question I'm at rn is, how to structure this internal core.
-
I know I want to have modules primary built around a data structure.
-
But which functions should just operate on data, vs which functions
should return more opaque data like Changesets or Multi's?-
Ideally, as little code as possible should work with changesets or multi's,
because they are very opaque data structures that are trickier to inspect
and debug than more application-level data structures. It's easier to debug
a function that changes the :name field of a profile by just returning a new
profile struct than one that returns a changeset -
However, Changeset's and Multi's are very much required to communicate to
the RUNTIME what commands/changes we would like to run
-
-
As a potential related idea to this, I want to explore the idea of like pure data structures and row-polymorphic modules. Something like the code below. This does make every module much more complicated with type definitions. But it also can make modules simpler, which could be good for easier testing
defmodule Profile do
defstruct [
:id,
:first_name,
:last_name
]
end
defmodule Matching do
# By defining a custom type, Matching is dissociated from any specific
# type of profile.
@type profile :: %{first_name: String.t, last_name: String.t}
@spec create(profile) :: :ok
def create(profile) do
MatchServer.create(profile.first_name, profile.last_name)
end
end
my_profile = %Profile{id: "asdf", first_name: "Jacob", last_name: "Bleser"}
Matching.create(my_profile)