Created
November 29, 2017 19:46
-
-
Save BenMorganIO/10ca6ea8d5a747cf7b54cd2956bb08fa to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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