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.