Elixir and Dialyzer, Types and Typespecs, using and understanding them

26
ELIXIR’S TYPE OF OPTIMISM HOW YOU CAN BE A TYPE OPTIMIST WITH DIALYZER AND TYPESPEC

Transcript of Elixir and Dialyzer, Types and Typespecs, using and understanding them

Page 1: Elixir and Dialyzer, Types and Typespecs, using and understanding them

ELIXIR’S TYPE OF OPTIMISM

HOW YOU CAN BE A TYPE OPTIMIST WITH DIALYZER AND TYPESPEC

Page 2: Elixir and Dialyzer, Types and Typespecs, using and understanding them

INTRODUCTION

TYPE BASICS

▸ Fundamental types: integer, atom, list, tuple, function

▸ Composite types: list of integers, tuple with varied members

▸ System of types: use of fundamental and composite types in functions, variables and structs.

Page 3: Elixir and Dialyzer, Types and Typespecs, using and understanding them

INTRODUCTION

ONE WAY TO THINK ABOUT A TYPE SYSTEM

▸ The function call structure is the wiring

▸ The types define the expected signal shapes going in and out.

A type is a pattern, it defines structure and can define legal value ranges or enumerations.

Page 4: Elixir and Dialyzer, Types and Typespecs, using and understanding them

INTRODUCTION

TYPE CHECKING

▸ Static type checking

▸ Enforced during compilation

▸ Everything has to be right, declared

▸ Dynamic type checking

▸ Defined by use

▸ Runtime evaluation of operations at the fundamental level

▸ Pattern matching

Page 5: Elixir and Dialyzer, Types and Typespecs, using and understanding them

OH BOY

HOLY WAR OF TYPE TYPES

WHICH IS BETTER?

Page 6: Elixir and Dialyzer, Types and Typespecs, using and understanding them

ELIXIR TYPESPEC

THE ELIXIR POSITION

▸ Dynamic typing

▸ Runtime enforcement of operations (“5” + 7)

▸ Pattern matching, guards on functions create some limits

▸ Testing can validate the intended use of a function

Page 7: Elixir and Dialyzer, Types and Typespecs, using and understanding them

WHAT ABOUT TESTING

▸ Can test expected and unexpected inputs

▸ Exercise the code to validate type handling

▸ Developers are optimistic testers

▸ Easy to have unexpected input conditions

▸ Doesn’t protect future code.

▸ Best at validating a function’s transformation

ELIXIR TYPESPEC

Page 8: Elixir and Dialyzer, Types and Typespecs, using and understanding them

ELIXIR TYPESPEC

THE SUPER POWER OF DIALYZER

▸ How is a function called by actual code?

▸ How does a function call other functions?

▸ Write signatures that are declarative

▸ Infer types up and down the call stack

▸ More exhaustive than what can be written as tests

▸ Your functions and structs are documented and validated

Page 9: Elixir and Dialyzer, Types and Typespecs, using and understanding them

ELIXIR TYPESPEC

DOWNSIDE

▸ Not DRY

▸ Define a struct

▸ Define a @type

▸ Write a function

▸ Write a @spec

▸ dialyzer takes a little time to run, output can be cryptic

Page 10: Elixir and Dialyzer, Types and Typespecs, using and understanding them

DIALYZER

DIALYZER READS ALL THE TYPES DECLARED

▸ Reads all the type specifications in the core modules, dependencies

▸ This takes a while, but is cached, done once.

▸ aka PLT, persistent lookup table

▸ Reads all the types you’ve declared

Page 11: Elixir and Dialyzer, Types and Typespecs, using and understanding them

LET’S DO IT

Page 12: Elixir and Dialyzer, Types and Typespecs, using and understanding them

SET UP

ADD DIALYXIR TO MIX.EXS DEPS()

defmodule Talk.Mixfile do use Mix.Project

. . .

defp deps do [ {:dialyxir, "~> 0.4", only: [:dev], runtime: false},

] end end

$ mix deps.get

$ mix deps.compile

Then

Page 13: Elixir and Dialyzer, Types and Typespecs, using and understanding them

SET UP

NEW MIX TASK

$ mix help mix # Runs the default task (current: "mix run") mix app.start # Starts all registered apps mix app.tree # Prints the application tree mix archive # Lists installed archives mix archive.build # Archives this project into a .ez file mix archive.install # Installs an archive locally mix archive.uninstall # Uninstalls archives mix clean # Deletes generated application files mix cmd # Executes the given command mix compile # Compiles source files 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

mix dialyzer # Runs dialyzer with default or project-defined flags. mix do # Executes the tasks separated by comma mix escript # Lists installed escripts mix escript.build # Builds an escript for the project mix escript.install # Installs an escript locally mix escript.uninstall # Uninstalls escripts mix help # Prints help information for tasks mix hex # Prints Hex help information mix hex.build # Builds a new package version locally mix hex.config # Reads, updates or deletes Hex config mix hex.docs # Fetch or open documentation of a package mix hex.info # Prints Hex information mix hex.key # Manages Hex API key mix hex.outdated # Shows outdated Hex deps for the current project mix hex.owner # Manages Hex package ownership mix hex.public_keys # Manages Hex public keys mix hex.publish # Publishes a new package version mix hex.retire # Retires a package version mix hex.search # Searches for package names mix hex.user # Registers or manages Hex user mix loadconfig # Loads and persists the given configuration mix local # Lists local tasks mix local.hex # Installs Hex locally mix local.phoenix # Updates Phoenix locally mix local.public_keys # Manages public keys mix local.rebar # Installs Rebar locally mix new # Creates a new Elixir project mix phoenix.new # Creates a new Phoenix v1.2.1 application mix profile.fprof # Profiles the given file or expression with fprof mix run # Runs the given file or expression mix test # Runs a project's tests mix xref # Performs cross reference checks iex -S mix # Starts IEx and runs the default task

Page 14: Elixir and Dialyzer, Types and Typespecs, using and understanding them

SET UP

FIRST RUN

flash:talk 03:32 525$ mix dialyzer Checking PLT... [:compiler, :elixir, :kernel, :logger, :stdlib] Finding suitable PLTs Looking up modules in dialyxir_erlang-19.2_elixir-1.4.1_deps-dev.plt Looking up modules in dialyxir_erlang-19.2_elixir-1.4.1.plt Looking up modules in dialyxir_erlang-19.2.plt Finding applications for dialyxir_erlang-19.2.plt Finding modules for dialyxir_erlang-19.2.plt Creating dialyxir_erlang-19.2.plt Looking up modules in dialyxir_erlang-19.2.plt Removing 3 modules from dialyxir_erlang-19.2.plt Checking 11 modules in dialyxir_erlang-19.2.plt Adding 149 modules to dialyxir_erlang-19.2.plt Finding applications for dialyxir_erlang-19.2_elixir-1.4.1.plt Finding modules for dialyxir_erlang-19.2_elixir-1.4.1.plt Copying dialyxir_erlang-19.2.plt to dialyxir_erlang-19.2_elixir-1.4.1.plt Looking up modules in dialyxir_erlang-19.2_elixir-1.4.1.plt Checking 160 modules in dialyxir_erlang-19.2_elixir-1.4.1.plt Adding 220 modules to dialyxir_erlang-19.2_elixir-1.4.1.plt Finding applications for dialyxir_erlang-19.2_elixir-1.4.1_deps-dev.plt Finding modules for dialyxir_erlang-19.2_elixir-1.4.1_deps-dev.plt Copying dialyxir_erlang-19.2_elixir-1.4.1.plt to dialyxir_erlang-19.2_elixir-1.\ 4.1_deps-dev.plt Looking up modules in dialyxir_erlang-19.2_elixir-1.4.1_deps-dev.plt Checking 380 modules in dialyxir_erlang-19.2_elixir-1.4.1_deps-dev.plt Adding 57 modules to dialyxir_erlang-19.2_elixir-1.4.1_deps-dev.plt Starting Dialyzer dialyzer --no_check_plt --fullpath --plt /Users/danj/Documents/elixir/typespec-\ talk/talk/_build/dev/dialyxir_erlang-19.2_elixir-1.4.1_deps-dev.plt /Users/danj\ /Documents/elixir/typespec-talk/talk/_build/dev/lib/talk/ebin Proceeding with analysis... done in 0m1.38s done (passed successfully)

flash:talk 03:38 526$

‣ mix dialyzer

‣ First run on an empty project ~6min

Page 15: Elixir and Dialyzer, Types and Typespecs, using and understanding them

CARDS

CARD EXAMPLEdefmodule Card do

def kind({_suit,value}) when is_number(value), do: :number

def kind({_suit,_value}), do: :face

def check_cards(card) do IO.puts("kind(#{inspect(card)}) -> #{inspect(kind(card))}") end

def main do check_cards({:spades, :king}) check_cards({:rubies, 10}) end

end

Page 16: Elixir and Dialyzer, Types and Typespecs, using and understanding them

CARDS

DIALYZER NORMAL RESULT

$ mix dialyzer Checking PLT... [:compiler, :elixir, :kernel, :logger, :stdlib] PLT is up to date! Starting Dialyzer dialyzer --no_check_plt --fullpath --plt /Users/danj/Documents/elixir/typespec-talk/talk/_build/dev/dialyxir_erlang-19.2_elixir-1.4.1_deps-dev.plt /Users/danj/Documents/elixir/typespec-talk/talk/_build/dev/lib/talk/ebin Proceeding with analysis... done in 0m1.61s done (passed successfully)

Page 17: Elixir and Dialyzer, Types and Typespecs, using and understanding them

CARDS

ADD TYPES AND A SPEC 1defmodule Card do 2

7

11 12 def check_cards(card) do 13 IO.puts("kind(#{inspect(card)}) -> #{inspect(kind(card))}") 14 end 15 16 def main do 17 check_cards({:spades, :king}) 18 check_cards({:rubies, 10}) 19 end 20end

3 @type suit :: ( :spade | :heart | :club | :diamond ) 4 @type value :: ( 2..10 | :jack | :queen | :king | :ace ) 5 @type card :: { suit, value } 6 @type card_kind :: { :number | :face }

8 @spec kind(card()) :: card_kind() 9 def kind({_suit,value}) when is_number(value), do: :number 10 def kind({_suit,_value}), do: :face

Page 18: Elixir and Dialyzer, Types and Typespecs, using and understanding them

CARDS

Proceeding with analysis... lib/card.ex:8: Invalid type specification for function 'Elixir.Card':kind/1. The success typing is ({_,_}) -> 'face' | 'number' done in 0m1.72s done (warnings were emitted)

8 @spec kind(card()) :: card_kind()

5 @type card :: { suit, value }

6 @type card_kind :: { :number | :face }

6 @type card_kind :: ( :number | :face )

oops

Page 19: Elixir and Dialyzer, Types and Typespecs, using and understanding them

CARDS

Proceeding with analysis... lib/card.ex:16: Function main/0 has no local return lib/card.ex:17: The call 'Elixir.Card':check_cards({'spades','king'}) will never return since it differs in the 1st argument from the success typing arguments: ({'club','ace' | 'jack' | 'king' | 'queen' | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10} | {'diamond','ace' | 'jack' | 'king' | 'queen' | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10} | {'heart','ace' | 'jack' | 'king' | 'queen' | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10} | {'spade','ace' | 'jack' | 'king' | 'queen' | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10}) lib/card.ex:18: The call 'Elixir.Card':check_cards({'rubies',10}) will never return since it differs in the 1st argument from the success typing arguments: ({'club','ace' | 'jack' | 'king' | 'queen' | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10} | {'diamond','ace' | 'jack' | 'king' | 'queen' | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10} | {'heart','ace' | 'jack' | 'king' | 'queen' | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10} | {'spade','ace' | 'jack' | 'king' | 'queen' | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10}) done in 0m1.75s done (warnings were emitted)

Page 20: Elixir and Dialyzer, Types and Typespecs, using and understanding them

CARDS

17 check_cards({:spades, :king})

lib/card.ex:17: The call 'Elixir.Card':check_cards({'spades','king'})

will never return since it differs in the 1st argument from the success typing arguments:

({'club','ace' | 'jack' | 'king' | 'queen' | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10} | {'diamond','ace' | 'jack' | 'king' | 'queen' | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10} | {'heart','ace' | 'jack' | 'king' | 'queen' | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10} | {'spade','ace' | 'jack' | 'king' | 'queen' | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10} )

Page 21: Elixir and Dialyzer, Types and Typespecs, using and understanding them

CARDS

1defmodule Card do 2 3 @type suit :: ( :spade | :heart | :club | :diamond ) 4 @type value :: ( 2..10 | :jack | :queen | :king | :ace ) 5 @type card :: { suit, value } 6 @type card_kind :: ( :number | :face ) 7 8 @spec kind(card()) :: card_kind() 9 def kind({_suit,value}) when is_number(value), do: :number 10 def kind({_suit,_value}), do: :face 11 12 @spec check_cards(card()) :: any() 13 def check_cards(card) do 14 IO.puts("kind(#{inspect(card)}) -> #{inspect(kind(card))}") 15 end 16 17 def main do 18 check_cards({:spade, :king}) 19 check_cards({:diamond, 10}) 20 end 21end

Page 22: Elixir and Dialyzer, Types and Typespecs, using and understanding them

CARDS

CARD EXAMPLEdefmodule Card do

def kind({_suit,value}) when is_number(value), do: :number

def kind({_suit,_value}), do: :face

def check_cards(card) do IO.puts("kind(#{inspect(card)}) -> #{inspect(kind(card))}") end

def main do check_cards({:spades, :king}) check_cards({:rubies, 10}) end

end

Page 23: Elixir and Dialyzer, Types and Typespecs, using and understanding them

STRUCT

TYPES AND STRUCT

1defmodule Card do 2 defstruct [:suit, :value] 3 4 @type suit :: ( :spade | :heart | :club | :diamond ) 5 @type value :: ( 2..10 | :jack | :queen | :king | :ace ) 6 7 @type t :: %Card{ suit: suit(), value: value()} 8 9 @type card_kind :: ( :number | :face ) 10 11 @spec kind(Card.t) :: card_kind() 12 def kind(%Card{value: value} = card) when is_number(value), do: :number 13 def kind(_card), do: :face 14end

Page 24: Elixir and Dialyzer, Types and Typespecs, using and understanding them

ADDITIONAL RESOURCES

Page 25: Elixir and Dialyzer, Types and Typespecs, using and understanding them

ADDITIONAL RESOURCES

DOCUMENTATION

▸ http://learnyousomeerlang.com/dialyzer

▸ https://hexdocs.pm/elixir/typespecs.html

▸ https://hex.pm/packages/dialyxir

▸ http://user.it.uu.se/~kostis/Papers/succ_types.pdf

▸ Elixir Conf 2016 Jason Voegele Dialyzer talk

Page 26: Elixir and Dialyzer, Types and Typespecs, using and understanding them

THANK YOU