<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.EncodeJason.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自体はモジュール間の階層構造を認識しません。すべてフラットに並ぶモジュールです。

JasonJason.Encode, Jason.Decoder なども、すべて同じレベルのモジュールなのです。

階層構造があるように見えるのは 人間だけ です。

なので、モジュール名で階層のように見せるのはコードを読む人間のためです。

階層構造があるようにみえても暗黙的な解決はされないので、 alias を使ってショートカットを作成する方法が提供されていると考えてもよいでしょう。

まとめ

  • 名前付き関数 は、モジュールの外では定義できない
  • モジュールの名前はアトムである
  • 先頭が大文字ではじまる識別子はアトムとして扱われる
  • モジュール属性の使い方の1つはモジュールやモジュール内の関数の説明である
  • 注釈モジュール属性コードから値を取得できる(コメントは値を取得できない)
  • モジュール属性の使い方の1つはモジュール内で利用する定数である
  • @type もモジュール属性。そして型定義という奥の深い話がある
  • alias の使い方その1。別名をつける。これは重複した名前のモジュールを使いたい場合に有効
  • alias の使い方その2。モジュールにアクセスするショートカットを作る
  • 実はモジュール自体に階層構造はない。階層構造を認識するのは人間だけ

次は 型定義 について書く予定です。

参考