Writing Type Specifications
Genetic Algorithms in Elixir — by Sean Moriarity (91 / 101)
👈 Cleaning Up Your Framework | TOC | What You Learned 👉
While Elixir isn’t a statically typed language, it does give you the ability to specify types using typespecs. Typespecs are specifications that communicate the intended use of a function. For example, a function add/2 that adds two numbers a and b might have the following specification:
@spec add(number, number) :: number
def add(a, b) do
a + b
endNotice the syntax used to declare a specification. You use the @spec attribute to indicate you’re defining a specification and then declare the parameter types and the return type of the function. The syntax is similar to the syntax you used in Chapter 3, Encoding Problems and Solutions, to define callbacks for your Problem behaviour and to define your Chromosome type.
Defining typespecs for your functions won’t do anything to improve the performance of your code; however, it does serve to enhance the readability of your code and can be used by dialyxir to find bugs and other problems with your code. In this section, you’ll define typespecs for the functions in the Toolbox.Crossover module and analyze them using dialyxir. You can extend this method to the other functions in your framework to identify problems and inconsistencies and improve your code base overall. You can also check out the Elixir typespec documentation[28] to learn more about typespecs and how to use them.
Crossover Typespecs
To get started, open up crossover.ex in toolbox. You’ll write typespecs for all of the functions in the Toolbox.Crossover module. Your file should contain four functions: single_point/2, uniform/3, whole_arithmetic/3, and order_one/2.
Every function takes at least two parameters which are two parent chromosomes and returns a tuple of two child chromosomes. uniform/3 and whole_arithmetic/3 take an additional parameter. uniform/3 takes a float between 0 and 1, representing the uniform crossover rate. whole_arithmetic/3 takes a float between 0 and 1, representing the percentage of genes to blend in each child chromosome.
Based on the parameters and return values of each function, implement your typespecs like this:
@spec single_point(Chromosome.t, Chromosome.t)
:: {Chromosome.t, Chromosome.t}
def single_point(p1, p2) do
...
end
@spec uniform(Chromosome.t, Chromosome.t, float)
:: {Chromosome.t, Chromosome.t}
def uniform(p1, p2, rate) do
...
end
@spec whole_arithmetic(Chromosome.t, Chromosome.t, float)
:: {Chromosome.t, Chromosome.t}
def whole_arithmetic(p1, p2, alpha) do
...
end
@spec order_one(Chromosome.t, Chromosome.t)
:: {Chromosome.t, Chromosome.t}
def order_one(p1, p2) do
...
endHere you define typespecs for every function. Notice how you can use the custom type t from the Chromosome module. Each function takes at least two parent chromosomes and they all return a tuple of child chromosomes. Your code is now a bit more expressive, and you can run it through tools like dialyxir to perform some static code analysis.
Running Dialyxir
dialyzer[29] is an Erlang static analysis tool that identifies type errors, unreachable code, software discrepancies, and more. dialyxir implements mix tasks that make using dialyzer from Elixir projects easier.
You’ll run your code module through dialyxir to identify any possible issues with your code. To start, add dialyxir to your dependencies, like this:
defp deps do
...
{:dialyxir, "~> 1.0", only: [:dev], runtime: false}
endNext, run mix deps.get. Once you have dialyxir installed, you can run it like this:
$ mix dialyzer
...
Total errors: 0, Skipped: 0, Unnecessary Skips: 0
done in 0m0.89s
done (passed successfully)dialyzer will take a long time to run. Your code should have passed without any errors. To see what it looks like when your code has errors, change the specification for single_point/2 to look like this:
@spec single_point(Chromosome.t, Chromosome.t) :: Chromosome.t
def single_point(p1, p2) do
...
endThen run dialyzer again:
$ mix dialyzer
...
Total errors: 1, Skipped: 0, Unnecessary Skips: 0
done in 0m0.95s
lib/toolbox/crossover.ex:6:invalid_contract
The @spec for the function does not match the success typing of the function.
Function:
Toolbox.Crossover.single_point/2
Success typing:
@spec single_point(atom() | %{:genes => _, _ => _},
atom() | %{:genes => _, _ => _}) ::
{%Types.Chromosome{:genes => [any()], _ => _},
%Types.Chromosome{:genes => [any()], _ => _}}This time dialyzer should run faster. Notice how it identifies the invalid type specification for you and then tells you what the correct type specification looks like. Obviously, this error was intentional, but it’s possible to make mistakes like this if your code has many different branches or if you don’t stop and check your code often.
Once you change your typespec back, dialyzer will return correctly. You can implement typespecs for all of the functions in your framework and try to run dialyzer to ensure they are correct.
👈 Cleaning Up Your Framework | TOC | What You Learned 👉
Genetic Algorithms in Elixir by Sean Moriarity can be purchased in other book formats directly from the Pragmatic Programmers. If you notice a code error or formatting mistake, please let us know here so that we can fix it.







