こんな構文の言語どうよ
Concepts
型のふるまいを規定したもの。
ある型がコンセプトに規定された関数をすべて実装している場合に、その型はコンセプトを満たしているものとしてふるまう。ただし、戻り値の型を含めてシグネチャに整合性がない場合、その型はコンセプトを満たしていないものとして扱われる。
// at関数を持つAt<T>コンセプトを定義。
concept At<T> {
at(self, Size) -> T | Nothing
}
以下のように実装する。
type Vec<T> = {
slice: [T]
len: Size
}
// Vec<T>にat関数を実装。
// これにより、Vec<T>は暗黙的にAt<T>コンセプトを満たしたものとみなされる。
fn at<T>(vec: Vec<T>, index: Size) -> T | Nothing {
if index < vec.len {
vec.slice[index]
} else {
nothing
}
}
関数等の引数では、明示的な型の代わりにコンセプトで型制約を設定できる。
// At<T>を実装していればどんな型でも受け取れる。
fn first<T>(container: At<T>) -> T | Nothing {
container.at(0)
}
Inline concept
局所的にコンセプトを利用したい場合は、インラインでコンセプトを定義することで名前空間を汚染せずに型制約を設定できる。
fn first<C, E>(container: C) -> E | Nothing
where
C: concept { at(C, Size) -> E | Nothing }
{
container.at(0)
}
Multiple dispatch
関数のオーバーロードが可能であり、関数呼び出しはコンパイル時に解決される。
関数名とシグネチャから実行可能な関数をディスパッチするが、複数の関数が実行可能であればより制約の強いものが選択される。
以下のケースでは、引数の型が同じかつEqコンセプトを満たす場合は1つ目の関数がディスパッチされ、そうでない場合は2つ目の関数がディスパッチされる。
fn assert_eq<T: Eq>(a: T, b: T) -> () {
if a == b {
()
} else {
panic("Assertion failed: values not equal")
}
}
fn assert_eq(a, b) -> ! {
panic("Assertion failed: cannot compare for equality")
}
assert_eq(123, 123) // Assertion passed
assert_eq(42, "42") // Assertion failed
Type Constraints
関数のシグネチャは省略することができる。
シグネチャを省略した場合は、関数の実装を満たす最小の型制約がコンパイラによって付与される。
fn sum(numbers) {
numbers.fold(0, fn(acc, x) -> acc + x)
}
上記の関数は、コンパイル時に以下のような型制約が付与される。
fn sum<A, B, C, D>(numbers: A) -> B
where
A: concept { fold(A, C, fn(D, D) -> D) -> B }
B: Any
C: Num
D: Add
{
numbers.fold(0, fn(acc, x) -> acc + x)
}
Operator overloading
各種演算子のオーバーロードをサポートし、各演算子は自身に対応するコンセプトを呼び出すことで実現している。
selfは自身を表す型キーワードであり、コンセプト内でのみ使用される。
Addコンセプト
concept Add {
add(self, self) -> self
}
fn (+)<T: Add>(lhs: T, rhs: T) -> T {
add(lhs, rhs)
}
Eqコンセプト
concept Eq {
eq(self, self) -> Bool
}
fn (==)<T: Eq>(lhs: T, rhs: T) -> Bool {
eq(lhs, rhs)
}
fn (!=)<T: Eq>(lhs: T, rhs: T) -> Bool {
!eq(lhs, rhs)
}
Ordコンセプト
また、コンセプトは、異なるコンセプトの部分集合として定義できる。
concept Ord: Eq {
cmp(self, self) -> Ordering
}
fn (<)<T: Ord>(lhs: T, rhs: T) -> Bool {
cmp(lhs, rhs) == Ordering.Less
}
fn (<=)<T: Ord>(lhs: T, rhs: T) -> Bool {
var order = cmp(lhs, rhs)
order == Ordering.Less ||
order == Ordering.Equal
}
fn (>)<T: Ord>(lhs: T, rhs: T) -> Bool {
cmp(lhs, rhs) == Ordering.Greater
}
fn (>=)<T: Ord>(lhs: T, rhs: T) -> Bool {
var order = cmp(lhs, rhs)
order == Ordering.Greater ||
order == Ordering.Equal
}
Indexコンセプト
配列のようなインデックスアクセスで使用する([])演算子。
concept Index<E> {
index(self, Size) -> E
}
fn ([])<T: Index<E>>(container: T, i: Size) -> E {
index(container, i)
}
ユーザーは演算子を定義できるが、あくまでもコンセプトに則ったものであり、演算子そのものを関数として定義することはできないため、以下はエラーとなる。
// 演算子そのもののオーバーロードはできない。
fn ([])(container: MyContainer, i: Size) -> MyElement { ... }
UFCS (Uniform Function Call Syntax)
統一関数呼び出し構文をサポートしている。
これにより、フリー関数をメソッドのように扱うことが可能になるほか、メソッドチェーンで書けるようになる。
例として、以下の2つの関数呼び出しでは同じ関数がディスパッチされる。
// 通常の関数呼び出し。
println(join(map(filter([1, 2, 3, 4], is_even), to_string), ", "))
// UFCSによる関数呼び出し。
[1, 2, 3, 4]
.filter(is_even)
.map(to_string)
.join(", ")
.println()
また、関数の第1引数がレシーバーとなる。
fn div(lhs, rhs) {
lhs / rhs
}
assert_eq(div(6, 3), 6.div(3))
Type constraint hierarchy
型制約には、すべての型のトップであるAny型制約と、すべての型のボトムであるNever型制約が存在する。
Any
最小の型制約として機能する。
以下の関数は、受け取った引数をそのまま返すだけの機能を持つ。このケースでは、引数に対していかなる操作も行っていないため、Anyが付与される。
Anyは関数を持たないコンセプトとして定義されている。
concept Any { }
fn id<T: Any>(value: T) -> T {
value
}
Anyコンセプトは関数を持たないため、すべての型が暗黙的にコンセプトを満たす。
このため、Anyはすべての型のスーパータイプとしてふるまう。
// 暗黙的にAnyを親として派生している。
concept Num { ... }
assert(Any >: Any)
assert(Any >: Num)
assert(Any >: Int)
assert(Any >: Never)
Never
最大の型制約として機能する。
必ずパニックして元の処理に戻らない関数や、到達し得ない分岐をコンパイラへ伝えるために使用する。
Never型は空のユニオン型のエイリアスとして定義されている。
type alias Never = |
fn error(message: String) -> Never {
panic(message)
}
Never型は空集合であるため、すべての型のサブタイプとしてふるまう。
assert(Never <: Any)
assert(Never <: Num)
assert(Never <: Int)
assert(Never <: Never)