Learn With Jeff

I learn by teaching

Your First Hex Package

Let’s talk about publishing a hex package.

(This post assumes that you have Erlang, Elixir and hex installed. If you don’t check out Elixir’s getting started guide.)

For the last few weeks I’ve been doing a deep dive into Elixir/Erlang’s success types (I will be posting more about that soon). During that time I ran into some issues where I was confused about the type signatures I was seeing. As a Rubyist, I found myself reaching for something like this:

"foo".class #=> String

However, because of Elixir’s pattern matching, this isn’t something you should actually be doing in your code. Instead we have guard clauses.

def foo(bar) when is_atom(bar) do
  IO.puts(bar)
end

In order to do a quick inspection of the types being passed into a given fuction, I need something that looks like this:

def foo(bar) do
  Type.check(bar)
    |> IO.puts
end

I built a little module to accomplish this functionality. I thought it would make a good example for this blog post.

My goals for this package are as follows:

  • Simple API
  • Well tested
  • Well documented

For the API, let’s take the above example. I want it to look something like this:

iex> Type.check(:foo)
'Atom'
iex> Type.check(%{hello: 'world'})
'Map'

I will start by creating the package:

$mix new type
$cd type
$mix test

Let’s take a look at the files that mix has generated for us.

First we have the config/config.exs. We won’t be utilitizing this file in this exercise. This is the file you can use to add your own configurations to a package. There is further documentation here if you are interested.

Next we have the lib folder. This is where all of the code for our package will go. Since we don’t have too much functionaility in this library, we will probably only need to use the provided lib/type.ex which has already defined a module for us.

defmodule Type do
end

Then, under test/, we have two files: test/test_helper.exs and test/type_test.exs.

Finally, we have the README and the mix.exs file. The mix file is where we will list our dependencies and other package metadata.

Writing your first tests

We have a pretty good idea what we want our code to do, so let’s start by writing some basic tests.

In test/type_test.exs we can start with the most basic types:

defmodule TypeTest do
  use ExUnit.Case
  doctest Type

  test "a number" do
    assert Type.check(1) == 'Integer'
  end

  test "a boolean" do
    assert Type.check(true) == 'Boolean'
  end

  test "a float" do
    assert Type.check(1.0) == 'Float'
  end

  test "a atom" do
    assert Type.check(:hi) == 'Atom'
  end

  test "a map" do
    assert Type.check(%{:hi => 1}) == 'Map'
  end

  test "a list" do
    assert Type.check([1,2,3])  == 'List'
  end

  test "a binary" do
    assert Type.check("abc") == 'Binary'
  end

  test "a bitstring" do
    assert Type.check(<< 1 :: size(1)>>) == 'Bitstring'
  end
end

These are the ones I know off the top of my head. Looking at the available guard clauses, we have a few not covered by these tests:

is_nil(arg)
is_function(arg)
is_number(arg)
is_pid(arg)
is_port(arg)
is_reference(arg)
is_tuple(arg)

Out of these, I am least sure what is_number does. From the Elixir docs:

isnumber(term) Returns true if term is either an integer or a floating point number; otherwise returns false

It looks like we don’t need to define this as it is a super type of floats and integers. There is no situation where I would want the Type.check/1 function to return ‘Number’ instead of a ‘Float’ or ‘Integer’. I want the most specific type it can regcognize.

Let’s implement tests for the rest of them:

  test "function" do
    assert Type.check(&(&1+1)) == 'Function'
  end

  test "nil" do
    assert Type.check(nil) == 'Nil'
  end

  test "is pid" do
    pid = Process.spawn(Type, :check, ["hi"],[])
    assert Type.check(pid) == 'Pid'
  end

  test "is port" do
    port = Port.open({:spawn,'test'},[])
    assert Type.check(port) == 'Port'
  end

  test "a reference" do
    pid = Process.spawn(Type, :check, ["hi"],[])
    ref  = Process.monitor(pid)
    assert Type.check(ref) == 'Reference'
  end

  test "a tuple" do
    assert Type.check({:ok, "200"}) == 'Tuple'
  end

That should cover the rest of the types we want our function to handle.

Making the tests pass

I will start out with a function that takes any argument, and returns a character list representation of the type of the argument.

To simplify the API, I will create a single function as an entry point to the module that calls a private function. I use the typespec notation to show that this function takes any type and always returns a character list.

@spec check(any()) :: char_list
def check(arg), do: _check(arg)

I will implement the rest of the module. We can use the built in guard clauses.

 defp _check(arg) when is_map(arg), do: 'Map'
 defp _check(arg) when is_list(arg), do: 'List'
 defp _check(arg) when is_atom(arg), do: 'Atom'
 defp _check(arg) when is_binary(arg), do: 'Binary'
 defp _check(arg) when is_bitstring(arg), do: 'Bitstring'
 defp _check(arg) when is_boolean(arg), do: 'Boolean'
 defp _check(arg) when is_float(arg), do: 'Float'
 defp _check(arg) when is_function(arg), do: 'Function'
 defp _check(arg) when is_integer(arg), do: 'Integer'
 defp _check(arg) when is_number(arg), do: 'Number'
 defp _check(arg) when is_pid(arg), do: 'Pid'
 defp _check(arg) when is_port(arg), do: 'Port'
 defp _check(arg) when is_reference(arg), do: 'Reference'
 defp _check(arg) when is_tuple(arg), do: 'Tuple'
 defp _check(arg) when is_nil(arg), do: 'Nil'

Let’s run the tests.

$mix test

We see two failing tests:

  1) test a boolean (TypeTest)
 test/type_test.exs:9
 Assertion with == failed
 code: Type.check(true) == 'Boolean'
 lhs:  'Atom'
 rhs:  'Boolean'
 stacktrace:
   test/type_test.exs:10

  2) test nil (TypeTest)
 test/type_test.exs:41
 Assertion with == failed
 code: Type.check(nil) == 'Nil'
 lhs:  'Atom'
 rhs:  'Nil'
 stacktrace:
   test/type_test.exs:42

It looks like booleans and nil must be implemented under the hood as atoms. Because Elixir will match in the order the functions are defined, and because defp _check(arg) when is_atom(arg), do: 'Atom' is defined third, it is being called before the checks for booleans and nil are reached. A quick check of the documentation confirms my suspicion: “The booleans true and false are, in fact, atoms.”

So, we move the defp _check(arg) when is_atom(arg), do: 'Atom' to the bottom and rerun the tests. They all pass.

Next, we can add documentation and a doctest:

 @doc """
  Returns  a string representation of the type of a passed argument

  ## Example
  iex> Type.check(:hello_world)
  'Atom'
  """
  @spec check(any()) :: char_list
  def check(arg), do: _check(arg

In the test file we see the line:

 doctest Type

This runs our doctests for the Type module with the rest of our test suite. Let’s run the tests again to make sure the doctest passes.

$mix test

It does. Let’s publish our package.

Your next step will be to register as a hex user. Here is the example from the hex documentation:

$ mix hex.user register
Username: johndoe
Email: john.doe@example.com
Password:
Password (confirm):
Registering...
Generating API key...
You are required to confirm your email to access your account, a confirmation email has been sent to john.doe@example.com

Then, we need to add metadata to the mix.exs file. Mine looks like:

defmodule Type.Mixfile do
  use Mix.Project

  def project do
    [app: :type,
     version: "0.0.1",
     elixir: "~> 1.2",
     build_embedded: Mix.env == :prod,
     start_permanent: Mix.env == :prod,
     description: "A module for checking the type of an argument",
     package: package,
     deps: deps]
  end

  def package do
    [
      maintainers: ["Jeffrey Baird"],
      licenses: ["MIT"],
      links: %{"GitHub" => "https://github.com/jeffreybaird/type"}
    ]
  end

  # Configuration for the OTP application
  #
  # Type "mix help compile.app" for more information
  def application do
    [applications: [:logger]]
  end

  defp deps do
    []
  end
end

After that is set up, we can build and publish our app.

$mix hex.build
$mix hex.publish

We then get the message “Don't forget to upload your documentation with mix hex.docs

So we run that:

$mix hex.docs
Compiled lib/type.ex
Generated type app
** (Mix) The task "docs" could not be found. Did you mean "do"?

This isn’t a very helpful error. A quick Google search tells me that in order to generate docs I need Exdoc. So, let’s add that as a dependency.

In mix.exs we can add these:

 defp deps do
    [{:earmark, ">= 0.0.0", only: :dev},
    {:ex_doc, "~> 0.10", only: :dev}]
  end

Then we run:

$mix deps.get

And then generate the docs with:

$mix docs

And publish them:

$mix hex.docs

Conclusion

That is it. You have published your first hex package! Hopefully you enjoyed the post. If you find any errors, please let me know.