Daat

API Docs

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.

Installation

def deps do
  [
    {:daat, "~> 0.1.0"}
  ]
end

Example + Motivation

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 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. My end-goal is to validate that the passed modules conform to a behaviour described by the declaration, but right now I am only validating that you did in fact pass a module.

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)