ZEAM開発ログ v.0.4.4 INT64判定をマクロで簡単に判定する

ZACKYこと山崎進です。

when 節の中で値がINT64の範囲の整数値かどうかを簡潔に判定する方法がわかりましたので，報告します。

[「ZEAM開発ログ 目次」はこちら](https://qiita.com/zacky1972/items/70593ab2b70d192813df)

# やりたいこと

[「ZEAM開発ログ v.0.4.0 型多相かつ型安全なNIFをC言語で書いてみる」](https://qiita.com/zacky1972/items/ddcbab5998d7864b3f18)で，型多相かつ型安全なコードにするために，次のように型検査していました。

```elixir
def add(a, b) when is_integer(a) and a <= @max_int and a >= @min_int
          and is_integer(b) and b <= @max_int and b >=@min_int do
  a + b
end
```

しかしいかにも冗長ですね。次のように書けるといいです。

```elixir
def add(a, b) when is_int64(a) and is_int64(b) do
  a + b
end
```

そこで次のように定義したとしましょう。

```elixir
def is_int64(value) do
  is_integer(value) and value <= @max_int and value >= @min_int
end
```

しかし，これでは when 節の中で使えません。次のようなエラーが出ます。

```
== Compilation error in file test/asm_test.exs ==
** (CompileError) test/asm_test.exs:4: cannot invoke remote function Asm.is_int64/1 inside guard
    (stdlib) lists.erl:1354: :lists.mapfoldl/3
    (elixir) expanding macro: Kernel.and/2
    test/asm_test.exs:4: Foo.add/2
    (elixir) lib/code.ex:677: Code.require_file/2
    (elixir) lib/kernel/parallel_compiler.ex:201: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/6
```

どうしたらいいでしょうか？

# マクロを使った解決

このような場合に使えるのが**マクロ**です。Elixir のマクロはプログラム中の構文要素を基にしてプログラムを生成できます。

さっそく紹介します。[GitHub の全体はこちらです。](https://github.com/zeam-vm/asm)

Elixir のコードは下記の通りです。

```elixir
defmodule Asm do
  use Constants

  @name :max_int
  @value 9_223_372_036_854_775_807

  @name :min_int
  @value -9_223_372_036_854_775_808

  @moduledoc """
  Asm aimed at implementing an inline assembler.

  Currently, it provides the following:

  * `is_int64` macro that can be used in `when` clauses to judge that a value is within INT64.
  * `max_int` is the constant value of maxium of INT64.
  * `min_int` is the constant value of minimum of INT64.
  """

  @doc """
  is_int64(value) returns true if the value is an integer, equals or is less than max_int and equals or is greater than min_int.
  """
  defmacro is_int64(value) do
    quote do
    	is_integer(unquote(value))
    	and unquote(value) <= unquote(Asm.max_int)
    	and unquote(value) >= unquote(Asm.min_int)
    end
  end

  def dummy(a), do: a
end
```

まず，[constants](https://hex.pm/packages/constants)を使用してモジュール間で定数`max_int`と`min_int`を共有できるようにしています。

マクロ本体はこの部分です。

```elixir
  defmacro is_int64(value) do
    quote do
    	is_integer(unquote(value))
    	and unquote(value) <= unquote(Asm.max_int)
    	and unquote(value) >= unquote(Asm.min_int)
    end
  end
```

`quote`と`unquote`が入り乱れて読みにくいのですが，文字列で書くとこんな感じと同じです。

```elixir
"is_integer(#{value}) and #{value} <= #{Asm.max_int} and #{value} >= #{Asm.min_int}"
```

要は `quote do ... end` 以下で指定されたコードを生成します。また，`unquote` の後のコードを評価した値をコードとして埋め込みます。 

なお，`dummy` 関数を入れているのは，関数が一つもないと `mix hex.publish` するときにエラーになるからです。

# 結果

[「ZEAM開発ログ v.0.4.0 型多相かつ型安全なNIFをC言語で書いてみる」](https://qiita.com/zacky1972/items/ddcbab5998d7864b3f18)で紹介したコードを次のように簡潔に書くことができるようになりました。

```elixir
defmodule NifLlvm do
  require Asm
  import Asm

  @on_load :load_nifs

  def load_nifs do
    :erlang.load_nif('./priv/libnifllvm', 0)
  end

  @moduledoc """
  Documentation for NifLlvm.
  """

  def main do
    IO.puts asm_1(1, 2)
    IO.puts asm_1(1.0, 2)
    IO.puts asm_1(1, 2.0)
    IO.puts asm_1(1.0, 2.0)
    IO.puts asm_1(Asm.max_int, 0)
    IO.puts asm_1(Asm.min_int, 0)
    try do
      IO.puts asm_1(Asm.max_int, 1)
    rescue
      error in [ArithmeticError] -> IO.puts "it needs BigNum!: #{Exception.message(error)}"
    end
    try do
      IO.puts asm_1(Asm.max_int + 1, 1)
    rescue
      error in [ArithmeticError] -> IO.puts "it needs BigNum!: #{Exception.message(error)}"
    end
  end

  def asm_1(a, b) do
    case {a, b} do
        {a, b} when is_int64(a) and is_int64(b)
          -> case asm_1_nif_ii(a, b) do
            x when is_integer(x) -> x
            :error -> raise ArithmeticError, message: "bad argument in arithmetic expression"
          end
        {a, b} when is_int64(a) and is_float(b) -> asm_1_nif_if(a, b)
        {a, b} when is_float(a) and is_int64(b) -> asm_1_nif_fi(a, b)
        {a, b} when is_float(a) and is_float(b) -> asm_1_nif_ff(a, b)
        _ -> raise ArithmeticError, message: "bad argument in arithmetic expression"
    end
  end

  def asm_1_nif_ii(a, b) when is_int64(a) and is_int64(b), do: raise "NIF asm_1_nif_ii/2 not implemented"
  def asm_1_nif_if(a, b) when is_int64(a) and is_float(b), do: raise "NIF asm_1_nif_if/2 not implemented"
  def asm_1_nif_fi(a, b) when is_float(a) and is_int64(b), do: raise "NIF asm_1_nif_fi/2 not implemented"
  def asm_1_nif_ff(a, b) when is_float(a) and is_float(b), do: raise "NIF asm_1_nif_ff/2 not implemented"

end
```

# Hex で公開しています！

このコードは Hex で公開しています。次のように使います。

mix.exs の

```elixir
  defp deps do
    [
      ...
    ]
  end
```

の ... のところに次の記述を足します。

```elixir
      {:asm, "~> 0.0.5"}
```

その後，`mix deps.get` を実行してから，`is_int64`, `max_int`, `min_int` を使いたいモジュールで次のようにします。

```elixir
defmodule Foo do
  require Asm  # これと
  import Asm   # この2行を追加する
  def add(a, b) when is_int64(a) and is_int64(b) do
    a + b
  end
end
```

次回は少し脱線してC言語とのインタフェース，とくに構造体とのやり取りを考える上で重要になってくるビット列について調査した[「ZEAM開発ログ v.0.4.5 ビット列について調べてみる」](https://qiita.com/zacky1972/items/939acbd6ff6580479253)です。お楽しみに！

:stars::stars::stars:<b>　お知らせ：Elixirもくもく会（リモート参加OK、入門トラック有）を9月28日に開催します　</b>:stars::stars::stars:

[<b>「fukuoka.ex#14：Elixir／Phoenixもくもく会～入門もあるよ」</b>](https://fukuokaex.connpass.com/event/100659/)を2018年9月28日金曜日に開催します

前回は，ゲリラ的に募った「Zoomによるリモート参加」を，今回から正式に受け付けるようになりましたので，福岡以外の首都圏や地方からでも参加できます（申し込みいただいたら、追ってZoom URLをconnpassメールでお送りします）

また，これまではElixir／Phoenix経験者を対象とした，もくもく会オンリーでしたが，今回から，入門者トラックも併設し，fukuoka.exアドバイザーズ／キャストに質問できるようにアップグレードしました

私，山崎も参加します！ この記事の延長線上のものを作ろうと思っています。

お申込みはコチラから
https://fukuokaex.connpass.com/event/100659/
[![image.png](https://qiita-image-store.s3.amazonaws.com/0/155423/3f0fbb56-e5e8-2c4d-7c62-1b0b311d6d02.png)](https://fukuokaex.connpass.com/event/100659/)