diff --git a/docs/slides.md b/docs/slides.md index aff48a2..e115b32 100644 --- a/docs/slides.md +++ b/docs/slides.md @@ -1245,6 +1245,111 @@ class: center, middle --- class: center, middle +## Typespecs + +--- +class: center, middle + +Elixir is a dynamically typed language, and as such, type specifications are never used by the compiler to optimize or modify code. + +--- + +Still, using type specifications is useful because: + +- they provide documentation (for example, tools such as ExDoc show type specifications in the documentation) + +- they're used by tools such as Dialyzer, that can analyze code with typespecs to find type inconsistencies and possible bugs + +--- + +Type specifications (most often referred to as typespecs) are defined in different contexts using the following attributes: + +- `@type` +- `@opaque` +- `@typep` +- `@spec` +- `@callback` +- `@macrocallback` + +--- +class: center, middle + +```elixir +defmodule StringHelpers do + @typedoc "A word from the dictionary" + @type word() :: String.t() + + @spec long_word?(word()) :: boolean() + def long_word?(word) when is_binary(word) do + String.length(word) > 8 + end +end +``` + +--- + +- We declare a new type (`word()`) that is equivalent to the string type (`String.t()`). + +- We describe the type using a `@typedoc`, which will be included in the generated documentation. + +- We specify that the `long_word?/1` function takes an argument of type `word()` and returns a boolean (`boolean()`), that is, either `true` or `false`. + +--- +class: center, middle + +### [Types and their syntax](https://hexdocs.pm/elixir/typespecs.html#types-and-their-syntax) + +--- + +#### Remote Types + +Any module is also able to define its own types and the modules in Elixir are no exception. For example, the Range module defines a `t/0` type that represents a range: this type can be referred to as `Range.t/0`. In a similar fashion, a string is `String.t/0`, and so on. + +--- +class: center, middle + +### Behaviours + +--- +class: center, middle + +Behaviours in Elixir (and Erlang) are a way to separate and abstract the generic part of a component (which becomes the behaviour module) from the specific part (which becomes the callback module). + +--- +class: center, middle + +#### Parser Example + +--- + +Defining a callback is a matter of defining a specification for that callback, made of: + +- the callback name (`parse` or `extensions` in the example) + +- the arguments that the callback must accept (`String.t`) + +- the expected type of the callback return value + +--- +class: center, middle + +#### Using Behaviour + +--- +class: center, middle + +Behaviours are useful because you can pass modules around as arguments and you can then call back to any of the functions specified in the behaviour. + +--- +class: center, middle + +#### Optional Callbacks + +Optional callbacks can be defined through the `@optional_callbacks` module attribute, which has to be a keyword list with `function` or `macro` name as key and `arity` as value. + +--- +class: center, middle + Code https://github.com/AgarwalConsulting/elixir_training diff --git a/examples/12_typespecs/bad_parser.ex b/examples/12_typespecs/bad_parser.ex new file mode 100644 index 0000000..f925dcb --- /dev/null +++ b/examples/12_typespecs/bad_parser.ex @@ -0,0 +1,9 @@ +defmodule BADParser do + @behaviour Parser + + @impl Parser + def parse, do: {:ok, "something bad"} + + @impl Parser + def extensions, do: ["bad"] +end diff --git a/examples/12_typespecs/csv_parser.ex b/examples/12_typespecs/csv_parser.ex new file mode 100644 index 0000000..63f330d --- /dev/null +++ b/examples/12_typespecs/csv_parser.ex @@ -0,0 +1,9 @@ +defmodule CSVParser do + @behaviour Parser + + @impl Parser + def parse(str), do: {:ok, "some csv " <> str} # ... parse CSV + + @impl Parser + def extensions, do: [".csv"] +end diff --git a/examples/12_typespecs/example_parser.ex b/examples/12_typespecs/example_parser.ex new file mode 100644 index 0000000..4b0d6e1 --- /dev/null +++ b/examples/12_typespecs/example_parser.ex @@ -0,0 +1,26 @@ +defmodule ExampleParser do + @spec parse_path(Path.t(), [module()]) :: {:ok, term} | {:error, atom} + def parse_path(filename, parsers) do + with {:ok, ext} <- parse_extension(filename), + {:ok, parser} <- find_parser(ext, parsers), + {:ok, contents} <- File.read(filename) do + parser.parse(contents) + end + end + + defp parse_extension(filename) do + if ext = Path.extname(filename) do + {:ok, ext} + else + {:error, :no_extension} + end + end + + defp find_parser(ext, parsers) do + if parser = Enum.find(parsers, fn parser -> ext in parser.extensions() end) do + {:ok, parser} + else + {:error, :no_matching_parser} + end + end +end diff --git a/examples/12_typespecs/json_parser.ex b/examples/12_typespecs/json_parser.ex new file mode 100644 index 0000000..32734f1 --- /dev/null +++ b/examples/12_typespecs/json_parser.ex @@ -0,0 +1,9 @@ +defmodule JSONParser do + @behaviour Parser + + @impl Parser + def parse(str), do: {:ok, "some json " <> str} # ... parse JSON + + @impl Parser + def extensions, do: [".json"] +end diff --git a/examples/12_typespecs/parser.ex b/examples/12_typespecs/parser.ex new file mode 100644 index 0000000..988de05 --- /dev/null +++ b/examples/12_typespecs/parser.ex @@ -0,0 +1,11 @@ +defmodule Parser do + @doc """ + Parses a string. + """ + @callback parse(String.t) :: {:ok, term} | {:error, atom} + + @doc """ + Lists all supported file extensions. + """ + @callback extensions() :: [String.t] +end diff --git a/examples/12_typespecs/simple.ex b/examples/12_typespecs/simple.ex new file mode 100644 index 0000000..dabd903 --- /dev/null +++ b/examples/12_typespecs/simple.ex @@ -0,0 +1,9 @@ +defmodule StringHelpers do + @typedoc "A word from the dictionary" + @type word() :: String.t() + + @spec long_word?(word()) :: boolean() + def long_word?(word) when is_binary(word) do + String.length(word) > 8 + end +end