どうも、靖宗です。
ようやく20章、終わりが見えてきました。と思ったらそのしたにMIX AND OTP
の文字が・・・
たぶんこっちが本編だよね( ´゚д゚`)
めげずに進みます。
Types and specs
Elixirは動的型付け系でした。(今まで特に型を指定して変数を使ってない)
下記の2点に於いてはtypespecs
を利用することで型を宣言できるようです。
- 関数の型宣言
- 特殊なデータ型の宣言
とりあえず進みます。
Function specifications
というかElixirの関数のドキュメントとかを見てみると書いてあるんですねこれが。
round(number) :: integer
この:: integer
という箇所が型の定義っぽいです。
ただしこれはドキュメント上の表記で、実際には
@spec round(number) :: integer def round(number), do: # implementation...
と、関数の属性を利用して型宣言するようです。
用意されてる型は仕様を見てね!とのこと。
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は下記の役割を担っているそうです。
- モジュールの実装しなきゃいけない関数の定義
- モジュール内の関数が実装されていることの保証
なんとなく分かるような分からんような・・・
兎に角次へ。
Defining behaviours
ここでパーサーを実装していく例で学習していきます。
JSONやXMLのパーサーを作って行く想定ですが、まずはもっと抽象的なパーサーについて考えます。
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でしたね。
モジュール定義の時に@behaviour
でbehaviour
を呼び、@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個目にモジュールそのものが放り込まれる仕様?
なにはともあれこの!
実装などはよく使いそうです。