技術メモ

プログラミングとか電子工作とか

Elixir入門(第二十章 Typespecs and behaviours)

f:id:ysmn_deus:20190122112104p:plain

どうも、靖宗です。
ようやく20章、終わりが見えてきました。と思ったらそのしたにMIX AND OTPの文字が・・・
たぶんこっちが本編だよね( ´゚д゚`)
めげずに進みます。

Types and specs

Elixirは動的型付け系でした。(今まで特に型を指定して変数を使ってない)
下記の2点に於いてはtypespecsを利用することで型を宣言できるようです。

  1. 関数の型宣言
  2. 特殊なデータ型の宣言

とりあえず進みます。

Function specifications

というかElixirの関数のドキュメントとかを見てみると書いてあるんですねこれが。

round(number) :: integer

この:: integerという箇所が型の定義っぽいです。
ただしこれはドキュメント上の表記で、実際には

@spec round(number) :: integer
def round(number), do: # implementation...

と、関数の属性を利用して型宣言するようです。
用意されてる型は仕様を見てね!とのこと。

hexdocs.pm

Defining custom types

C言語でいうtypedefみたいなものでしょうか。とりあえず進む。

とりあえずこんなモジュールを作りたいというサンプル。

defmodule LousyCalculator do
  @spec add(number, number) :: {number, String.t}
  def add(x, y), do: {x + y, "You need a calculator to do that?!"}

  @spec multiply(number, number) :: {number, String.t}
  def multiply(x, y), do: {x * y, "Jeez, come on!"}
end

文字列の型がString.tになってるのはErlangとの関係性とのこと。
関数の型宣言の{number, String.t}が冗長!うっとい!のでこれを宣言してスッキリしようぜ!という流れになるはず。
ここで、関数の属性の一つ@typeを利用して型を宣言します。

defmodule LousyCalculator do
  @typedoc """
  Just a number followed by a string.
  """
  @type number_with_remark :: {number, String.t}

  @spec add(number, number) :: number_with_remark
  def add(x, y), do: {x + y, "You need a calculator to do that?"}

  @spec multiply(number, number) :: number_with_remark
  def multiply(x, y), do: {x * y, "It is like addition on steroids."}
end

ついでに@typedocも出てきてますが@docとかと一緒なので割愛。
@type新しく宣言したい型 :: 実際の型とすれば、関数内でその型が利用できるみたいです。

モジュールが利用できる場合であれば、その宣言した型が使える模様。
上のモジュールの型宣言を利用するならLousyCalculator.number_with_remarkという感じですね。

defmodule QuietCalculator do
  @spec add(number, number) :: number
  def add(x, y), do: make_quiet(LousyCalculator.add(x, y))

  @spec make_quiet(LousyCalculator.number_with_remark) :: number
  defp make_quiet({num, _remark}), do: num
end

モジュールの一部っぽくなってるので、名前空間を汚すこと無く使用できます。
プライベートでのみ利用する場合は@defpという属性があるようです。

Static code analysis

ドキュメント生成の時とかにも訳に立つから使うんやで!的な内容。
どこまでやるかは追々考えましょう。

Behaviours

直訳で”ふるまい”ですが、「Javaでいうinterfaceみたいなもんだよ」と書かれています。おそらくモジュールの仕様や実際に使うときにdefimplで実装しないと怒られるとか?
Behavioursは下記の役割を担っているそうです。

  1. モジュールの実装しなきゃいけない関数の定義
  2. モジュール内の関数が実装されていることの保証

なんとなく分かるような分からんような・・・
兎に角次へ。

Defining behaviours

ここでパーサーを実装していく例で学習していきます。
JSONXMLのパーサーを作って行く想定ですが、まずはもっと抽象的なパーサーについて考えます。
JSONであろうがXMLであろうが、とりあえず仕様としては

  • parse/1という関数でパースして、{:ok, term}を返す
  • extensions/0でそのパーサーがどんな拡張子に対応してるか(複数の想定もあるのでリスト)を返す

という感じでしょうか。

ここで、この抽象的な概念を実装するためにbehaviourを作ります。

defmodule Parser do
  @callback parse(String.t) :: {:ok, term} | {:error, String.t} # termはany、Elixirの仕様を確認。
  @callback extensions() :: [String.t]
end

普通のモジュールを作る感じで、抽象的な箇所を@callback属性で実装しています。
なにげに型の定義も一緒にできるんですね。 他の言語で言えばinterfaceとかabstractとかに近い感じでしょうか。
必要に応じて、このモジュール(behaviour)を継承するイメージ?でもオブジェクト指向っぽいので継承では無い筈。

Adopting behaviours

早速使っていく

defmodule JSONParser do
  @behaviour Parser

  @impl Parser
  def parse(str), do: {:ok, "some json " <> str} # ... parse JSON
  
  @impl Parser
  def extensions, do: ["json"]
end
defmodule YAMLParser do
  @behaviour Parser

  @impl Parser
  def parse(str), do: {:ok, "some yaml " <> str} # ... parse YAML
  
  @impl Parser
  def extensions, do: ["yml"]
end

JSONじゃないほうはYAMLでしたね。
モジュール定義の時に@behaviourbehaviourを呼び、@implで実装していく流れですね。
普段Pythonを使ってる身としてはデコレータっぽいんですが、完全に非なるものです・・・(´・ω・`)

behaviourで宣言してるのに@implで実装してないとwarningがでるようですが、一応コンパイルは通る?
一応ためす。

iex(2)> defmodule JSONParser do
...(2)>   @behaviour Parser
...(2)>   @impl Parser
...(2)>   def parse(str), do: {:ok, "some json " <> str} # ... parse JSON
...(2)> end
warning: function extensions/0 required by behaviour Parser is not implemented (in module JSONParser)
  iex:2

通ったっぽい。とりあえず警告してくれるだけヨシとしましょう。
@implしておきながら間違った実装をしている場合も警告してくれる。

defmodule BADParser do
  @behaviour Parser

  @impl Parser
  def parse, do: {:ok, "something bad"}
  
  @impl Parser
  def extensions, do: ["bad"]
end

parse/1なのにparse/0になってますね。

Dynamic dispatch

@callbackでいちいち実装するんじゃなくて、@callbackで実装したものを利用する以外はどのモジュールでも同じものを実装したいときは、動的に関数を生成できるようです。

defmodule Parser do
  @callback parse(String.t) :: {:ok, term} | {:error, String.t}
  @callback extensions() :: [String.t]

  def parse!(implementation, contents) do
    case implementation.parse(contents) do
      {:ok, data} -> data
      {:error, error} -> raise ArgumentError, "parsing error: #{error}"
    end
  end
end

behaviourとして実装してるモジュールで、宣言時の関数を呼び出すと引数の1個目にモジュールそのものが放り込まれる仕様?
なにはともあれこの!実装などはよく使いそうです。