やっちのわいわい日記

電通大に編入した元高専生の日記。日記を書きつつ編入のこととか勉強のこととか書こうと思っています。編入とかで質問があるかたは@amhflcl0514にDMくださいお

Elixir勉強日記6 〜モジュール編〜

Elixir勉強日記6回目です

前回で関数まわりに触って若干お腹いっぱいになってサボり過ぎました

続きを進めます

f:id:nishikino3:20190611145254j:plain

elixirschool.com

モジュール... defmodule的な?

モジュール

私たちは経験的に、全ての関数を1つの同じファイルとスコープに持つと手に負えないことを知っています。 このレッスンでは関数をまとめ、構造体として知られる特別なマップを定義することで、コードをより効率のよい形に組織化する方法を取り上げます。

スコープを切り分けるためにあるんですね(情弱)

モジュール

モジュールは関数群を名前空間へと組織する最良の方法です。 関数をまとめることに加えて、関数のレッスンで取り上げた名前付き関数やプライベート関数を定義できます。

落ち着いて考えたらクラスないしないと困りますね

基本的な例を見てみましょう:

defmodule Example do
  def greeting(name) do
    "Hello #{name}."
  end
end

iex> Example.greeting "Sean"
"Hello Sean."

Elixirではモジュールをネストすることが可能で、機能ごとにさらなる名前空間をつけることができます:

defmodule Example.Greetings do
  def morning(name) do
    "Good morning #{name}."
  end

  def evening(name) do
    "Good night #{name}."
  end
end

iex> Example.Greetings.morning "Sean"
"Good morning Sean."

.でネストの表現するんですね〜(Rubyだと:

モジュールの属性(attribute)

モジュール属性はElixirでは一般に定数として用いられることがほとんどです。 単純な例を見てみましょう:

defmodule Example do
  @greeting "Hello"

  def greeting(name) do
    ~s(#{@greeting} #{name}.)
  end
end

どうゆうことだってばよ...

重要なので言及しておきますと、 Elixir には予約されている属性があります。 もっとも一般的なのは以下の3つです:

  • moduledoc — 現在のモジュールにドキュメントを付けます。
  • doc — 関数やマクロについてのドキュメント管理。
  • behaviour — OTP またはユーザが定義した振る舞い(ビヘイビア)に用います。

あーー属性ってのはモジュール内で共通して扱う値のことなんですね

日本語にするとなんか微妙にぱっとしないですね

調べたら属性 => attributesでした

日本語分かりくすぎる

構造体(Structs)

構造体は定義済みのキーの一群とデフォルト値を持つ特殊なマップです。 モジュール内部で定義されなくてはならず、そのモジュールから名前をとります。 構造体にとっては、モジュール内部で自身しか定義されていないというのもありふれたことです。

構造体ってきくとCを思い出しますが...

モジュール内で定義されるマップの属性みたいな感じなんすね

構造体を定義するには defstruct を用い、フィールドとデフォルト値のキーワードリストを添えます:

defmodule Example.User do
  defstruct name: "Sean", roles: []
end

いくつか構造体を作ってみましょう:

iex> %Example.User{}
#Example.User<name: "Sean", roles: [], ...>

iex> %Example.User{name: "Steve"}
#Example.User<name: "Steve", roles: [], ...>

iex> %Example.User{name: "Steve", roles: [:manager]}
#Example.User<name: "Steve", roles: [:manager]>
# 構造体はあたかもマップのように更新することができます:

iex> steve = %Example.User{name: "Steve"}
#Example.User<name: "Steve", roles: [...], ...>
iex> sean = %{steve | name: "Sean"}
#Example.User<name: "Sean", roles: [...], ...>

構造体はマップのように更新できるのね...

パターンマッチングが未だに慣れない...

最も重要なことですが、構造体はマップに対してマッチすることができます:

iex> %{name: "Sean"} = sean
#Example.User<name: "Sean", roles: [...], ...>

Elixir 1.8以降、構造体にカスタムイントロスペクション機能が追加されました。

かすたむいんとろすぺくしょん...

カスタムイントロスペクションがどのように使われるのかを理解するため、 sean の中身を見てみましょう。

iex> inspect(sean)
"#Example.User<name: \"Sean\", roles: [...], ...>"

この例では全てのフィールドが出力対象になっていますが、出力したくない項目がある場合、どのようにしたら良いでしょうか? この場合、 @derive を利用することで実現することができます! roles を出力から除外したい場合、以下のように記述します。

defmodule Example.User do
  @derive {Inspect, only: [:name]}
  defstruct name: nil, roles: []
end
# 注記: @derive {Inspect, except: [:roles]} でも実現することができます。

モジュールを更新したら、 iex で確認してみましょう。

iex> sean = %Example.User{name: "Sean"}
#Example.User<name: "Sean", ...>
iex> inspect(sean)
"#Example.User<name: \"Sean\", ...>"
# roles が出力から除外されました!

モジュールの構造体から値を絞って出力するってことなんすかね

クラスのインスタンス変数のアクセス権的な扱いみたいになってるのでしょうか...?

いやモジュールを直で叩いたら構造体の全部の値出てくるから違うか(普段からinspectでモジュールを呼べってことなのか)

コンポジション(Composition)

さて、モジュールと構造体の作り方がわかったので、コンポジションを用いてモジュールや構造体に既存の機能を追加する方法を学びましょう。 Elixir は他のモジュールと連携する様々な方法を用意しています。

他のモジュールと連携

alias

モジュール名をエイリアスすることができます。 Elixir のコードでは頻繁に使われます:

defmodule Sayings.Greetings do
  def basic(name), do: "Hi, #{name}"
end

defmodule Example do
  alias Sayings.Greetings

  def greeting(name), do: Greetings.basic(name)
end

これを使うことでモジュール間でメソッドのやり取り(継承に近い?)感じなんですね

alias を使わない場合

defmodule Example do
  def greeting(name), do: Sayings.Greetings.basic(name)
end

2つのエイリアス間で衝突があったり、全体を別名でエイリアスしたい場合には、 :as オプションを使います:

defmodule Example do
  alias Sayings.Greetings, as: Hi

  def print_message(name), do: Hi.basic(name)
end

扱いやすい名前で扱えるのはありがたいですね

複数のモジュールを一度にエイリアスすることも可能です:

defmodule Example do
  alias Sayings.{Greetings, Farewells}
end

便利!

import

モジュールをエイリアスするよりも、関数を取り込みたいという場合には、 import を使います:

iex> last([1, 2, 3])
** (CompileError) iex:9: undefined function last/1
iex> import List
nil
iex> last([1, 2, 3])
3

あっ普通はimportつかうんだ..

フィルタリング

デフォルトでは全ての関数とマクロが取り込まれますが、 :only や :except オプションを使うことでフィルタすることができます。

まあ全部使うわけじゃないですしね

pythonとかでよく見かける気がします

特定の関数やマクロを取り込むには、名前/アリティのペアを :only や :except に渡す必要があります。 last/1 で最後の関数のみを取り込んでみましょう:

iex> import List, only: [last: 1]
iex> first([1, 2, 3])
** (CompileError) iex:13: undefined function first/1
iex> last([1, 2, 3])
3

last/1 で指定された関数以外を全て取り込むには、同じ関数で試してみましょう:

iex> import List, except: [last: 1]
nil
iex> first([1, 2, 3])
1
iex> last([1, 2, 3])
** (CompileError) iex:3: undefined function last/1

名前とアリティのペアを渡すのってアリティ指定するのちょっとだるいですね

名前/アリティのペアに加えて、 :functions と :macros という2つの特別なアトムもあります。これらはそれぞれ関数とマクロのみを取り込みます:

import List, only: :functions
import List, only: :macros

どうでもいいけど:macrosってマクロスっぽい

require

他のモジュールのマクロを使用することをElixirに伝えるために require を使うことができます。 import とのわずかな違いは、関数ではなくマクロを使用可能とすることです。

defmodule Example do
  require SuperMacros

  SuperMacros.do_stuff
end

マクロを呼び出すのが require

まだロードされていないマクロを呼びだそうとすると、Elixir はエラーを発生させます。

それはそう

use

use マクロを用いることで他のモジュールを利用して現在のモジュールの定義を変更することができます。 コード上で use を呼び出すと、実際には提供されたモジュールに定義されている using/1 コールバックを呼び出します。 using/1 マクロの結果はモジュールの定義の一部になります。 この動作に対する理解を深めるために簡単な例を見ましょう:

ほかのモジュールを利用して現在のモジュールの定義を変更する...?

defmodule Hello do
  defmacro __using__(_opts) do
    quote do
      def hello(name), do: "Hi, #{name}"
    end
  end
end

マクロについても多少調べたほうがええんかな

とりあえずuseで呼び出すと__using__が呼び出されるって感じなんですね

__using__って名前Pythonに似てる気がしますね

ここでは hello/1 関数を定義する using/1 コールバックを定義した Hello モジュールを作りました。 この新しいコードを試すために新しいモジュールを作ります:

defmodule Example do
  use Hello
end

IExでこのコードを試して見ると Example モジュールで hello/1 を使えるのがわかります。

iex> Example.hello("Sean")
"Hi, Sean"

使えるようになる...???

defmacro __using__ が走ってhelloメソッドが生えてるってことかな

ここで use が Hello の using/1 コールバックを呼び出して、結果のコードをモジュールに追加します。 基本的な例を見せたので、ここからはこのコードを変更して using/1 にオプションをサポートする方法を見てみましょう。 greeting オプションを追加します:

defmodule Hello do
  defmacro __using__(opts) do
    greeting = Keyword.get(opts, :greeting, "Hi")

    quote do
      def hello(name), do: unquote(greeting) <> ", " <> name
    end
  end
end

新しく作った greeting オプションを含むために Example モジュールを更新します:

defmodule Example do
  use Hello, greeting: "Hola"
end

IExで試して見ると挨拶が変わるのを確認できます。

iex> Example.hello("Sean")
"Hola, Sean"

これらは use がどうやって動作するのかを説明する簡単な例でしたが、これは Elixir のツールボックスで信じられないほどに強力なツールです。 Elixir を学び続けたら use をあっちこっちで見ることになるでしょう。かならず見ることになりそうな例をひとつあげれば、 use ExUnit.Case, async: true です。

うーん実際にどう使うと便利なのかイメージがなかなかつかないので、使われているところを見たら考えよう(思考停止)

注意: quote 、 alias 、 use 、 require はメタプログラミングで使用してたマクロです。

まとめ

  • defmoduleでモジュール化できる
  • モジュールで扱うattributesをもつことができる
  • defstructを使うことでモジュールの値を構造体(Structs)としてマップのように扱うことができる
  • モジュール間を色々つなぎこむことができる
  • aliasで別のモジュールのメソッドをコピー?というか扱えるようになる
  • 普通はaliasではなくimportでメソッドとマクロをごっそり取り込む
  • requireでモジュールのマクロを使用可能にする
  • useでモジュールでdefmacro __using__で定義されたマクロが走る