ZACKYこと山崎進です。
when 節の中で値がINT64の範囲の整数値かどうかを簡潔に判定する方法がわかりましたので,報告します。
やりたいこと
「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_int
と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
quote
とunquote
が入り乱れて読みにくいのですが,文字列で書くとこんな感じと同じです。
"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 ビット列について調べてみる」です。お楽しみに!
お知らせ:Elixirもくもく会(リモート参加OK、入門トラック有)を9月28日に開催します
「fukuoka.ex#14:Elixir/Phoenixもくもく会~入門もあるよ」を2018年9月28日金曜日に開催します
前回は,ゲリラ的に募った「Zoomによるリモート参加」を,今回から正式に受け付けるようになりましたので,福岡以外の首都圏や地方からでも参加できます(申し込みいただいたら、追ってZoom URLをconnpassメールでお送りします)
また,これまではElixir/Phoenix経験者を対象とした,もくもく会オンリーでしたが,今回から,入門者トラックも併設し,fukuoka.exアドバイザーズ/キャストに質問できるようにアップグレードしました
私,山崎も参加します! この記事の延長線上のものを作ろうと思っています。
お申込みはコチラから
https://fukuokaex.connpass.com/event/100659/