Making An Elixir Project

Now I will go over making an Elixir project. This is a continuation of my post about learning project structure and testing from the beginning when learning a new programming language.

Elixir took a bit more work. I made a project and I thought I was doing it correctly, but after a certain point every time I ran the tests it ran the app instead. I could not figure out why. So I started over. I followed along with a project in Dave Thomas’s Elixir book. He does not start a project until Chapter 13, which I think is odd. Why not start a project from the beginning?

Right now I do not know a whole lot about Elixir or the Elixir community or ecosystem, so this post might contain some opinions and speculations that will seem $INSERT_NEGATIVE_TERM to Elixir experts.

You can install Elixir with the asdf tool. It should manage dependencies for Elixir itself, but not your Elixir projects; Elixir requires another language named Erlang to be installed. Check the asdf Getting Started page to download and install it.

After you install asdf, you need to install the Erlang and Elixir plugins, and then install Erlang and Elixir themselves.


asdf
asdf plugin add erlang https://github.com/asdf-vm/asdf-erlang.git
asdf plugin-add elixir https://github.com/asdf-vm/asdf-elixir.git
asdf plugin list
asdf install erlang latest
asdf install elixir latest
asdf list
asdf list elixir
asdf list-all erlang
asdf list-all elixir

The tool to manage Elixir projects and dependecies is called Mix. To list all the commands, use “mix help”. You can find out more here and here. It is to Elixir what Maven or Gradle is to Java, or Leiningen is to Clojure. I think it is more like Gradle or Leiningen than Maven, because I think that it is easier to add functionality to Mix that it is to Maven, and it is easier to add functionality to Gradle and Leiningen than Maven. I think the Phoenix web framework adds some Mix tasks. My installation of Elixir and Mix has some Phoenix tasks built-in. I do not know if that is because whoever made the asdf package included them, or if they are part of all Elixir installations. I would be a bit surprised if the Elixir maintainers would include Phoenix and play favorites.

First make a directory for Elixir projects.


ericm@latitude:~$ mkdir elixir.projects
ericm@latitude:~$ cd elixir.projects/
ericm@latitude:~/elixir.projects$ 

Next run Mix to make a new project


ericm@latitude:~$ cd elixir.projects/
ericm@latitude:~/elixir.projects$ mix new foothold
\* creating README.md
- creating .formatter.exs
- creating .gitignore
- creating mix.exs
- creating lib
- creating lib/foothold.ex
- creating test
- creating test/test_helper.exs
- creating test/foothold_test.exs

Your Mix project was created successfully.
You can use "mix" to compile it, test it, and more:

    cd foothold
    mix test

Run "mix help" for more commands.
ericm@latitude:~/elixir.projects$ cd foothold/
ericm@latitude:~/elixir.projects/foothold$ ls
lib/  mix.exs  README.md  test/

Elixir uses modules instead of classes, and they are in namespaces. I want to make one for my project called “foothold”. I ran “mix help”, but none of the task summaries looked like what I want, so we have to go old-school and do this by hand. I am not sure if Elixir calls them “namespaces”, but that is how I think of them.


ericm@latitude:~/elixir.projects/foothold$ mkdir lib/foothold
ericm@latitude:~/elixir.projects/foothold$ mkdir test/foothold

As with our golang project, make a package (or namespace, or prefix) for some modules that we will write.


ericm@latitude:~/elixir.projects/foothold$ mkdir lib/more_strings
ericm@latitude:~/elixir.projects/foothold$ mkdir test/more_strings

We will make a couple of files that make duplicates of strings and reverse strings, and we will include some tests for them. The modules will have the “ex” extension. The tests will have the “exs” extension because they are scripts and if we compile our app the tests would not be included.

Make a file lib/more_strings/duplicate.ex:


defmodule MoreStrings.Duplicate do

  def duplicate_string(arg_string) do
    String.duplicate(arg_string, 2)
  end

  def make_three_copies(arg_string) do
    String.duplicate(arg_string, 3)
  end

end

Make a file test/more_strings/duplicate_test.exs:


defmodule MoreStrings.DuplicateTest do
  use ExUnit.Case          # bring in the test functionality
  import ExUnit.CaptureIO  # And allow us to capture stuff sent to stdout
  doctest MoreStrings.Duplicate
  alias MoreStrings.Duplicate, as: MSD

  test "try duplicate_string" do
    assert "andand" == MSD.duplicate_string( "and" )
    refute "andanda" == MSD.duplicate_string( "and" )
  end

  test "try make_three_copies" do
    IO.puts "In the test for make_three_copies"
    assert "zxcvzxcvzxcv" == MSD.make_three_copies( "zxcv" )
  end
end

Make lib/more_strings/reverse.ex:


defmodule MoreStrings.Reverse do

  def reverse_stuff do
    IO.puts "In MoreStrings.Reverse"
  end

  # why doesn't it like this?
  def actually_reverse_string(arg_string) do
    IO.puts "In MoreStrings.actually_reverse_string with arg #{arg_string}"
    IO.puts String.reverse(arg_string)
    String.reverse(arg_string)
  end

  def revv(arg_string) do
    IO.puts "In MoreStrings.Reverse.revv with arg #{arg_string}"
     IO.puts String.reverse(arg_string)
  end
end

Make test/more_strings/reverse_test.exs


defmodule MoreStrings.ReverseTest do
  use ExUnit.Case          # bring in the test functionality
  import MoreStrings.Reverse
  import ExUnit.CaptureIO  # And allow us to capture stuff sent to stdout

  # alias MoreStrings.Reverse, as: MSR
  # import MoreStrings.Reverse

  test "try reverse" do
    IO.puts "In the test try reverse"
    # assert "dolleh" == MSR.actually_reverse_string( "ahello" )
    assert MoreStrings.Reverse.actually_reverse_string("ahello") == "olleha"
    refute actually_reverse_string( "hello" ) == "dollehd"
  end

  test "ttttttt" do
    IO.puts "In test tttttt"
    assert 4 == 2 + 2
  end

end

Now compile the app with “mix compile” and run the tests with “mix test –trace”. Adding the –trace will print a message to the console for each test being run even if you do not have any IO.puts statements.


ericm@latitude:~/elixir.projects/foothold$ mix compile
Compiling 3 files (.ex)
Generated foothold app
ericm@latitude:~/elixir.projects/foothold$ mix test --trace
Compiling 3 files (.ex)
Generated foothold app
warning: unused import ExUnit.CaptureIO
  test/more_strings/reverse_test.exs:4

warning: unused import ExUnit.CaptureIO
  test/more_strings/duplicate_test.exs:3


MoreStrings.DuplicateTest [test/more_strings/duplicate_test.exs]
  * test try duplicate_string (0.02ms) [L#7]
  * test try make_three_copies [L#12]In the test for make_three_copies
  * test try make_three_copies (0.03ms) [L#12]

FootholdTest [test/foothold_test.exs]
  * doctest Foothold.hello/0 (1) (0.00ms) [L#3]
  * test greets the world (0.00ms) [L#5]
In the test try reverse

MoreStrings.ReverseTest [test/more_strings/reverse_test.exs]
In MoreStrings.actually_reverse_string with arg ahello
  * test try reverse [L#9]olleha
In MoreStrings.actually_reverse_string with arg hello
ollehericm@latitude:~/elixir.projects/foothold$ mix compile
Compiling 3 files (.ex)
Generated foothold app
ericm@latitude:~/elixir.projects/foothold$ mix test --trace
Compiling 3 files (.ex)
Generated foothold app
warning: unused import ExUnit.CaptureIO
  test/more_strings/reverse_test.exs:4

warning: unused import ExUnit.CaptureIO
  test/more_strings/duplicate_test.exs:3


MoreStrings.DuplicateTest [test/more_strings/duplicate_test.exs]
  * test try duplicate_string (0.02ms) [L#7]
  * test try make_three_copies [L#12]In the test for make_three_copies
  * test try make_three_copies (0.03ms) [L#12]

FootholdTest [test/foothold_test.exs]
  * doctest Foothold.hello/0 (1) (0.00ms) [L#3]
  * test greets the world (0.00ms) [L#5]
In the test try reverse

MoreStrings.ReverseTest [test/more_strings/reverse_test.exs]
In MoreStrings.actually_reverse_string with arg ahello
  * test try reverse [L#9]olleha
In MoreStrings.actually_reverse_string with arg hello
olleh
  * test try reverse (0.1ms) [L#9]
  * test ttttttt [L#16]In test tttttt
  * test ttttttt (0.02ms) [L#16]


Finished in 0.1 seconds (0.00s async, 0.1s sync)
1 doctest, 5 tests, 0 failures

Randomized with seed 154594

  * test try reverse (0.1ms) [L#9]
  * test ttttttt [L#16]In test tttttt
  * test ttttttt (0.02ms) [L#16]

Finished in 0.1 seconds (0.00s async, 0.1s sync)
1 doctest, 5 tests, 0 failures

Randomized with seed 154594

Run “iex -S mix” in the root of your project to use your modules. IEx is the interactive Elixir shell that comes with Elixir. You can type in Elixir code and get results. It is sort of like un-automated unit tests. You can end the session by hitting Control-C (or as we say in Emacs land: C-c) and then “a” and the return key.


ericm@latitude:~/elixir.projects/foothold$ iex -S mix
Erlang/OTP 25 [erts-13.0.2] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [jit:ns]

Interactive Elixir (1.13.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> MoreStrings.Reverse.actually_reverse_string("ahello")
In MoreStrings.actually_reverse_string with arg ahello
olleha
"olleha"
iex(2)> alias MoreStrings.Duplicate, as: MSD
MoreStrings.Duplicate
iex(3)> MSD.duplicate_string( "and" )
"andand"
iex(4)> MSD.make_three_copies( "zxcv" )
"zxcvzxcvzxcv"
iex(5)> 
BREAK: (a)bort (A)bort with dump (c)ontinue (p)roc info (i)nfo
       (l)oaded (v)ersion (k)ill (D)b-tables (d)istribution

Now add an external dependency to the project. The package we will add is Decimal, a package for arbitrary precision decimal artithmatic (Hex page here, documentation here, Github repo here). First we need to add a reference to it in our mix.exs file in the “defp deps” section:


defp deps do
  [
    {:decimal, "~> 2.0"}
    # {:dep_from_hexpm, "~> 0.3.0"},
    # {:dep_from_git, git: "https://github.com/elixir-lang/my_dep.git", tag: "0.1.0"}
  ]
end

Here are the Mix tasks associated with dependencies:


mix deps              # Lists dependencies and their status
mix deps.clean        # Deletes the given dependencies' files
mix deps.compile      # Compiles dependencies
mix deps.get          # Gets all out of date dependencies
mix deps.tree         # Prints the dependency tree
mix deps.unlock       # Unlocks the given dependencies
mix deps.update       # Updates the given dependencies

Run “mix deps.get” to fetch the dependencies and “mix deps.compile” if it makes you feel better:


ericm@latitude:~/elixir.projects/foothold$ mix deps.get
Resolving Hex dependencies...
Resolution completed in 0.033s
New:
  decimal 2.0.0
\* Getting decimal (Hex package)
ericm@latitude:~/elixir.projects/foothold$ mix deps.compile
==> decimal
Compiling 4 files (.ex)
Generated decimal app
ericm@latitude:~/elixir.projects/foothold$ mix deps
\* decimal 2.0.0 (Hex package) (mix)
  locked at 2.0.0 (decimal) 34666e9c
  ok

Add a module that depends on Decimal in lib/foothold/decimal_stuff.ex, and make a few calls so we have something to test:


defmodule Foothold.DecimalStuff do
  def do_decimal_add(a, b) do
    Decimal.add(a, b)
  end


  def do_decimal_subtract(a, b) do
    Decimal.sub(a, b)
  end

  def do_decimal_compare(a, b) do
    Decimal.compare(a, b)
  end

end

Add the following to test/foothold/decimal_test.exs


defmodule Foothold.DecimalTest do

  use ExUnit.Case
  import Foothold.DecimalStuff 
  import Decimal

  test "test do_decimal_add" do
    assert Decimal.add(2,3) == do_decimal_add( 2, 3 )
  end

  test "test do_decimal_compare_lt" do
    assert :lt == do_decimal_compare(1, 2)
  end

  test "test do_decimal_compare_gt" do
    assert :gt == do_decimal_compare( 2, 1 )
  end

  test "test do_decimal_subtract" do
    # assert 3 == do_decimal_subtract( 5, 2 )
    # assert Decimal.subtract( 5, 2 ) == do_decimal_subtract( 5, 2 )
    assert Decimal.new( 3 ) == do_decimal_subtract( 5, 2 )
  end

  #  def do_decimal_subtract(a, b) do
  #   def do_decimal_compare(a, b) do
end

Now run the tests again:


ericm@latitude:~/elixir.projects/foothold$ mix test --trace
==> decimal
Compiling 4 files (.ex)
Generated decimal app
==> foothold
Compiling 1 file (.ex)
Generated foothold app
warning: unused import Decimal
  test/foothold/decimal_test.exs:5

warning: unused import ExUnit.CaptureIO
  test/more_strings/reverse_test.exs:4

warning: unused import ExUnit.CaptureIO
  test/more_strings/duplicate_test.exs:3


FootholdTest [test/foothold_test.exs]
  * doctest Foothold.hello/0 (1) (0.00ms) [L#3]
  * test greets the world (0.00ms) [L#5]

MoreStrings.DuplicateTest [test/more_strings/duplicate_test.exs]
  * test try duplicate_string [L#7]In the test for make_three_copies
  * test try duplicate_string (0.00ms) [L#7]
  * test try make_three_copies (0.1ms) [L#12]
In the test try reverse

MoreStrings.ReverseTest [test/more_strings/reverse_test.exs]
In MoreStrings.actually_reverse_string with arg ahello
  * test try reverse [L#9]olleha
In MoreStrings.actually_reverse_string with arg hello
olleh
  * test try reverse (0.1ms) [L#9]
  * test ttttttt [L#16]In test tttttt
  * test ttttttt (0.02ms) [L#16]

Foothold.DecimalTest [test/foothold/decimal_test.exs]
  * test test do_decimal_compare_gt (0.01ms) [L#15]
  * test test do_decimal_subtract (0.01ms) [L#19]
  * test test do_decimal_add (0.01ms) [L#7]
  * test test do_decimal_compare_lt (0.00ms) [L#11]


Finished in 0.04 seconds (0.00s async, 0.04s sync)
1 doctest, 9 tests, 0 failures

Randomized with seed 333086

Next add a module to be the main module for a command line app. Put this in lib/foothold/cli.ex:


defmodule Foothold.CLI do

  import MoreStrings.Reverse
  import MoreStrings.Duplicate

  @default_count 4
  @moduledoc """
  Handle the command line parsing and the dispatch to
  the various functions 
  """
  def main(argv) do
    IO.puts "in main for Foothold"

    reverse_stuff()
    # why doesn't it like this?
    actually_reverse_string( "this is my string" )
    revv( "this is my string for revv" )
    IO.puts duplicate_string "this is a string to be duplicated"
    IO.puts make_three_copies "one copy "


    argv
    |> parse_args
    |> process
    IO.puts "Done with CLI"
  end

  @doc """
  'argv' can be -h or --help, which returns :help

  Otherwise it is a github user name, project name, and (optionally)
  the number of entries to format.

  Return a tuple '{ user, project, count }', or ':help' if help was given.
  """
  def parse_args(argv) do
    OptionParser.parse(argv, switches: [ help: :boolean],
                             aliases:  [ h:    :help   ])
    |> elem(1)
    |> args_to_internal_representation()
  end

  def args_to_internal_representation([user, project, count]) do
    { user, project, String.to_integer(count) }
  end

  def args_to_internal_representation([user, project]) do
    { user, project, @default_count }
  end

  def args_to_internal_representation(_) do # bad arg or --help
    :help
  end

  def process(:help) do
    IO.puts """
    usage:  issues <user> <project> [ count | #{@default_count} ]
    """
    System.halt(0)
  end

  def process({_user, _project, _count}) do
    IO.puts "In process"
  end

end

Next, put the following in the mix.exs file for the project:


defp escript_config do
  [
    main_module: Foothold.CLI
  ]
end

Escript is an Elixir utility that turns compiled projects into zip archives.

Then we can compile our application with “mix compile” and run it with “mix run -e ‘Foothold.CLI.main([“-h”])'”.


ericm@latitude:~/elixir.projects/foothold$ mix compile
warning: function escript_config/0 is unused
  mix.exs:30

Compiling 2 files (.ex)
Generated foothold app
ericm@latitude:~/elixir.projects/foothold$ mix run -e 'Foothold.CLI.main(["-h"])'
warning: function escript_config/0 is unused
  mix.exs:30

in main for Foothold
In MoreStrings.Reverse
In MoreStrings.actually_reverse_string with arg this is my string
gnirts ym si siht
In MoreStrings.Reverse.revv with arg this is my string for revv
vver rof gnirts ym si siht
this is a string to be duplicatedthis is a string to be duplicated
one copy one copy one copy 
usage:  issues <user> <project> [ count | 4 ]

That is the basics to get a project up and running as you learn Elixir. As I stated before, I do not like having code floating in space, or making tiny edits to small files.

I think that deploying an Elixir app to production would take more steps, and you have to know more about the Erlang VM, but that should be enough to get you started.

You’re welcome.

Image from Jruchi II Gospel, a 12-century manuscript housed at the National Research Center of Georgian Art History and Monument Protection, assumed allowed under public domain.