もやもやエンジニア

IT系のネタで思ったことや技術系のネタを備忘録的に綴っていきます。フロント率高め。

Elixir 入門 その19 - Typespecs and behaviours

Types and specs

  • ElixirはTypespecを使い以下の目的に用いる
    • カスタムデータ型を定義するとき
    • 関数のシグネチャ(仕様)を定義するとき
  • Typespecは開発者のためだけのものではない。例えばErlangの静的解析ツールdialyzer は typespecディレクティブを参照する。

Function specifications

# 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

  • 例えばJSONYAMLの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

  • ~rはよく使われるSigilで正規表現を作成するために使う
  • 正規表現Perl-compatible 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ではすでに組み込み済みの多数のprotocolがある。今までにみてきたEnumモジュールの関数では Enumerable プロトコルを実装したデータ型を引数として受けていた。
  • 他にも例えば、to_stringは様々なデータ型を受け取ることができるが、それらはString.Chars プロトコルの実装によって実現している。

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つの目的をかなえる
    • ユーザーやVMによって使用されるためのモジュールの説明をつける
    • 定数として動く
    • コンパイルの間に使われるためテンポラリ領域として動く

As annotations

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

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 が返るようになる
  • 詳しいことはメタプロの章で