<Enjoy-Elixir>Jasonを読む(1)
Enjoy-Elixirと称する、プログラミング言語Elixirを勉強する記事群の1つです。
Elixir製のJSONライブラリjason のコードを読みます。
本日のコード
今回、読むコードは jason/lib/jason.ex
の一部です。
defmodule Jason do
@moduledoc """
A blazing fast JSON parser and generator in pure Elixir.
"""
alias Jason.{Encode, Decoder, DecodeError, EncodeError, Formatter}
@type escape :: :json | :unicode_safe | :html_safe | :javascript_safe
@type maps :: :naive | :strict
@type encode_opt :: {:escape, escape} | {:maps, maps} | {:pretty, boolean | Formatter.opts()}
@type keys :: :atoms | :atoms! | :strings | :copy | (String.t() -> term)
@type strings :: :reference | :copy
@type decode_opt :: {:keys, keys} | {:strings, strings}
# ... 省略
end
次のことを学びます。
- モジュール定義とモジュール属性
alias
キーワード
モジュール定義とモジュール属性
モジュールの定義方法
Elixirの主役は関数です。関数には 無名関数
と 名前付き関数
があります。
無名関数
は fn (arg, ...) -> ... end
を使って次のように定義します。
## say は関数名ではありません。
iex> say = fn word -> IO.puts "Hello, #{String.capitalize(word)}" end
#Function<44.40011524/1 in :erl_eval.expr/5>
## 無名関数は、 `.` を使って呼び出します。
iex > say.("world")
Hello, World
:ok
もう1つの関数、 名前付き関数
は実はモジュールの中でないと定義できません。モジュールの外で定義しようとすると次のようにエラーになります。
iex> def say do
...> IO.puts "hey"
...> end
** (ArgumentError) cannot invoke def/2 outside module
(elixir 1.12.1) lib/kernel.ex:5960: Kernel.assert_module_scope/3
(elixir 1.12.1) lib/kernel.ex:4699: Kernel.define/4
(elixir 1.12.1) expanding macro: Kernel.def/2
iex:1: (file)
名前付き関数
は def
を使って定義しますが、モジュールの外で使うとエラーになります。
名前付き関数
を定義するためには、モジュールが必要になります。 というわけで、モジュールって重要な要素ですね。
iex> defmodule Say do
...> def hey do
...> IO.puts "hey"
...> end
...> end
{:module, Say,
<<70, 79, 82, 49, 0, 0, 4, 228, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 0, 148,
0, 0, 0, 16, 10, 69, 108, 105, 120, 105, 114, 46, 83, 97, 121, 8, 95, 95,
105, 110, 102, 111, 95, 95, 10, 97, 116, ...>>, {:hey, 0}}
## 定義したモジュールの名前付き関数を呼び出す
iex> Say.hey
hey
:ok
モジュールの名前は実はアトム
では、 jason
のコードに戻りましょう。
defmodule Jason do
@moduledoc """
A blazing fast JSON parser and generator in pure Elixir.
"""
# ...省略
end
defmodule
を使って、 Jason
という名前のモジュールを定義しています。
ここで問題です。モジュールの名前にしている Jason
とは何でしょうか? たとえば、 jason
と小文字で定義することはできないのでしょうか? やってみましょう。
iex> defmodule jason do
...> end
** (CompileError) iex:15: undefined function jason/0
(stdlib 3.15.1) lists.erl:1358: :lists.mapfoldl/3
(elixir 1.12.1) expanding macro: Kernel.defmodule/2
iex:15: (file)
エラーになりますね。 jason
は関数と解釈されてしまっていますね。
豆知識的なものですが、モジュールの名前に使えるのは アトム です。
アトムというのはコード上の見た目は、 :hoge
というようなものですね。あと :"hello world"
もアトムです。そして、実は Jason
という識別子もアトムになります。 is_atom
関数は、引数がアトムだったら、 true
を返します。確認してみましょう。
iex> is_atom(:hoge)
true
iex> is_atom(:"hello world")
true
iex> is_atom(Jason)
true
iex> is_atom(123)
false
iex> is_atom("hello")
false
上の結果のとおり、 Jason
はアトムです。実は、ここではElixirによる暗黙的な解釈・処理が行われています。暗黙的なので、わかりにくいです。または、知らなくてもいいといえば、知らなくてもいいものです。でも Jason
というような 先頭が大文字ではじまる識別子は(本当の姿を隠していますが)アトムです。
では、Jason
という識別子の本当の姿を見てみましょう。
## もちろん、文字列とアトムは一致しない
iex> Jason == "Jason"
false
## これは、一致しそうに思えるが別のものである
iex> Jason == :Jason
false
## これも一致しない
iex> Jason == :jason
false
## Jasonの本当の姿はコレだ!!
iex> Jason == :"Elixir.Jason"
true
そうなんです。 Jason
は :"Elixir.Jason"
というアトムと同値なのです! これはつまり、 Jason
と書くと暗黙的にElixirは :"Elixir.Jason"
というアトムに変換して扱う、ということです。たとえば、 HTML
という識別子は、 :"Elixir.HTML"
というアトムになります。
なので、
defmodule Jason do
## ...
end
というモジュールの定義は、
defmodule :"Elixir.Jason" do
## ...
end
と定義しても同じになります。実際に、上のようにモジュール名の記述を変更して、コンパイルしテストを実行した場合、同じ結果になります。
モジュール属性またはメタデータ
モジュール属性は、モジュールの中に定義できるデータです。そして結構、奥が深いです。ここでは簡単に触れます。
モジュール属性はElixirで定義されているものの他にユーザーが定義できるものがあります。
モジュールの中で、 @
をプレフィックスにして記述します。
モジュール属性の主な使い方は2つです。
- モジュールやモジュール内の関数についての注釈・解説
- モジュール内で使用する定数の定義
注釈・解釈というのは、つまり説明のことです。 Jason
モジュールを見てみましょう。
defmodule Jason do
@moduledoc """
A blazing fast JSON parser and generator in pure Elixir.
"""
## ... 省略
end
上の @moduledoc
がモジュール属性です。@moduledoc
はモジュールについての注釈のために用意されているモジュール属性です。
モジュール属性とコメントの違いは何かというと、モジュール属性は文字通りメタデータなので、 @moduledoc
はコードで取得することができます。
## Code.fetch_docs 関数を使って、ドキュメントを取得する
iex> {:docs_v1, _, :elixir, _, %{"en" => module_doc}, _, _} = Code.fetch_docs(Jason)
iex> IO.puts module_doc
A blazing fast JSON parser and generator in pure Elixir.
:ok
次に、モジュール内の定数としての使い方は、モジュール内で繰り返し使うような値や引数のデフォルト値を設定したりするのに使います。
稚拙で荒いコード例ですが、次のようにユーザー定義のモジュール属性を定義し、関数で利用できます。
defmodule Sample do
@message "hello, world"
@default_arg [0, 10, 20, 30, 40 , 50]
def say do
IO.puts @message
end
def show(arg \\ @default_arg) do
IO.inspect arg
end
end
Sample.say
Sample.show
Sample.show "hello"
## 出力は次の通り
hello, world
[0, 10, 20, 30, 40, 50]
"hello"
とりあえず、モジュールのコードを読むときに @hogehoge
みたいなものを見て、「これはモジュール属性だな」とわかればOKです。
だから、
@type escape :: :json | :unicode_safe | :html_safe | :javascript_safe
@type maps :: :naive | :strict
@type encode_opt :: {:escape, escape} | {:maps, maps} | {:pretty, boolean | Formatter.opts()}
@type keys :: :atoms | :atoms! | :strings | :copy | (String.t() -> term)
@type strings :: :reference | :copy
@type decode_opt :: {:keys, keys} | {:strings, strings}
上記の @type ...
もモジュール属性です。そして、型定義や関数仕様という機能や概念とつながっていて、奥が深い。型定義や関数仕様については別記事で書く予定です。
とりあえず、モジュール属性については今回はここまで。
alias
キーワード
alias
はモジュールに別名をつけるキーワードです。そして、ショートカットを作るのにも使われます。
defmodule Jason do
@moduledoc """
A blazing fast JSON parser and generator in pure Elixir.
"""
alias Jason.{Encode, Decoder, DecodeError, EncodeError, Formatter}
# ... 省略
end
上の例では、ショートカットを作成する目的で alias
が使われています。
Decoder
の完全なモジュール名は Jason.Decoder
です。 Jason
モジュールの中で、次のように Decoder
は使われています。
def decode(input, opts \\ []) do
input = IO.iodata_to_binary(input)
Decoder.parse(input, format_decode_opts(opts))
## Jason.Decoder.parse(input, format_decode_opts(opts))
end
Jason.Decoder.parse
と書くべきところを Decoder.parse
とショートカットして書けるようにするために、 alias
を使ったわけです。
{ ... }
で囲んでいる理由は、まとめてショートカットを作るためです。
alias Jason.Encode
alias Jason.Decoder
alias Jason.DecodeError
alias Jason.EncodeError
alias Jason.Formatter
上記のような別名の定義をまとめて1行で定義したのが、 { ... }
を使った書き方です。
alias Jason.{Encode, Decoder, DecodeError, EncodeError, Formatter}
別名というくらいですから、まったく違った名前にすることも可能です。次のように。
alias ThisIsTooLongNameModule, as: ShortName
ただし、別名の定義をするとわかりにくくなる場合があるので注意が必要です。
まったくの別名にするケースとしては、使用するモジュールの名前が重複している場合でしょう。
alias ModuleA.Utils, as: UtilsA
alias ModuleB.Utils, as: UtilsB
上の例の場合は、 Utils
という名前が重なってしまい、同じスコープで同時には使えません。そのため別名を与える必要があります。
ここで少し モジュールの階層 について説明します。
Jason
モジュールと Jason.Encode
、 Jason.Decoder
などのモジュールは階層構造があるように見えますよね。
なので、 Jason
モジュールの中でなら、暗黙的に階層を認識して配下のモジュールにアクセスできるはずだ、と考えてもおかしくありません。
たとえば次のようなコード。
defmodule ModuleA.Child do
def say do
IO.puts "Hi, This is ModuleA Child"
end
end
defmodule ModuleA do
def say do
IO.puts "Hi, This is ModuleA"
## モジュール階層の下にある関数を呼び出せるか?
Child.say
end
end
ModuleA.say
実行結果は次の通りです。
Hi, This is ModuleA
** (UndefinedFunctionError) function Child.say/0 is undefined (module Child is not available)
Child.say()
(elixir 1.12.1) lib/code.ex:1261: Code.require_file/2
## 結論: 呼び出せない
暗黙的に階層構造が解決されて、関数の呼び出しが出来ると期待するかもしれませんが、エラーになります。
実は、Elixir自体はモジュール間の階層構造を認識しません。すべてフラットに並ぶモジュールです。
Jason
も Jason.Encode
, Jason.Decoder
なども、すべて同じレベルのモジュールなのです。
階層構造があるように見えるのは 人間だけ です。
なので、モジュール名で階層のように見せるのはコードを読む人間のためです。
階層構造があるようにみえても暗黙的な解決はされないので、 alias
を使ってショートカットを作成する方法が提供されていると考えてもよいでしょう。
まとめ
名前付き関数
は、モジュールの外では定義できない- モジュールの名前はアトムである
- 先頭が大文字ではじまる識別子はアトムとして扱われる
- モジュール属性の使い方の1つはモジュールやモジュール内の関数の説明である
- 注釈モジュール属性コードから値を取得できる(コメントは値を取得できない)
- モジュール属性の使い方の1つはモジュール内で利用する定数である
@type
もモジュール属性。そして型定義という奥の深い話があるalias
の使い方その1。別名をつける。これは重複した名前のモジュールを使いたい場合に有効alias
の使い方その2。モジュールにアクセスするショートカットを作る- 実はモジュール自体に階層構造はない。階層構造を認識するのは人間だけ
次は 型定義
について書く予定です。