Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

ZACKYこと山崎進です。

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

「ZEAM開発ログ 目次」はこちら

やりたいこと

「ZEAM開発ログ v.0.4.0 型多相かつ型安全なNIFをC言語で書いてみる」で,型多相かつ型安全なコードにするために,次のように型検査していました。

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

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

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

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

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 の全体はこちらです。

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を使用してモジュール間で定数max_intmin_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

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

"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言語で書いてみる」で紹介したコードを次のように簡潔に書くことができるようになりました。

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 の

  defp deps do
    [
      ...
    ]
  end

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

      {:asm, "~> 0.0.5"}

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

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 ビット列について調べてみる」です。お楽しみに!

:stars::stars::stars: お知らせ:Elixirもくもく会(リモート参加OK、入門トラック有)を9月28日に開催します :stars::stars::stars:

「fukuoka.ex#14:Elixir/Phoenixもくもく会~入門もあるよ」を2018年9月28日金曜日に開催します

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

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

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

お申込みはコチラから
https://fukuokaex.connpass.com/event/100659/
image.png

zacky1972
北九州市立大学 国際環境工学部 准教授 / ナッジ社会実装研究センター センター長 / Elixir 推し / fukuoka.ex / Pelemay / ZEAM / Personal Vision Co-Creator / KK-SHiFT / 技術相談,共同研究依頼,進路相談,適職診断など,随時受付ます
https://zacky1972.github.io
fukuokaex
エンジニア/企業向けにElixirプロダクト開発・SI案件開発を支援する福岡のコミュニティ
https://fukuokaex.fun/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away