普通のプログラミング言語の普通の関数は、実行時に決まる値を受け取ることができます。
# 疑似コード
def foo(x: int, y: int) = print(x + y)
x = parseInt(readLine())
# x の値は実行時に決まる
foo(x + 4, 2 * x) # x + 4 や 2 * x の値も実行時に決まる
これに対して、「コンパイル時に決まる値しか受け付けない引数」を表現できると便利な場面があるのではないかと思うことが最近ありました。
動機:SIMD用の組み込み関数
例えば、一部のSIMD命令は即値(コンパイル時定数)を引数に受け取ります。これに対応する組み込み関数もコンパイル時定数を受け取れる必要があります。例えば、SSE2で整数ベクトルの特定の位置の値を置き換える pinstrw
命令は「挿入する位置」を即値として受け取り、これに対応する組み込み関数 _mm_insert_epi16
も「位置」を表す第3引数がコンパイル時定数でなければなりません。
#include <emmintrin.h>
__m128i a;
int value;
int i;
void foo(void)
{
a = _mm_insert_epi16(a, value, 3); // 第3引数がコンパイル時定数(3)なのでOK
a = _mm_insert_epi16(a, value, i); // 第3引数がコンパイル時定数ではないのでコンパイルエラー
}
Haskell処理系のGHCにもSIMD命令に対応する組み込み関数があります。pinstrw
に対応するのは insertInt16X8#
です。
insertInt16X8# :: Int16X8# -> Int16# -> Int# -> Int16X8#
これも、位置を表す第3引数がコンパイル時定数である必要があります。
これらの組み込み関数を直接に使う分にはいいんですが、「組み込み関数は低レベルすぎる」と言ってラップしようとしたら問題が発生します。ラッパーのコード生成ができないのです。
-- ラッパー
insertInt16X8 :: Int16X8 -> Int16 -> Int -> Int16X8
insertInt16X8 (Int16X8 v) (I16# x) (I# pos) = Int16X8 (insertInt16X8# v x pos)
-- pos がコンパイル時定数ではないのでコード生成できない!
関数に __attribute__((always_inline))
みたいな属性をつけられたら良いのかもしれませんが、Haskellにはそういう属性はありません。
なので、「コンパイル時定数であることを要求する引数」を表現できるようにして、そういう引数を受け取る関数は常にインライン化したり、特殊化したりするようにしたら良いのではないか、と思いました。
C++のテンプレート引数
C++だとテンプレート引数として値を受け渡すことで「コンパイル時定数であることを要求する引数」を表現できます。むしろ、普通のテンプレート引数は「型をコンパイル時定数として渡している」と思うことができるかもしれません。
Haskellでエミュレートする
Haskellで「コンパイル時定数」に近いのは「型レベルの何か」です。もちろん、Haskellには多相再帰があって実行時に型を捻出できる(「プログラム中に現れる型の全体」がコンパイル時に確定しない)ので、型レベル引数だからといってコンパイル時定数というわけではありませんが、似たことはできそうです。例えば、型クラスを使ってこんな感じです:
class SIMDPositionX8 (pos :: Natural) where
insertInt16X8Proxy# :: Int16X8# -> Int16# -> Proxy# pos -> Int16X8#
instance SIMDPositionX8 0 where
insertInt16X8Proxy# v x _ = insertInt16X8# v x 0# -- 位置を表す引数がコンパイル時定数なのでコード生成できる
instance SIMDPositionX8 1 where
insertInt16X8Proxy# v x _ = insertInt16X8# v x 1#
-- 中略
instance SIMDPositionX8 7 where
insertInt16X8Proxy# v x _ = insertInt16X8# v x 7#
-- ラッパー
insertInt16X8 :: Int16X8 -> Int16 -> forall pos -> SIMDPositionX8 pos => Int16X8
insertInt16X8 (Int16X8 v) (I16# x) pos = Int16X8 (insertInt16X8Proxy# v x (proxy# :: Proxy# pos))
-- 使う側は insertInt16X8 v x 3 という風に使える
まあ、「位置」が1個くらいなら型クラスを自力で書けますが、shuffle命令のラッパーは全ての場合を自力で書くのは難しそうです。なので、compiler magicで型クラスのインスタンスを用意するのがよさそうです。
アイディア:実行時に受け渡ししづらい値もコンパイル時なら渡せるのでは
例えば、新しくシステムプログラミング言語を作りたいとしましょう。そういう言語では、多倍長整数や(関数の)クロージャーのような型は組み込み型として提供するのは難しそうです。
しかし、「コンパイル時に確定して、実行時のコード生成を伴わない」前提であれば、多倍長整数やクロージャーのようなリッチな型を提供できるのではないでしょうか。
依存型での引数の見方
依存型では、項に依存した型を持つ関数を記述できます。依存型言語では、関数の引数はいくつかの観点で分類できます(用語はうろ覚えで書いているので、一般的でない用法だったらすみません):
- dependentか、non-dependentか
- dependentであれば、以降の型はその引数に依存できる。non-dependentであれば、以降の型はその引数に依存できない。
- irrelevant (erased)か、relevant (retained)か
- irrelevantであれば、関数の値はその引数に実行時に依存できない。relevantであれば、関数の値はその引数に実行時に依存できない。
- visibleか、invisible (inferred)か
- visibleであれば、関数を呼び出す側はその引数の明示的に与える必要がある(普通の引数)。invisibleであれば、型推論等の機構でその引数の値が決まる。
この分類に「コンパイル時定数である引数」を追加するとしたら、irrelevant/relevantのところに第3の種類として追加することになるでしょうか。irrelevant/relevant/specializedみたいな感じで。
プログラミング言語のconst
プログラミング言語では、「変数の定義後に値を変更できない」程度の意味でconst (constant) という用語を使うことが多いです。しかし、関数型プログラミング言語愛好家の私としては、「変数の定義後に値を変更できない」のは当たり前です。constというのはもっと強い性質に使うべきではないでしょうか。
そう、「コンパイル時に値が決まっている定数」です。私が新しくプログラミング言語を作るとしたら、「constはコンパイル時定数を表す」とする……かもしれません。
Zigのcomptime
【12月20日 追記】
https://ziglang.org/documentation/0.13.0/#Compile-Time-Parameters
Zigのcomptimeキーワードは関数の引数にも使えるようです。コンパイル時限定で使える comptime_int
と comptime_float
という型もあるようです。というわけで、Zigのcomptimeは私の考えていたことをすでに実現していたようです。