Elixir 入門 その19 - Typespecs and behaviours
Types and specs
- ElixirはTypespecを使い以下の目的に用いる
- カスタムデータ型を定義するとき
- 関数のシグネチャ(仕様)を定義するとき
- Typespecは開発者のためだけのものではない。例えばErlangの静的解析ツールの dialyzer は typespecディレクティブを参照する。
Function specifications
@spec
を使うと関数のシグネチャを定義できる。- 使える型は Elixir v1.0.5 Documentation を見る
- 以下はround/1の例
# specの左辺に関数名と引数を、右辺には関数の戻り値の基本型を書く # listを返す場合は [integer] のようにする。tupleは{:atom, integer} など。 @spec round(number) :: integer def round(number), do: # implementation...
Defining custom types
@type
を使うとビルトインの基本型の他にカスタムの型を定義することができる- スコープをprivateにしたいときは
@typep
を使うとよい
defmodule LousyCalculator do # カスタム型の説明と定義 @typedoc """ Just a number followed by a string. """ @type number_with_offense :: {number, String.t} # returnにカスタム型を指定できる @spec add(number, number) :: number_with_offense def add(x, y), do: {x + y, "You need a calculator to do that?!"} @spec multiply(number, number) :: number_with_offense def multiply(x, y), do: {x * y, "Jeez, come on!"} end
- exportも可能
defmodule PoliteCalculator do @spec add(number, number) :: number def add(x, y), do: make_polite(LousyCalculator.add(x, y)) # 引数の型としてカスタム型を指定している @spec make_polite(LousyCalculator.number_with_offense) :: number defp make_polite({num, _offense}), do: num end
Behaviours
- 多くのモジュールはAPIを共有している。例えばPlugの個々のモジュールにはinit/1とcall/2が実装されている。
- Behaviourは以下の目的のために定義される。Javaにおけるinterfaceのようなもの。
- モジュールによって実装しなければいけない関数を定義する
- 必要な関数がすべて実装されていることを保証する
Defining behaviours
- 例えばJSONとYAMLのParserを書きたいとき、Parserとしては同じ動きで扱うものが違うだけなので共通のインタフェースを作ったほうがよい。まずはParserというbehaviourを定義する
defmodule Parser do # behaviour をrequireして __using__ を呼び出して初期設定を行う use Behaviour # defcallbackで実装しなければいけない関数を定義。 # defcallbackで定義した関数はbehaviourを実装したモジュール内ですべて定義する必要がある defcallback parse(String.t) :: any defcallback extensions() :: [String.t] end
defmodule JSONParser do @behaviour Parser def parse(str), do: # ... parse JSON def extensions, do: ["json"] end defmodule YAMLParser do @behaviour Parser def parse(str), do: # ... parse YAML def extensions, do: ["yml"] end
Elixir 入門 その18 - try, catch and rescue
- Elixirは errors throws exits の3つのErrorに関する仕組みを持つ
- ただ、Erlangでは基本的に「クラッシュさせるならさせておけ」というポリシーがあるのでどのような場合にErrorをハンドリングするかは考えるべき
Errors
- ErrorあるいはExceptionはコードの中で想定外のことが起きた場合に使われる
# 例えばatomとintegerを計算しようとすると算術例外が起きる iex> :foo + 1 ** (ArithmeticError) bad argument in arithmetic expression :erlang.+(:foo, 1) # raiseを使えばランタイムで意図的に例外を起こせる iex> raise "oops" ** (RuntimeError) oops
- 自分でカスタムエラーを定義することもできる
# defexception で自前の例外を定義できる defmodule MyError do defexception message: "default message" end
# 例外の名前はモジュール名が表示される iex> raise MyError ** (MyError) default message iex> raise MyError, message: "custom message" ** (MyError) custom message
- try-catch的な仕組みもある。Elixirではcatchはrescueという名前
iex> try do ...> raise "oops" ...> rescue ...> e in RuntimeError -> e ...> end %RuntimeError{message: "oops"}
- ただ、Elixirではあまりこの仕組みは使わない。例えばファイルを開く場合、成功した場合は
:ok
を含んだtupleを、なんらかの理由で失敗した場合は:error
を含んだtupleがファイルオープンの関数の結果として返される。 - 標準ライブラリの多くの関数は成功・失敗の情報を含んだtupleを返す。
- つまり、パターンマッチを使って関数の結果を見て判定できる
iex> case File.read "hello" do ...> {:ok, body} -> IO.puts "Success: #{body}" ...> {:error, reason} -> IO.puts "Error: #{reason}" ...> end
- なので標準のライブラリのAPIを使っている場合、ほとんどの場合においてtry-throw-rescueの仕組みは使わない。APIでカバーできない場合に初めて使うことを考慮すべき。
after
を使うと必ず実行される。try-catchにおけるfinallyのようなもの。- 例えばファイルを開いたのちに書き込む処理で、ファイルが開かれた後に削除される可能性があるのであれば、以下のように例外をケアしたほうがよいだろう
iex> {:ok, file} = File.open "sample", [:utf8, :write] iex> try do ...> IO.write file, "olá" ...> raise "oops, something went wrong" ...> after ...> File.close(file) ...> end ** (RuntimeError) oops, something went wrong
Exits
- すべてのElixirのコードは相互に通信しあいながら動く。unhandled exceptionなどでプロセスが死ぬ場合、そのプロセスはexit signalを送る。同様に開発者が明示的にexit signalを送ることもできる。
# spawnで新しく作成したプロセスが明示的にexitを送る例 iex> spawn_link fn -> exit(1) end #PID<0.56.0> ** (EXIT from #PID<0.56.0>) 1
- Exit signalは Erlang VMから受け継いだ、フォールトレトラントなシステムの重要な部分である。プロセスは通常、
supervised processes
からのexitを待つsupervision trees
の下で動く。いったんsupervised process
からexit
を受け取ったら、それを監督するprocessは、そのprocessをkickしてrestartする。(ちょっとここ訳怪しい) - この仕組みの下で動くElixir(というかErlang)ではプロセスが失敗したら早く終わらしたほうが良いということになる。なぜなら
supervision tree
の構造はプロセスが失敗したら再開されることを保証するからである。
Elixir 入門 その17 - Sigils
~
で始まるテキストをSigilといい、文字列を扱うための仕組みを提供する。- delimiter sigil delimiter modifier の形式。
r/hogehoge/i
みたいな形。
Regular expressions
# fooかbarだったらtrueを返す正規表現を作成 iex> regex = ~r/foo|bar/ ~r/foo|bar/ iex> "foo" =~ regex true iex> "bat" =~ regex false
Strings, char lists and words sigils
Strings
~s
は文字列を生成するためのSigil。"
で囲むのと同じ意味合いだけど、こちらは文字列内に文字列内に"
が含まれている場合でもエスケープしないでstringを作成できる
iex> ~s(this is a string with "double" quotes, not 'single' ones) "this is a string with \"double\" quotes, not 'single' ones"
Char lists
~c
も上に同じ。
iex> ~c(this is a char list containing 'single quotes') 'this is a char list containing \'single quotes\''
Word lists
~w
はスペース区切りの文字列をSplitしてListを返すSigil- modifierとしてc、s、a を受け入れることができて、指定すると分割した上でmodifierに応じた型でListに詰め込まれて変換される
iex> ~w(foo bar bat) ["foo", "bar", "bat"] # aはatomになる iex> ~w(foo bar bat)a [:foo, :bar, :bat]
Interpolation and escaping in sigils
- sigilは小文字と大文字で動きが異なる。
~s
と~S
の違いを見てみる
# こちらは中の式やコードが展開されて文字列に変換される iex> ~s(String with escape codes \x26 #{"inter" <> "polation"}) "String with escape codes & interpolation" # こちらは中の式はそのまま出力される iex> ~S(String with escape codes \x26 #{"inter" <> "polation"}) "String with escape codes \\x26 \#{\"inter\" <> \"polation\"}"
サポートしているエスケープ文字
\" – double quote
- \' – single quote
- \ – single backslash
- \a – bell/alert
- \b – backspace
- \d - delete
- \e - escape
- \f - form feed
- \n – newline
- \r – carriage return
- \s – space
- \t – tab
- \v – vertical tab
- \0 - null byte
- \xDD - character with hexadecimal representation DD (e.g., \x13)
\x{D...} - character with hexadecimal representation with one or more hexadecimal digits (e.g., \x{abc13})
関数のドキュメントを書くときなどは
~S
を使うと便利な場合がある。
# 例えば使い方のコメントで下のように書くところを iex> convert("\\\"foo\\\"") # ~Sでくるめばこう書ける iex> convert("\"foo\"")
Custom sigils
- Sigilは自作することもできる
- 作り方は sigil_{identifier}/2 で関数を定義する。引数のところは stringとオプションを受け取るList
- Macroを使って動的にSigilを作成することもできるらしい。
# 整数を返す `~i` というSigilを作る iex> defmodule MySigils do ...> def sigil_i(string, []), do: String.to_integer(string) ...> def sigil_i(string, [?n]), do: -String.to_integer(string) ...> end iex> import MySigils iex> ~i(13) 13 iex> ~i(42)n -42
Elixir 入門 その16 - Comprehensions
- Elixirではあるリストをfilterしてmapにかけて別のリストを作るということをしたい場合、Enumモジュールを使うことが一般的だが、リスト内包表記を使って書くこともできる。forを使うとEnumやStreamを使って書くより簡単な記述にできることがある。
- (確か)Erlangの書き方に近い
Generators and filters
n <- [1, 2, 3, 4]
のような式をgeneratorという
# for リストから取り出した値 <- リスト , do: 値を加工する式 iex> for n <- 1..4, do: n * n [1, 4, 9, 16]
- リストから取り出す場合、フィルタをかけることができる
# パターンマッチを使ったフィルタ iex> values = [good: 1, good: 2, bad: 3, good: 4] iex> for {:good, n} <- values, do: n * n [1, 4, 16] # マッチするものが無い場合は空で返る iex> for {:hoge, n} <- values, do: n * n [] # フィルタ用の関数を作成。評価の結果がnilかfalseの場合はフィルタで取り除かれる iex> multiple_of_3? = fn(n) -> rem(n, 3) == 0 end # 3の倍数のものだけが取り出されている iex> for n <- 0..5, multiple_of_3?.(n), do: n * n [0, 9]
- generatorとfilterは複数定義することができる。
# ディレクトリ内のファイルを取得し、各ファイルのサイズをリストにして返す例 # dirとpathにgeneratorを使って束縛し、regular fileのみを抽出している # ファイルのパスをpathという変数に束縛しているが、スコープはやはりレキシカルで内部からのみ参照可能 for dir <- dirs, file <- File.ls!(dir), path = Path.join(dir, file), File.regular?(path) do File.stat!(path).size end
- バイナリを扱うこともできる
# バイナリからピクセルのリストを受け取ってtupleに変換する例 iex> pixels = <<213, 45, 132, 64, 76, 32, 76, 0, 0, 234, 32, 15>> iex> for <<r::8, g::8, b::8 <- pixels>>, do: {r, g, b} [{213, 45, 132}, {64, 76, 32}, {76, 0, 0}, {234, 32, 15}]
into:
オプションを使うとList以外のデータ型に変換することができるinto:
は変換後の元データにinto:
で指定した値の型に変換した上で結合して返る。変換出来ない場合は例外。
# into が無いと bitのリストが返る iex> for <<c <- " hello world ">>, c != ?\s, do: <<c>> ["h", "e", "l", "l", "o", "w", "o", "r", "l", "d"] # into: "" をつけるとstringで返る iex> for <<c <- " hello world ">>, c != ?\s, into: "", do: <<c>> "helloworld" # into: "Elixir " をつけるとElixir が先頭に結合された状態で返る iex> for <<c <- " hello world ">>, c != ?\s, into: "Elixir ", do: <<c>> "Elixir helloworld" # Mapを操作する場合、keyはそのままにvalueだけ加工したい場合など便利 iex> for {key, val} <- %{"a" => 1, "b" => 2}, into: %{}, do: {key, val * val} %{"a" => 1, "b" => 4} # intoが無いとこうなる iex> for {key, val} <- %{"a" => 1, "b" => 2}, do: {key, val * val} [{"a", 1}, {"b", 4}]
Elixir 入門 その15 - Protocols
Protocol
- ProtocolはElixirでポリモーフィズムを実現するための仕組み。プロトコルを使うことでどのようなデータでもやり取りできるようになる
- たとえば Elixirではnilとfalseだけがfalseとして扱われ、それ以外のすべてはtrueと評価されるが、アプリケーションによっては空白をfalseと評価したいケースもないだろうか?このような場合はプロトコルを使うと実現できる。
# Blankというプロトコルを定義 defprotocol Blank do @doc "Returns true if data is considered blank/empty" def blank?(data) end # プロトコルを実装していく # Integers are never blank defimpl Blank, for: Integer do def blank?(_), do: false end # Just empty list is blank defimpl Blank, for: List do def blank?([]), do: true def blank?(_), do: false end # Just empty map is blank defimpl Blank, for: Map do def blank?(map), do: map_size(map) == 0 end # Just the atoms false and nil are blank defimpl Blank, for: Atom do def blank?(false), do: true def blank?(nil), do: true def blank?(_), do: false end
- blank?/1を呼んで確認してみる
iex> Blank.blank?(0) false iex> Blank.blank?([]) true iex> Blank.blank?([1, 2, 3]) false # 実装されていないデータ型を指定すると例外 iex> Blank.blank?("hello") ** (Protocol.UndefinedError) protocol Blank not implemented for "hello"
- このようにargmentの型に応じて動きを変えることを実現している。
Protocols and structs
- Protocolはstructと共に使うと、より効果的に使える
# User structを定義 defmodule User do defstruct name: "john", age: 27 end # さきほどのBlank protocolにUserの型を実装 defimpl Blank, for: User do # nameが入っていなかったら空とする def blank?(%User{name: ""}), do: true def blank?(_), do: false end
# デフォルト値が入っているのでfalse iex> Blank.blank?(%User{}) false # nameを"" にするとblank扱いになる iex> Blank.blank?(%User{name: ""}) true
Falling back to Any
- protocolにanyを実装するとあらゆるデータ型に適用される
# protocol に@fallback_to_anyを追加 defprotocol Blank do @fallback_to_any true def blank?(data) end # anyを実装 defimpl Blank, for: Any do def blank?(_), do: false end
# 上の例では実装されていなかったstringがanyに適用されることがわかる iex> Blank.blank?("hello") false
Built-in protocols
Elixir 入門 その14 - Structs
- Mapの拡張してStructがある。
- Structは初期値の設定とコンパイル時のチェックを行ってくれる
Defining structs
- defstruct でStructを定義できる
defmodule User do # フィールド名と初期値を定義する defstruct name: "John", age: 27 end
# mapに似てるけど % モジュール名 {} で作成できる iex> %User{} %User{age: 27, name: "John"} # 初期値を与えればデフォルト値は上書かれる iex> %User{name: "Meg"} %User{age: 27, name: "Meg"} # 存在しないフィールドを指定すると例外となる iex> %User{oops: :field} ** (CompileError) iex:3: unknown key :oops for struct User
Accessing and updating structs
- Mapと同じようにStructのフィールドにアクセスしたり更新したりできる
- 同様にパターンマッチも使える
# Userを初期値の状態でjohnとして束縛 iex> john = %User{} %User{age: 27, name: "John"} # フィールドには . でアクセスできる iex> john.name "John" # 更新はmap と ほとんど同じように更新できる # Mapと同じくimmutableな存在なので更新後の値をvariableに束縛しないと後で使えない iex> meg = %{john | name: "Meg"} %User{age: 27, name: "Meg"} # 存在しないフィールドを更新しようとするとエラーになる iex> %{meg | oops: :field} ** (ArgumentError) argument error # パターンマッチを使ってnameフィールドの値を取得している iex> %User{name: name} = john %User{age: 27, name: "John"} iex> name "John" # マッチしなければエラー iex> %User{name: "Meg"} = john ** (MatchError) no match of right hand side value: %User{age: 27, name: "John"}
Structs are just bare maps underneath
- StructはMapとしてみた場合、struct という特別なフィールドを持っている
- MapのプロトコルやEnum、DictなどのモジュールではStructをargumentにできない。
- Mapモジュールのargumentにはできる。
# is_mapはtrueが返る iex> is_map(john) true # johnの__struct__フィールドを見るとUserが返る iex> john.__struct__ User # Mapモジュールは使える iex> Map.keys(john) [:__struct__, :age, :name]
Elixir 入門 その13 - Module attributes
- ElixirのModuleのattributeは次の3つの目的をかなえる
As annotations
- ElixirはErlangからモジュール属性の概念を取り入れている
- moduledocとdocはよく使われ、h コマンドで表示される。
- elixir-lang/ex_doc · GitHub を使うとアノテーションからHTMLを生成してくれる
defmodule Math do # モジュールのバージョンを定義する属性 @vsn 2 # モジュールの説明を定義する属性 @moduledoc """ Provides math-related functions. ## Examples iex> Math.sum(1, 2) 3 """ # ここまでモジュールの説明 # 関数やMacroの説明 @doc """ Calculates the sum of two numbers. """ def sum(a, b), do: a + b end
As constants
- attributeは定数としても使われ、必ず属性名と値を設定する
- ランタイムに動いているのではなくコンパイル時に登録される(Module.register_attribute/3)
defmodule MyServer do @initial_state %{host: "147.0.0.1", port: 3456} IO.inspect @initial_state end
As temporary storage
- elixir-lang/plug · GitHub というWebライブラリの作成やフレームワークのための共通の仕組みがある
- 例えば開発者は自身でPlugを使ってはWebサーバーで動くPlugを定義できる。
defmodule PlugSample do use Plug.Builder # plug/1 を呼ぶことで @plugs attributeに指定したplugが追加される # コンパイル前にcall/2が走ってhttp requestを受けたときに実行される処理として登録したplugを順番に実行するように作られる # なので例えば先に:send_ok を定義するとsend_respの後にheaderをセットしようとするので死ぬ plug :set_header plug :send_ok def set_header(conn, _opts) do IO.puts "ここがはじめにうごく" put_resp_header(conn, "x-header", "set") end def send_ok(conn, _opts) do IO.puts "ここがつぎにうごく" send_resp(conn, 200, "ok") end end IO.puts "Running PlugSample with Cowboy on http://localhost:4000" Plug.Adapters.Cowboy.http PlugSample, []
> mix run --no-halt lib/plug_sample.ex Running PlugSample with Cowboy on http://localhost:4000 # http://localhost:4000 で ok が返るようになる
- 詳しいことはメタプロの章で