ジェネリクス

型をパラメータとして受け取る汎用コードの書き方を学びます。


基本概念

NagiScript のジェネリクスはコンパイル時に単型化(monomorphization)されます。 Zig や Odin と同様に、 使用時には具体的な型に展開されます。

filename.ngs
// この関数は T が i32 なら fn(i32) i32 に、
// T が str なら fn(str) str に展開されます
fn identity<T>(x: T) T {
    x
}

val n = identity<i32>(42)    // 42
val s = identity<str>("hi")  // "hi"

型推論

型パラメータを省略すると推論されます:

filename.ngs
val n = identity(42)      // T = i32 と推論
val s = identity("hi")    // T = str と推論

複数の型パラメータ

filename.ngs
fn pair<A, B>(first: A, second: B) (A, B) {
    (first, second)
}

val p = pair<i32, str>(42, "hello")
// p = (42, "hello")

制約 (Constraints)

型クラス風の制約

filename.ngs
// Printable 型クラス(仮想的)
fn print_all<T: Printable>(items: List<T>) void {
    for item in items {
        item.print()
    }
}

構造的制約

filename.ngs
// T が .len() メソッドを持つことを要求(構造的制約)
fn length_of<T>(x: T) i32 {
    x.len()
}

val s_len = length_of("hello")  // 5
val v_len = length_of(List<i32> { 1, 2, 3 })  // 3

ジェネリクス構造体

filename.ngs
struct Stack<T> {
    items: List<T>
}

fn Stack<T>.push(self: Stack<T>, item: T) void {
    self.items.add(item)
}

fn Stack<T>.pop(self: Stack<T>) Option<T> {
    if self.items.is_empty() {
        Option.None
    } else {
        val last = self.items[self.items.len() - 1]
        self.items.remove(self.items.len() - 1)
        Option.Some(last)
    }
}

fn Stack<T>.peek(self: Stack<T>) Option<T> {
    if self.items.is_empty() {
        Option.None
    } else {
        Option.Some(self.items[self.items.len() - 1])
    }
}

// 使用例
var stack = Stack<i32> { items: List<i32> {} }
stack.push(10)
stack.push(20)
stack.push(30)

val top = stack.pop()  // Option.Some(30)

ジェネリクス列挙型

Result 型

filename.ngs
enum Result<T, E> {
    Ok(T)
    Err(E)
}

fn Result<T, E>.is_ok(self: Result<T, E>) bool {
    match self {
        Result.Ok(_) => true,
        Result.Err(_) => false
    }
}

fn Result<T, E>.unwrap(self: Result<T, E>) T {
    match self {
        Result.Ok(v) => v,
        Result.Err(_) => panic("unwrap on Err")
    }
}

Option 型

filename.ngs
enum Option<T> {
    Some(T)
    None
}

fn Option<T>.is_some(self: Option<T>) bool {
    match self {
        Option.Some(_) => true,
        Option.None => false
    }
}

fn Option<T>.unwrap_or(self: Option<T>, default: T) T {
    match self {
        Option.Some(v) => v,
        Option.None => default
    }
}

関数型のジェネリクス

filename.ngs
fn map<T, U>(list: List<T>, f: fn(T) U) List<U> {
    var result = List<U> {}
    for item in list {
        result.add(f(item))
    }
    result
}

val doubled = map(List<i32> { 1, 2, 3 }, fn(x: i32) i32 { x * 2 })
// doubled = [2, 4, 6]

val lengths = map(List<str> { "hello", "world" }, fn(s: str) i32 { s.len() })
// lengths = [5, 5]

型推論の仕組み

filename.ngs
// 型パラメータが明示的
val v1 = Vec2<i32> { x: 1, y: 2 }

// 型推論による省略
val v2 = Vec2 { x: 1, y: 2 }  // Vec2<i32> と推論

// 複数の型パラメータでは推論が難しい場合
val p = pair<i32, str>(42, "hello")  // 明示的に指定

モノモーフィゼーションの例

filename.ngs
// このコードは以下のように展開されます

// 元のコード
fn add<T: Numeric>(a: T, b: T) T {
    a + b
}

val x = add(10, 20)      // T = i32
val y = add(1.5, 2.5)    // T = f64

// 展開後(コンパイル時に生成される)
fn add_i32(a: i32, b: i32) i32 {
    a + b
}

fn add_f64(a: f64, b: f64) f64 {
    a + b
}

実践的な例: 汎用リスト

filename.ngs
struct List<T> {
    data: Rc<ListData<T>>
}

struct ListData<T> {
    items: RawSlice<T>
    len: i32
}

fn List<T>.push(self: List<T>, item: T) void {
    // Rc を fork してコピー-on-write を実現
    val new_data = self.data.fork()
    new_data.items.add(item)
    self.data = new_data
}

fn List<T>.get(self: List<T>, index: i32) T {
    self.data.items[index]
}

fn List<T>.len(self: List<T>) i32 {
    self.data.len
}

// 使用例
val numbers = List<i32> {}
numbers.push(10)
numbers.push(20)
numbers.push(30)

io.println(numbers.get(0))  // 10
io.println(numbers.len())   // 3