Closed11

ZigでConceptsっぽいことやってみたい

hasturhastur

動機

ZigにはGenericsが有るがC++17以前と同じく制約が課せない。
文法的に存在しないので同じことはできないが何とかならんか調べてみる。

hasturhastur

Concepts

C++のテンプレートパラメータに制約を課すための仕組み。
https://cpprefjp.github.io/reference/concepts.html

さすがにこのレベルは無理だし望まない。
そもそも言語の方向性的に入ら無さそう。

template <typename T>
T square(T x) { return x * x; }
fn square(x: anytype) @TypeOf(x) { return x * x; }

単純にこういう関数で受け入れる値を限定してわかりやすくしたい。

hasturhastur

「C++20コンセプト入門以前」を変換してみる

https://qiita.com/yohhoy/items/f3d90c598348817cd29c
こちらを参考にしながら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と組み込み関数、ジェネリクスの紹介として面白いコードな気がする。

hasturhastur

個人的にZigの好きなポイントがこの辺に現れてる。
C++でconstexprが必要になった理由とかもcomptimeで解消されているし、コンパイル時に何かやる処理が素直に書けるのがえらい。
(さらにプリプロセッサマクロも排除できる)

hasturhastur

本来やりたい事

さっきの例だと綺麗に書けたね、良かったね。で終わってしまうんだが本来はこういう事をしたかった。

// 描画可能コンセプト。
// メンバ関数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()メンバ関数を持っていない
}

https://cpprefjp.github.io/lang/cpp20/concepts.html

要するに受け入れる型がフリーダムすぎるからコチラで決めた条件に従ってもらおうという話。

hasturhastur

Zigにconceptsは無いので無理なんだけど似たことできないかな。
とりあえず組み込関数でコンパイルエラーを吐けるので前の例と合わせて書いてみる。

hasturhastur
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);
}

とりあえず書いてみた。

hasturhastur

コンセプトは最初、テンプレートを使用した際のコンパイルエラーメッセージを読みやすいものにするため設計された。

という目的はコレもでも達成できているのか?
コンパイルエラーが分かりやすいかは@compileErrorの文面によるが、まあ分かりやすくなるんじゃないかな。

この書き方の問題はisDrawableのチェックを通り抜けたとしてもエラーが出る場合があることだよな。
適切に条件を列挙できるかという話。

余談だけどコレが通るのが納得いかない。
https://godbolt.org/z/eq351dxde

hasturhastur

C++14の策定において、コンセプトの複雑さを回避して必要最小限の機能のみをまとめた「軽量コンセプト (Concept Lite)」という機能を入れることが計画された。これは、C++11で導入されたconstexprを使用し、bool型の定数式として制約を定義し、requires節にbool型定数式を指定して制約するというものだった。

書いた内容と一番近いのはこの方式か?

入ってくる型自体は早期にチェックして弾けるし、条件を適切に列挙できるなら問題なく使えはする。
あと、普通のプログラムなのでZig書いてれば読める。

hasturhastur

まとめ

入ってくる型の制約という意味では弱いが一応、それっぽいのは書ける。

そもそもC++とZigでは状況が違うので必要となるconceptsの要件も違う。
C++だと複雑怪奇なエラーを吐かれるがZigだと割と素直な内容だしSFINAEで頑張るみたいな状況も無いので素直に比較するのが失敗だったような。

とはいえ何かしらGenericsに制限をかけたいなとは思う。
Issue眺めてるとあんまり入る気はしないけど

このスクラップは2022/07/19にクローズされました