Daat

hex.pm versionAPI Docslicense

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

Examples

Examples can be found in the test directory

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 we can inject mocks into the service for testing purposes.

Typically, Elixir programmers might do this in one of two ways:

Both of these approaches work, but they have some drawbacks:

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)

Acknowledgements

This library was inspired by a talk given by @expede at Code BEAM SF 2020