Daʻat is not always depicted in representations of the sefirot; and could be abstractly considered an "empty slot" into which the germ of any other sefirot can be placed. — Wikipedia
Daat is an experimental library meant to provide parameterized modules to Elixir.
This library is mostly untested, and should be used at your risk.
def deps do
[
{:daat, "~> 0.2.0"}
]
end
Examples can be found in the test directory
Imagine that you have a module named UserService
, that exposes a function named follow/2
. When called, the system sends an email to the user being followed. It would be nice if we could extract actually sending the email from this module, so that we aren't coupling ourselves to a specific email client, and so that we can inject mocks into the service for testing purposes.
Typically, Elixir programmers might do this in one of two ways:
- Adding a
send_email
argument to the function, which expects a callback responsible for sending the email - Fetching the implementation of
send_email
from configuration at runtime
Both of these approaches work, but they have some drawbacks:
- Adding callbacks to all of our function signatures shifts complexity to the caller, and makes for more complicated function signatures
- Storing callbacks in global configuration means losing out on the ability to run multiple instances of the module at once. This might be okay for production environments, but in testing it removes the ability to run all of your tests concurrently
- Because this dependency injection happens at runtime, we are unable to confirm, at compile-time, that the dependencies being passed to a module conform to that modue's requirements
By using parameterized, or higher-order modules, we can instead define a module that specifies an interface, and acts as a generator for modules of that interface. By then passing our dependencies to this generator, we are able to dynamically create new modules that implement our desired behaviour. This approach addresses all three points above.
That being said, this library is highly experimental, and I'm still working out the ideal interface and syntax for supportng this behaviour. If you have ideas, I'd love to hear them!
Here's an example of the above use-case:
import Daat
# UserService has one dependency: a function named `send_email/2`
defpmodule UserService, send_email: 2 do
def follow(user, follower) do
send_email().(user.email, "You have been followed by: #{follower.name}")
end
end
definst(UserService, MockUserService, send_email: fn to, body -> :ok end)
user = %{name: "Janice", email: "janice@example.com"}
follower = %{name: "Chris", email: "chris@example.com"}
MockUserService.follow(user, follower)
You're also able to specify that a dependency should be a module. If that module defines a behaviour, then the dependency will be validated as implementating that behaviour.
import Daat
defmodule Mailer do
@callback send_email(to :: String.t(), body :: String.t()) :: :ok
end
defmodule MockMailer do
@behaviour Mailer
@impl Mailer
def send_email(_to, _body) do
:ok
end
end
# UserService has one dependency: a function named `send_email/2`
defpmodule UserService, mailer: Mailer do
def follow(user, follower) do
mailer().send_email(user.email, "You have been followed by: #{follower.name}")
end
end
definst(UserService, MockUserService, mailer: MockMailer)
user = %{name: "Janice", email: "janice@example.com"}
follower = %{name: "Chris", email: "chris@example.com"}
MockUserService.follow(user, follower)
This library was inspired by a talk given by @expede at Code BEAM SF 2020