ZigでConceptsっぽいことやってみたい
動機
ZigにはGenericsが有るがC++17以前と同じく制約が課せない。
文法的に存在しないので同じことはできないが何とかならんか調べてみる。
Concepts
C++のテンプレートパラメータに制約を課すための仕組み。
さすがにこのレベルは無理だし望まない。
そもそも言語の方向性的に入ら無さそう。
template <typename T>
T square(T x) { return x * x; }
fn square(x: anytype) @TypeOf(x) { return x * x; }
単純にこういう関数で受け入れる値を限定してわかりやすくしたい。
「C++20コンセプト入門以前」を変換してみる
こちらを参考にしながらC++からZigへ変換してみる。
Day3
const std = @import("std");
pub fn twice(x: anytype) void {
const T = @TypeOf(x);
std.debug.print("{d}\n", .{ x * @as(T, 2) });
}
関数オーバーロードはそもそも無いので一気にDay3へ。
特に面白味もない関数テンプレートなんだけど組み込み関数がいい味出してる。
組み込み関数が型や型情報持ってこれるのがZigのいい点だと思う。
Day4
const std = @import("std");
pub fn twice(x: anytype) void {
const T = @TypeOf(x);
if (comptime std.meta.trait.isNumber(T)) {
std.debug.print("{d}\n", .{ x * @as(T, 2) });
}
else {
std.debug.print("*\n", .{});
}
}
一気にC++から乖離。
型を情報として扱えるので変な事(SFINAE)しなくても、そのまま書ける。
ただしif (comptime std.meta.trait.isNumber(T))
でcomptime
を付けないとtwice("x");
でコンパイルエラーになるので注意が必要。
ここは少しハマった。
Day6
const std = @import("std");
pub fn twice(x: anytype) void {
const T = @TypeOf(x);
if (comptime std.meta.trait.isNumber(T)) {
if (std.meta.trait.isIntegral(T) and std.meta.trait.isUnsignedInt(T)) {
std.debug.print("{d}u\n", .{x * @as(T, 2)});
}
else {
std.debug.print("{d}\n", .{x * @as(T, 2)});
}
} else {
std.debug.print("*\n", .{});
}
}
ZigにSFINAEは無いのでそのまま実装。
何でも受け入れる関数なので特に困らなかったが特定の条件を満たす型を受け入れるとかだと問題が起きる(そもそも制約掛けられないから)。
Concepts関係なくZigのcomptimeと組み込み関数、ジェネリクスの紹介として面白いコードな気がする。
本来やりたい事
さっきの例だと綺麗に書けたね、良かったね。で終わってしまうんだが本来はこういう事をしたかった。
// 描画可能コンセプト。
// メンバ関数draw()を持つことを要求する
template <class T>
concept Drawable = requires (T& x) {
x.draw(); // 型Tに要求する操作をセミコロン区切りで列挙する。
// ここでは、メンバ関数draw()を呼び出せることを要求している。
};
// テンプレートパラメータTをコンセプトDrawableで制約する。
// Drawableの要件を満たさない型が渡された場合は制約エラーとなる
template <Drawable T>
void f(T& x) {
x.draw();
}
struct Circle {
void draw() {}
};
struct Box {
void draw() {}
};
int main() {
Circle c;
f(c); // OK
Box b;
f(b); // OK
//int n = 0;
//f(n); // コンパイルエラー!
// 型intはDrawableコンセプトの要件を満たさず、
// draw()メンバ関数を持っていない
}
要するに受け入れる型がフリーダムすぎるからコチラで決めた条件に従ってもらおうという話。
Zigにconcepts
は無いので無理なんだけど似たことできないかな。
とりあえず組み込関数でコンパイルエラーを吐けるので前の例と合わせて書いてみる。
const std = @import("std");
fn isDrawable(comptime T: type) bool {
return std.meta.trait.hasFn("draw")(T);
}
fn f(x: anytype) void {
if (comptime !isDrawable(@TypeOf(x))) { @compileError("requires Drawable"); }
x.draw();
}
const Circle = struct {
pub fn draw(_: @This()) void {}
};
const Box = struct {
pub fn draw(_: @This()) void {}
};
pub fn main() !void {
const circle = Circle {};
const box = Box {};
f(circle);
f(box);
// const i: i32 = 0;
// f(i);
}
とりあえず書いてみた。
コンセプトは最初、テンプレートを使用した際のコンパイルエラーメッセージを読みやすいものにするため設計された。
という目的はコレもでも達成できているのか?
コンパイルエラーが分かりやすいかは@compileError
の文面によるが、まあ分かりやすくなるんじゃないかな。
この書き方の問題はisDrawable
のチェックを通り抜けたとしてもエラーが出る場合があることだよな。
適切に条件を列挙できるかという話。
余談だけどコレが通るのが納得いかない。
C++14の策定において、コンセプトの複雑さを回避して必要最小限の機能のみをまとめた「軽量コンセプト (Concept Lite)」という機能を入れることが計画された。これは、C++11で導入されたconstexprを使用し、bool型の定数式として制約を定義し、requires節にbool型定数式を指定して制約するというものだった。
書いた内容と一番近いのはこの方式か?
入ってくる型自体は早期にチェックして弾けるし、条件を適切に列挙できるなら問題なく使えはする。
あと、普通のプログラムなのでZig書いてれば読める。
まとめ
入ってくる型の制約という意味では弱いが一応、それっぽいのは書ける。
そもそもC++とZigでは状況が違うので必要となるconcepts
の要件も違う。
C++だと複雑怪奇なエラーを吐かれるがZigだと割と素直な内容だしSFINAEで頑張るみたいな状況も無いので素直に比較するのが失敗だったような。
とはいえ何かしらGenericsに制限をかけたいなとは思う。
Issue眺めてるとあんまり入る気はしないけど