Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

パターンマッチング

💡 お知らせ: このドキュメントはAIによって翻訳されています。表現に違和感がある場合は、原文(英語)を参照するか、翻訳にご協力ください。

Enum に対するマッチング

Flix は、代数的データ型(algebraic data type)に対するパターンマッチングを サポートしています。

たとえば、図形をモデル化する代数的データ型があるとします。

enum Shape {
    case Circle(Int32)
    case Square(Int32)
    case Rectangle(Int32, Int32)
}

このとき、パターンマッチングを使って Shape の面積を計算する関数を、 次のように書けます。

def area(s: Shape): Int32 = match s {
    case Shape.Circle(r)       => 3 * (r * r)
    case Shape.Square(w)       => w * w
    case Shape.Rectangle(h, w) => h * w
}

レコードに対するマッチング

上記はレコード型に対しても機能しますが、構文が少し異なります。先ほどの Shape 型を、今度はレコードを使って書き換えてみましょう。

enum Shape {
    case Circle({ radius = Int32 })
    case Square({ width = Int32 })
    case Rectangle({ height = Int32, width = Int32 })
}

def area(s: Shape): Int32 = match s {
    case Shape.Circle({ radius })           => 3 * (radius * radius)
    case Shape.Square({ width })            => width * width
    case Shape.Rectangle({ height, width }) => height * width
}

上の例では、各パターンが指定されたラベルをちょうど持つことを、暗黙に 要求しています。多すぎても少なすぎてもいけません。 しかし一般に、レコードパターンの構文はその型の構文と似ています。 そのため、少なくとも特定のラベルを 1 つ持つレコードにマッチさせることができます。

def f(r: { height = Int32 | a }): Int32 = match r {
    case { height | _ } => height
    // 拡張部分は使われないのでワイルドカードパターンになっています
}

ただし、パターンは型も規定する(imply)ことに注意してください。そのため、 次の例は動作しません。

def badTypes(r: { height = Int32 | a }): Int32 = match r {
    case { height } => height
}

さらに、すべての case は同じ型でなければならないため、これも動作しません。

match ??? {
    case { height | _ } => height
    case { height }     => height
}

これはやや不自然な(こじつけのような)例かもしれませんが、よくある落とし穴を示しており、 簡単に修正できます。

これは、最初の case が height ラベルを持つ多相的なレコード(polymorphic record)であるのに対し、2 番目の case が height ラベル のみ が定義された 閉じたレコード(closed record)にマッチするためです。

さらに、{ label } パターンは実際には { label = pattern } の 糖衣構文(syntactic sugar)です。そのため、複数のレコードを扱う場合は、 異なるパターンを使う必要があるかもしれません。

def shadowing(r1: { height = Int32 | a }, r2: { height = Int32 | b }): Int32 =
    match (r1, r2) {
        case ({ height | _ }, { height | _ }) => height + height
        // `height = height` が 2 回定義されるため、これは動作しません
    }

しかし、変数の名前を変えれば、プログラムは型検査を通ります。

def renaming(r1: { height = Int32 | a }, r2: { height = Int32 | b }): Int32 =
    match (r1, r2) {
        case ({ height = h1 | _ }, { height = h2 | _ }) => h1 + h2
    }

まとめると、レコードパターンの例をいくつか挙げます。

  • { } - 空のレコード
  • { radius = r } - radius ラベルだけを持つレコード。その値はスコープ内で r に束縛されます
  • { radius } - radius ラベルだけを持つレコード(これは実際には { radius = radius } の糖衣構文です)
  • { radius | _ } - 少なくとも radius ラベルを持つレコード
  • { radius | r } - 少なくとも radius ラベルを持つレコード。レコードの残りの部分は r に束縛されます

let パターンマッチ

パターン match 構文に加えて、let 束縛(let-binding)を使って値を分解する こともできます。たとえば:

let (x, y, z) = (1, 2, 3)

これは変数 xyz を、それぞれ値 123 に束縛します。

網羅的(exhaustive)なパターンであれば、let 束縛の中で使えます。たとえば:

let (x, Foo(y, z)) = (1, Foo(2, 3))

これは、Foo がその型における唯一のコンストラクタである場合に限り、 正当です。

次の let 束縛は網羅的でないため 不正 です。

let (1, 2, z) = ...
let Some(x) = ...

Flix コンパイラは、このような網羅的でないパターンを拒否します。

let パターンマッチはレコードと相性が良く、レコードを分解して、関心のある ラベルだけを使うことができます。

let { height | _ } = r;
height + height

マッチラムダ

パターンマッチは、ラムダ式とともに使うこともできます。たとえば:

List.map(match (x, y) -> x + y, (1, 1) :: (2, 2) :: Nil)

は次と等価です。

List.map(w -> match w { case (x, y) => x + y }, (1, 1) :: (2, 2) :: Nil)

let 束縛の場合と同様に、このようなパターンマッチも網羅的でなければなりません。

2 つのラムダ式の違いに注意してください。

let f = (x, y, z) -> x + y + z + 42i32
let g = match (x, y, z) -> x + y + z + 42i32

ここで f3 つInt32 引数を取る関数であるのに対し、g1 つ の 3 要素タプル (Int32, Int32, Int32) を引数に取る関数です。