Skip to content

Instantly share code, notes, and snippets.

@BenMorganIO
Created November 29, 2017 19:46
Show Gist options
  • Save BenMorganIO/10ca6ea8d5a747cf7b54cd2956bb08fa to your computer and use it in GitHub Desktop.
Save BenMorganIO/10ca6ea8d5a747cf7b54cd2956bb08fa to your computer and use it in GitHub Desktop.
# Hey everyone,
#
# So, my name's Ben and tonight, I'll be talking about Dialyzer and what you can
# do with it.
#
center <<-EOS
\e[1mDiscovering Dialyzer\e[0m
Ben A. Morgan
@BenMorganIO
ElixirTO November 2017
EOS
# So, Dialyzer is a tool from the Erlang distribution. Dialyzer hunts for
# issues in your code and then raises them for you. To start, Dialyzer stands
# for "Discrepancy Analyze for Erlang".
#
center <<-EOS
Dialyzer stands for
\e[1mDi\e[0mscrepancy \e[1mA\e[0mna\e[1mlyz\e[0me for \e[1mEr\e[0mlang
EOS
# Dialyzer is optimistic. Meaning that if it detects an issue with your code,
# there _is_ an issue with your code. If you say that your Dialyzer results are
# wrong, it isn't. Dialyzer is always right because it's optimistic. It doesn't
# pessimistically include false positives.
#
# The algorithm that Dialyzer uses is called success typing. Which basically
# means that your code is innocent until proven guilty.
#
# Success typing does this because it'll always over-approximate the valid
# inputs. As it compiles your code, it begins to recognize constraints on your
# code. And since these constraints help dictate what values can be used, it in
# turn begins to understand what the return types would be.
#
# If the constraints are unsatisfied, then you'll have a type violation returned
# back to you.
#
# This all comes back to how, if Dialyzer says there's something wrong with your
# code, there is. And it is very difficult to say that without conviction, to
# say it with a "probably incorrect" or a "most likely incorrect" since the
# algorithm is success typing.
#
block <<~EOS
Dialyzer:
- Is optimistic
- Uses "success typing" algorithm
- Determines code is innocent until proven guilty
EOS
# So, what type of discrepancies does Dialyzer catch?
#
# Dialyzer will catch standard type errors such as possibly trying to use an
# atom like a string, when errors raise in your code, when there's possibly a
# case or a cond that may need to be a bit more comprehensive, when a case or a
# cond may be too comprehensive, and finally, when you may have some issues with
# your concurrent code.
block <<~EOS
Dialyzer is a tool for:
- Type errors
- Code that raises exceptions
- Unsatisified conditions
- Redundant code
- Race conditions
EOS
# Elixir has some helpers that we should talk about before we begin.
section "Helpers in Elixir" do
# Once of them is the `i/1` function which will list module information when
# needed. This is really useful if you need to find out where a module is
# defined (see Source).
code <<~CODE
\e[1m# i/1\e[0m
iex(1)> i Enum
Term
Enum
Data type
Atom
Module bytecode
.kiex/elixirs/elixir-1.5.1/lib/elixir/bin/../lib/elixir/ebin/Elixir.Enum.beam
Source
.kiex/builds/elixir-git/lib/elixir/lib/enum.ex
Version
[146949188620703248421151154827839291711]
Compile options
[]
Description
Use h(Enum) to access its documentation.
Call Enum.module_info() to access metadata.
Raw representation
:"Elixir.Enum"
Reference modules
Module, Atom
Implemented protocols
IEx.Info, Inspect, List.Chars, String.Chars
CODE
# This next one is called `t/1` which lists the type information of a function
# which should be inherently useful given the topic of this topic. It's useful
# if you receive an error from Dialyzer that says a type doesn't exist.
# Sometimes it's a third-party library and you can use `t/1` to be able to
# list the types and see where things may have wrong.
code <<~CODE
\e[1m# t/1\e[0m
iex(2)> t Enum
@type t() :: Enumerable.t()
@type acc() :: any()
@type element() :: any()
@type index() :: integer()
@type default() :: any()
CODE
end
# Now let's get into actually using Dialyzer.
section "Diving into Dialyzer" do
# Here, you can see how one may be able to call Dialyzer. If you look at the
# two options that it lists, --build_plt and --add_to_plt, you'll notice that
# they're talking about his mysterious thing called PLT.
code <<~CODE
$ dialyzer
Checking whether the PLT /Users/ben/.dialyzer_plt is up-to-date...
dialyzer: Could not find the PLT: /Users/ben/.dialyzer_plt
Use the options:
--build_plt to build a new PLT; or
--add_to_plt to add to an existing PLT
For example, use a command like the following:
dialyzer --build_plt --apps erts kernel stdlib mnesia
Note that building a PLT such as the above may take 20 mins or so
If you later need information about other applications, say crypto,
you can extend the PLT by the command:
dialyzer --add_to_plt --apps crypto
For applications that are not in Erlang/OTP use an absolute file name.
CODE
# PLT means Persistent Lookup Table which is used for caching the output from
# modules that have already receive success typing.
center "PLT means Persistent Lookup Table"
# Since using Erlang's Dialyzer can be a bit of work and is a bit out of scope
# of this talk, we're going to talk about Dialyxir which basically compiles
# your Elixir app for you, builds your Persistent Lookup Table, and then it
# executes Dialyzer onto your elixir application.
#
code <<~CODE
def deps do
[
{:dialyxir, "~> 0.5", only: [:dev], runtime: false}
]
end
CODE
# Executing `mix dialyzer` can take some time, since building a PLT is quite
# expensive on your CPU.
#
code <<~CODE
$ mix dialyzer
CODE
# To demonstrate how Dialyzer works, I've setup a new mix application called
# "foo" where we can copy and paste some code into. And its PLT has already
# been generated, so we won't have to wait 20 minutes or even more for the
# program to compile.
#
code <<~CODE
# mix new foo
CODE
### Explain structure of Foo
end
# For the first error, we'll try and use functions in a way that they shouldn't
# use.
section "Incorrect use of built-in functions" do
# So, sometimes we use code and sometimes, under the hood, it can look like
# this. This here is a poor type conversion. We're trying to convert an atom
# to an atom with the assumption that it was a string.
#
code <<~CODE
defmodule Foo do
def hello, do: String.to_atom(:foo)
end
CODE
# We get two errors back when we run this operation. Firstly, we get told that
# there's going to be no local return. Whenever you see this error come back
# from Dialyzer, you should absolutely be concerned. Because, whenever this
# does happen, it means that there's an error that can and will happen inside
# of your codebase.
#
# "no local return" happens because something prevents your function from
# doing a return. And that is 99% of the time an error inside of your
# codebase.
#
# Now, the error just under it, which says that we broke a contract, tells us
# that we tried doing a binary to an atom, yet we provided an atom. So if you
# see code like this that says "breaks the contract" it means more or less
# that you're providing the wrong type, struct, or format that the function
# didn't expected.
#
code <<~CODE
defmodule Foo do
def hello, do: String.to_atom(:foo)
end
$ mix dialyzer
Checking PLT...
[:compiler, :elixir, :kernel, :logger, :stdlib]
PLT is up to date!
Starting Dialyzer
dialyzer args: [check_plt: false,
init_plt: 'foo/_build/dev/dialyxir_erlang-20.0_elixir-1.5.1_deps-dev.plt',
files_rec: ['foo/_build/dev/lib/foo/ebin'],
warnings: [:unknown]]
done in 0m1.33s
\e[38;5;196mlib/foo.ex:21: Function hello/0 has no local return\e[0m
\e[38;5;196mlib/foo.ex:22: The call erlang:binary_to_atom('foo','utf8') breaks the contract (Binary,Encoding) -> atom() when Binary :: binary(), Encoding :: 'latin1' | 'unicode' | 'utf8'\e[0m
\e[38;5;190mdone (warnings were emitted)\e[0m
CODE
end
# Next up in errors is generating what are called type errors which is quite
# similar to the incorrect use of built in functions.
section "Argument errors" do
# Here we have a function and we basically want to see what happens when we
# multiply fifty as a string by 10.
code <<~CODE
defmodule BadMath do
def run("50" = num) do
num * 10
end
end
CODE
# Here, in the error, we can see that the first error is no return. And to
# recap, no local return means that the code will crash. Then, we have another
# error which tells us again that there will never be a return since 1st
# argument of multiplication is not of the success type; which is a number().
code <<~CODE
defmodule BadMath do
def run("50" = num) do
num * 10
end
end
$ mix dialyzer
Compiling 1 file (.ex)
Generated foo app
Checking PLT...
[:compiler, :elixir, :kernel, :logger, :stdlib]
PLT is up to date!
Starting Dialyzer
dialyzer args: [check_plt: false,
init_plt: 'foo/_build/dev/dialyxir_erlang-20.0_elixir-1.5.1_deps-dev.plt',
files_rec: ['foo/_build/dev/lib/foo/ebin'],
warnings: [:unknown]]
done in 0m1.36s
\e[38;5;196mlib/foo.ex:2: Function run/1 has no local return\e[0m
\e[38;5;196mlib/foo.ex:3: The call erlang:'*'(num@1::<<_:16>>,10) will never return since it differs in the 1st argument from the success typing arguments: (number(),number())\e[0m
\e[38;5;190mdone (warnings were emitted)\e[0m
CODE
end
# Often times us programmers will add extra checks to our code for errors or for
# edge cases, but Dialyzer can check for redundant code and actually save you
# some time if it feels that you've already done a good enough job.
section "Redundant Code" do
# In this example, we basically want to check if an amount is greater than
# zero. If it is, then we add a case statement which will also check if that
# amount was greater than 0.
#
code <<~CODE
defmodule Bad do
def check(amount) when amount > 0, do: {:ok, amount}
def run(amount) do
case check(amount) do
amount when amount <= 0 -> {:error, "amount must be positive; 1 or higher"}
_ -> amount
end
end
end
CODE
# Dialyzer comes back with a notification that line 6 will never succeed. We
# can see that it never would since the guard on line 2 does a check for us.
code <<~CODE
defmodule Bad do
def check(amount) when amount > 0, do: {:ok, amount}
def run(amount) do
case check(amount) do
amount when amount <= 0 -> {:error, "amount must be positive; 1 or higher"}
_ -> amount
end
end
end
$ mix dialyzer
Compiling 1 file (.ex)
Checking PLT...
[:compiler, :elixir, :kernel, :logger, :stdlib]
PLT is up to date!
Starting Dialyzer
dialyzer args: [check_plt: false,
init_plt: 'foo/_build/dev/dialyxir_erlang-20.0_elixir-1.5.1_deps-dev.plt',
files_rec: ['foo/_build/dev/lib/foo/ebin'],
warnings: [:unknown]]
done in 0m1.48s
\e[38;5;196mlib/foo.ex:6: Guard test amount@2::{'ok',_} =< 0 can never succeed\e[0m
\e[38;5;190mdone (warnings were emitted)\e[0m
CODE
end
# We can also have Dialyzer check our guards as well.
section "Guard Clauses" do
# In this example, we're basically passing in an integer and making sure that
# it's a float.
code <<~CODE
defmodule BadGuard do
def run(10 = amount) when is_float(amount) do
amount * amount
end
end
CODE
# This error is really similar to the previous error where we also get told
# that a guard cannot succeed and it also explicitly tells us that there was
# a function used that didn't match up.
code <<~CODE
defmodule BadGuard do
def run(10 = amount) when is_float(amount) do
amount * amount
end
end
$ mix dialyzer
Compiling 1 file (.ex)
== Compilation error in file lib/foo.ex ==
** (CompileError) lib/foo.ex:3: undefined function multiply/1
(stdlib) erl_eval.erl:670: :erl_eval.do_apply/6
Bens-MBP:foo ben$ mix dialyzer
Compiling 1 file (.ex)
Generated foo app
Checking PLT...
[:compiler, :elixir, :kernel, :logger, :stdlib]
PLT is up to date!
Starting Dialyzer
dialyzer args: [check_plt: false,
init_plt: 'foo/_build/dev/dialyxir_erlang-20.0_elixir-1.5.1_deps-dev.plt',
files_rec: ['foo/_build/dev/lib/foo/ebin'],
warnings: [:unknown]]
done in 0m1.49s
\e[38;5;196mlib/foo.ex:2: Function run/1 has no local return\e[0m
\e[38;5;196mlib/foo.ex:2: Guard test is_float(amount@1::10) can never succeed\e[0m
\e[38;5;190mdone (warnings were emitted)\e[0m
CODE
end
# So, sometimes we can clearly see when an error is going to happen inside of
# our code. Yet, sometimes, Dialyzer doesn't catch it and marks it as a pass.
# To get around this, we use what are called specs in Elixir.
section "Using specs to help Dialyzer" do
# Here, we have code that clearly should break. We pass in "50" as a string
# and in the end, we should be able to multiply it by 10.
code <<~CODE
defmodule BadMultiply do
def multiply(amount), do: amount * 10
def get_amount({:ok, value}), do: value
def run("50" = amount) do
{:ok, amount}
|> get_amount
|> multiply
end
end
CODE
# When we run this broken code through Dialyzer, we clearly see that it's not
# catching the error. The code passed successfully.
code <<~CODE
defmodule BadMultiply do
def multiply(amount), do: amount * 10
def get_amount({:ok, value}), do: value
def run("50" = amount) do
{:ok, amount}
|> get_amount
|> multiply
end
end
$ mix dialyzer
Compiling 1 file (.ex)
Checking PLT...
[:compiler, :elixir, :kernel, :logger, :stdlib]
PLT is up to date!
Starting Dialyzer
dialyzer args: [check_plt: false,
init_plt: 'foo/_build/dev/dialyxir_erlang-20.0_elixir-1.5.1_deps-dev.plt',
files_rec: ['foo/_build/dev/lib/foo/ebin'],
warnings: [:unknown]]
done in 0m1.34s
\e[38;5;34mdone (passed successfully)\e[0m
CODE
# To solve this, we use what's called a spec. We say, the first argument must
# be an integer and the return must be an integer. We do this throughout our
# codebase not only for Dialyzer, but for the developer as well.
#
# If these fail, that means either the functionality of your application has
# changed, you've either discovered a pre-existing issue, or you've added one
# to the codebase.
code <<~CODE
defmodule BadMultiply do
@spec multiply(integer) :: integer
def multiply(amount), do: amount * 10
@spec get_amount({:ok, integer}) :: integer
def get_amount({:ok, value}), do: value
@spec run(integer) :: integer
def run("50" = amount) do
{:ok, amount}
|> get_amount
|> multiply
end
end
CODE
# And finally, we can see that with the specs, Dialyzer is now able to
# recognize the error and report to us appropriately.
code <<~CODE
defmodule BadMultiply do
@spec multiply(integer) :: integer
def multiply(amount), do: amount * 10
@spec get_amount({:ok, integer}) :: integer
def get_amount({:ok, value}), do: value
@spec run(integer) :: integer
def run("50" = amount) do
{:ok, amount}
|> get_amount
|> multiply
end
end
$ mix dialyzer
Compiling 1 file (.ex)
Generated foo app
Checking PLT...
[:compiler, :elixir, :kernel, :logger, :stdlib]
PLT is up to date!
Starting Dialyzer
dialyzer args: [check_plt: false,
init_plt: 'foo/_build/dev/dialyxir_erlang-20.0_elixir-1.5.1_deps-dev.plt',
files_rec: ['foo/_build/dev/lib/foo/ebin'],
warnings: [:unknown]]
done in 0m1.5s
\e[38;5;196mlib/foo.ex:9: Function run/1 has no local return\e[0m
\e[38;5;196mlib/foo.ex:11: The call 'Elixir.BadMultiply':get_amount({'ok',<<_:16>>}) breaks the contract ({'ok',integer()}) -> integer()\e[0m
\e[38;5;190mdone (warnings were emitted)\e[0m
CODE
end
# Now that you've seen the power of specs, we should talk about how to create
# them and use them inside of your applications.
section "How to write your own types and use them" do
# Here's a really simple User model. Here, you can add a `t` type to them and
# be able to use that to represent what you pass into the param.
code <<~CODE
defmodule App.User do
@type t :: %__MODULE__{}
@spec changeset(t, map) :: Ecto.Changeset.t
def changeset(user, attrs) do
...
end
end
CODE
# Also, in specs, passing in a module is _not_ the same as passing a struct.
# I'm mainly adding this slide because I see it happen a lot; I used to this.
# Basically, you want to represent the data that is being passed in. And
# modules are, as far as Erlang is concerned, actually atoms. They're not
# structs. So if you're using module names and not structs in your specs,
# please update this.
#
code "User != %User{}"
end
section "Review of Dialyzer" do
block "Dialyzer is a static analysis tool."
block "Dialyzer uses success typing."
block "If Dialyzer finds something, you are usually at fault."
block "Dialyzer uses optimistic type checking, so some errors can get away from you."
block "Use `@spec` to help Dialyzer and yourself."
end
section ". Fin ." do
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment