Elixir developers typically conform to the standard of returning Tuple structures from functions which can either succeed or fail in order to effectively signal their status to the caller. Whilst this encourages the caller to directly handle the various cases which can arise, sometimes you just know that everything was fine, or you just want to set the world on fire if something goes wrong. Functions like this are known as "bang" functions and are usually appended with a ! to signal that calling them has the ability to cause a crash in the running process. Not every function requires a bang counterpart, but providing the wrappers is an unnecessary distraction for those that do.

It's for these use cases that I wrote a small utility named unsafe which aids in the creation of such functions without making the developer think too much about them. It still encourages the creation of "safe" functions, whilst providing compile-time generation of their unsafe counterparts.

Demonstration

Consider the following module. Even just looking at a small example demonstrates the amount of unnecessary code required to provide these unsafe options as part of a library API.

defmodule TestModule do

  def execute(true),
    do: { :ok, true }
  def execute(false),
    do: { :error, false }

  def execute!(bool),
    do: bool |> execute |> handle

  defp handle({ :ok, true }),
    do: true
  defp handle({ :error, false }),
    do: raise RuntimeError, message: "Uh oh!"
end

Picking apart the definition above, we can clearly see exactly which components are required inside an unsafe wrapper. Naturally all unsafe functions require a safe counterpart, a function with the same arity and root name (in this case execute/1). The match in the arity means that we need to know exactly what arguments are being expected by the safe version of the function; for example we can't call a safe function test/4 using an unsafe variant test!/0. Finally we need to know the logic required to unwrap the values coming back from the safe function; this allows us to provide error mapping, various error handling, message formatting, etc.

Example

The Unsafe API offers a simple attribute definition as a way to automatically generate these functions at compile time, rather than having to write them yourself. The result is a much smaller codebase, and less effort on behalf of the developer (woo, laziness!). To demonstrate, let's rewrite that module above using Unsafe:

defmodule TestModule do
  use Unsafe.Generator

  @unsafe [{ :execute, 1, :handle }]

  def execute(true),
    do: { :ok, true }
  def execute(false),
    do: { :error, false }

  defp handle({ :ok, true }),
    do: true
  defp handle({ :error, false }),
    do: raise RuntimeError, message: "Uh oh!"
end

This essentially creates the exact same definitions just using a simple annotation and handler definition. The observant developer will have immediately noticed that this has not saved any code; it's still the same number of lines. The real advantage is the likelihood that a developer will be using the same structures across most of their API implementation, which can easily result in a single handler being suitable for unwrapping results from a multitude of functions. This has the effect that any extra functions requiring unsafe generation (which can be handled with the same logic) simply become another entry in the @unsafe list. Likewise functions with multiple arities just become an extra definition in the @unsafe attribute, rather than adding more and more wrappers across your API.

Unsafe has several options and patterns which can all be found in the documentation. The main features include:

  • wrapping several arities in one definition
  • setting a default handler for all functions in a module
  • using either a public or private function for handling
  • generating (optional) documentation stubs for your wrappers

All of these things are included as of v1.0, and it's likely that more will be added as they become necessary and/or evident. Feel free to check it out on GitHub at whitfin/unsafe or in your projects and let me know your feedback!