Skip to content

型引数

型引数を使うと、要素の型に依らない関数やデータを1つの定義で書けます。宣言も適用も [ ] で書き、呼び出し側では書かずに引数から推論します。

type Pair[A, B](first: A, second: B)
fn swap[A, B](p: Pair[A, B]) -> Pair[B, A] {
Pair(first: p.second, second: p.first)
}
fn main() {
p = Pair(first: 1, second: "one")
dbg(swap(p)) # Pair(first: "one", second: 1)
dbg(swap(Pair(first: True, second: 2.5))) # Pair(first: 2.5, second: True)
}

swap[A, B] のように名前の後に型引数を宣言します。fn、type、enum、effect に書けます。型に適用するときも [ ] を使い、List[Int]、Pair[String, List[Int]] のようになります。swap(p) の A と B は p の型から決まります。

enum Tree[A] {
Leaf
Node(Tree[A], A, Tree[A])
}
fn size[A](t: Tree[A]) -> Int {
t match {
Leaf -> 0
Node(l, _, r) -> size(l) + 1 + size(r)
}
}
fn main() {
t = Node(Node(Leaf, 1, Leaf), 2, Leaf)
dbg(size(t)) # 2
}

型引数の名前は A や T のような大文字名のほか、Key や Item のような型名でも書けます。意味のある名前を付けたいときは型名を使います。

fn group_first[Key, Value](pairs: List[(Key, Value)]) -> Option[Key] {
pairs match {
[(k, _), ..] -> Some(k)
[] -> None
}
}
fn main() {
dbg(group_first([("a", 1), ("b", 2)])) # Some("a")
}

型名で書いた型引数が外側の同じ名前の型を隠すと、警告が出ます。

type Item(name: String)
fn first[Item](xs: List[Item]) -> Option[Item] { # type parameter `Item` shadows an outer definition
List.head(xs)
}
fn main() {
dbg(first([1, 2])) # Some(1)
}

型は Hindley-Milner 方式で推論します。どの型でも使える多相な定義になるのは、トップレベルの関数と const だけです。

fn wrap[A](x: A) {
[x] # 戻り値の型 List[A] は推論する
}
const EMPTY = []
fn main() {
dbg(wrap(1)) # [1]
dbg(wrap("s")) # ["s"]
ints: List[Int] = EMPTY
strings: List[String] = EMPTY
dbg(ints) # []
}

トップレベルの fn は引数の型を省けないので、多相な関数は型引数を宣言して書きます。戻り値の型は推論に任せられます。

ブロックの中の束縛は多相になりません。無名関数を変数に入れて、別の型で2回使うとエラーです。

fn main() {
id = fn(x) { x }
a = id(1)
b = id("s")
}
type mismatch: expected `Int`, found `String`

最初の id(1) で x の型が Int に決まります。いろいろな型で使いたい関数はトップレベルの fn にします。

型引数に [A: Ord] の形で制約を書けます。制約は Trait の名前で、[A: Ord + Show] のように + で並べます。

fn max_of[A: Ord](a: A, b: A) -> A {
if a > b { a } else { b }
}

Prelude には Eq、Ord、Show、Hash の4つの Trait があります。== は Eq、< は Ord、文字列の補間は Show を求めます。

効果の集合に付ける Immediate の制約([R: Immediate])は 0.20 で動きます。中断しない関数だけを受けたい場所で使います(外部関数)。

List や Option のような型構成子そのものを型引数に渡すこと(高カインド型)はできません。List.map と Option.map は別々の関数です。

型引数はふつうの型のほかに、エラーの集合と効果の集合も受けられます。失敗しうる関数や効果を使う関数をフィールドに持つ型を作るときに使います。

import Std.Console.{Console, SystemConsole}
type Request(path: String)
type Response(status: Int, body: String)
error NotFound(path: String)
type Handler[E, R](run: fn(Request) -> Response fails E use R)
fn hello(req: Request) -> Response {
Response(status: 200, body: "hello #{req.path}")
}
fn logged(req: Request) -> Response fails NotFound use Console {
out = use Console
out.print_line("GET #{req.path}")
if req.path == "/" { Response(status: 200, body: "ok") } else { fail NotFound(path: req.path) }
}
fn serve[E, R](h: Handler[E, R], req: Request) -> Response fails E use R {
h.run(req)
}
fn main() {
pure: Handler[{}, {}] = Handler(run: hello)
dbg(serve(pure, Request(path: "/a"))) # Response(status: 200, body: "hello /a")
noisy: Handler[NotFound, Console] = Handler(run: logged)
with SystemConsole {
r = serve(noisy, Request(path: "/x")) escape {
NotFound(path:) -> Response(status: 404, body: path)
}
dbg(r) # Response(status: 404, body: "/x")
}
}

type のフィールドに書いた関数型で fails と use を省くと、「失敗せず、効果も持たない」の意味になります。効果のある関数を持たせるときは、上の Handler[E, R] のように型引数で受けます。E がエラーの集合で R が効果の集合だということは、fails E と use R という使われ方から推論されます。

集合を渡すときは fails 節と use 節と同じ形で、Handler[NotFound | Timeout, { Db, Log }] と書きます。空なら Handler[{}, {}]、要素が1つなら Handler[NotFound, Console] のように囲まずに書けます。

細かい規則は 型と効果の体系 にあります。