LoginSignup
15
4

More than 1 year has passed since last update.

Elixirのforもタダの関数(だが、ただならぬ関数)

Last updated at Posted at 2018-10-07

fukuoka.ex代表のpiacereです
今回もご覧いただいて、ありがとうございます:bow:

ifが関数であることを調べましたが、今度は、forが関数であることを調べてみましょう

Elixirのforが関数であることを確認する

iexを起動して、普通のforの動きを確認してください

iex> for x <- [ 1, 3, 5 ], do: x + 2
[3, 5, 7]

まずforが、リストを返すものであることを、ここで初めて知る方もいたのでは無いでしょうか?

つまり、本質的には、Enum.mapと変わらない、ということです

逆に、他言語でのforとは、だいぶノリが異なることも、ここから分かります

では、ifのときと同じく、「do:」以降を消してみましょう(ちなみに、ここからの流れは、ifのときの調査内容を最後から辿ったら、省略できる訳ですが、せっかくなので、イチイチ調べてみます)

iex> for x <- [ 1, 3, 5 ]
** (CompileError) iex: missing :do option in "for"

ifのときは、「undefined function」と出てきましたが、直接、「do:」が無い、というエラーメッセージが返ってきました

まぁ、ひとまず、ifと同じく、カッコで囲んで実行したり、「do:」以外の第2引数を渡したら、エラーメッセージは同じでした

iex> for( x <- [ 1, 3, 5 ] )
** (CompileError) iex: missing :do option in "for"
iex> for( x <- [ 1, 3, 5 ], "hoge" )
** (CompileError) iex: missing :do option in "for"

「do:」を渡してみましょう

iex> for( x <- [ 1, 3, 5 ], do: x + 2 )
[3, 5, 7]

冒頭のforと同じことができるようになりました

アリティを指定して確認してみる

forもアリティを使って、引数の個数を指定して、直接、調べてみましょう

iex> &for/1
** (CompileError) iex: missing :do option in "for"
    (elixir) src/elixir_fn.erl:14: anonymous fn/4 in :elixir_fn.expand/3
    (stdlib) lists.erl:1354: :lists.mapfoldl/3
    (elixir) src/elixir_fn.erl:19: :elixir_fn.expand/3
iex> &for/2
** (CompileError) iex: missing :do option in "for"
    (elixir) src/elixir_fn.erl:14: anonymous fn/4 in :elixir_fn.expand/3
    (stdlib) lists.erl:1354: :lists.mapfoldl/3
    (elixir) src/elixir_fn.erl:19: :elixir_fn.expand/3
iex> &for/3
** (CompileError) iex: missing :do option in "for"
    (elixir) src/elixir_fn.erl:14: anonymous fn/4 in :elixir_fn.expand/3
    (stdlib) lists.erl:1354: :lists.mapfoldl/3
    (elixir) src/elixir_fn.erl:19: :elixir_fn.expand/3

ifと異なり、引数が2つでは無いし、3つになっても、同じエラーメッセージでした…

では、0個だと、どうでしょう?

iex> &for/0
** (CompileError) iex:54: undefined function for/0

0個は、存在しないようです

それでは、4個だと、どうでしょう?

iex> &for/4
** (CompileError) iex: missing :do option in "for"
    (elixir) src/elixir_fn.erl:14: anonymous fn/4 in :elixir_fn.expand/3
    (stdlib) lists.erl:1354: :lists.mapfoldl/3
    (elixir) src/elixir_fn.erl:19: :elixir_fn.expand/3

なるほど、どうやらforは、可変長の引数であるようです

forの仕様を知っていると…

実は、Elixirのforの仕様を知っていると、上記は調べなくても、類推できたのです

forは、たとえば、以下のような記述ができます

iex> for x <- [ 1, 3, 5 ], y <- [ 2, 4, 6 ], do: x + y
[3, 5, 7, 5, 7, 9, 7, 9, 11]

こんな感じで、2つ以上のリスト分解の組み合わせをループできるんですねー

更にforは、こんな条件判定も書けます

iex> for x <- [ 1, 3, 5 ], x < 4, do: x
[1, 3]

複数リスト分解と、複数条件判定を組み合わせると、こんな書き方もできます

iex> for x <- [ 1, 3, 5 ], y <- [ 2, 4, 6 ], x < 4, y < 5, do: x + y
[3, 5, 5, 7]

リスト分解と、条件判定は、特に順番を意識せずにも書けるので、こんな書き方だって、できます

for x <- [ 1, 3, 5 ], x < 4, y <- [ 2, 4, 6 ], y < 5, do: x + y
[3, 5, 5, 7]

つまり、「do:」が最後にあれば、幾つでもリストを並べられるし、条件判定も複数書けるのが、Elixirのforなのです

ここで、「do:」は、if同様、単独で成り立つ構文では無く、暗黙のキーワードリストであることが推測できます

iex> for( x <- [ 1, 3, 5 ], [ do: x + 2 ] )
[3, 5, 7]

実行できました

ダメ元でマップを試すと、やはりダメでした

iex> for( x <- [ 1, 3, 5 ], %{ do: x + 2 } )
** (CompileError) iex: missing :do option in "for"

forをiex内ヘルプで確認してみる

最後にタネ明かしですが、iex内で、「h ~」とすると、forの正体が確認できます(ifの経緯を知っていれば、ここから始めればOKです)

iex> h for
* defmacro for(args)

Comprehensions allow you to quickly build a data structure from
an enumerable or a bitstring.
 …

forは、defmacroで定義されており、引数が可変長であることが明かされています

forはKernel.SpecialFormsモジュールの関数

for/nは、Kernelモジュールの関数では無く、Kernel.SpecialFormsモジュールの関数であることが、以下リファレンスで確認できます
https://hexdocs.pm/elixir/Kernel.SpecialForms.html#for/1

iex内ヘルプでも、Kernel.SpecialForms無のforと同一であることが確認できます

iex> h Kernel.SpecialForms.for
* defmacro for(args)

Comprehensions allow you to quickly build a data structure from
an enumerable or a bitstring.
 …

Kernel.SpecialFormsの他の関数を見てみると…

上記のリファレンスの左側を見ると、forだけで無い、Kernel.SpecialFormsモジュールの関数がリストされていますが、その中には、「 %{}」や「&」、「fn」、「quote」といった、およそ関数で実装されているようには見えない、特殊な構文が並んでいます

こういったところから、Elixirの内部実装の一端が、少しだけ垣間見えたのでは無いでしょうか?

さて最後に、モジュール名も分かったところで、Github上のElixir本体のソースコードを少しだけ覗いてみましょう
https://github.com/elixir-lang/elixir/blob/master/lib/elixir/lib/kernel/special_forms.ex

defmodule Kernel.SpecialForms do
  @moduledoc """
  Special forms are the basic building blocks of Elixir, and therefore
  cannot be overridden by the developer.
  We define them in this module. Some of these forms are lexical (like
  `alias/2`, `case/2`, etc). The macros `{}/1` and `<<>>/1` are also special
  forms used to define tuple and binary data structures respectively.
  This module also documents macros that return information about Elixir's
  compilation environment, such as (`__ENV__/0`, `__MODULE__/0`, `__DIR__/0` and `__CALLER__/0`).
  Finally, it also documents two special forms, `__block__/1` and
  `__aliases__/1`, which are not intended to be called directly by the
  developer but they appear in quoted contents since they are essential
  in Elixir's constructs.
  """

  defmacrop error!(args) do
    quote do
      _ = unquote(args)

      message =
        "Elixir's special forms are expanded by the compiler and must not be invoked directly"

      :erlang.error(RuntimeError.exception(message))
    end
  end
 …

forの定義を見てみます

1394:  defmacro for(args), do: error!([args])

ポイントは、unquoteが、幾つもの関数パターンマッチで構成されている点で、これはサクッと読みこなすことはできません

もし、いつか機会があれば、Elixirカーネルハックで再びお会いしましょう:wink:

p.s.「いいね」よろしくお願いします

よろしければ、ページ左上の image.pngimage.png のクリックをお願いしますー:bow:

15
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
4