回答:
パターンマッチングを理解するには、3つの部分を説明する必要があります。
代数的データ型の要約
MLのような関数型言語では、「素集合」または「代数的データ型」と呼ばれる単純なデータ型を定義できます。これらのデータ構造は単純なコンテナーであり、再帰的に定義できます。例えば:
type 'a list =
| Nil
| Cons of 'a * 'a list
スタックのようなデータ構造を定義します。このC#と同等であると考えてください。
public abstract class List<T>
{
public class Nil : List<T> { }
public class Cons : List<T>
{
public readonly T Item1;
public readonly List<T> Item2;
public Cons(T item1, List<T> item2)
{
this.Item1 = item1;
this.Item2 = item2;
}
}
}
したがって、Cons
およびNil
識別子of x * y * z * ...
は、がコンストラクタといくつかのデータ型を定義する、単純で単純なクラスを定義します。コンストラクターへのパラメーターには名前がなく、位置とデータ型によって識別されます。
a list
クラスのインスタンスは次のように作成します。
let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))
これは次と同じです:
Stack<int> x = new Cons(1, new Cons(2, new Cons(3, new Cons(4, new Nil()))));
一言で言えばパターンマッチング
パターンマッチングは一種のタイプテストです。上記のようなスタックオブジェクトを作成したとしましょう。次のように、スタックをピークおよびポップするメソッドを実装できます。
let peek s =
match s with
| Cons(hd, tl) -> hd
| Nil -> failwith "Empty stack"
let pop s =
match s with
| Cons(hd, tl) -> tl
| Nil -> failwith "Empty stack"
上記のメソッドは、次のC#と同等です(そのように実装されていません)。
public static T Peek<T>(Stack<T> s)
{
if (s is Stack<T>.Cons)
{
T hd = ((Stack<T>.Cons)s).Item1;
Stack<T> tl = ((Stack<T>.Cons)s).Item2;
return hd;
}
else if (s is Stack<T>.Nil)
throw new Exception("Empty stack");
else
throw new MatchFailureException();
}
public static Stack<T> Pop<T>(Stack<T> s)
{
if (s is Stack<T>.Cons)
{
T hd = ((Stack<T>.Cons)s).Item1;
Stack<T> tl = ((Stack<T>.Cons)s).Item2;
return tl;
}
else if (s is Stack<T>.Nil)
throw new Exception("Empty stack");
else
throw new MatchFailureException();
}
(ほとんどの場合、ML言語は実行時の型テストやキャストなしでパターンマッチングを実装しているため、C#コードは多少誤解を招きます。実装の詳細は、手を振ってください:))
一言で言えば、データ構造の分解
では、ピークメソッドに戻りましょう。
let peek s =
match s with
| Cons(hd, tl) -> hd
| Nil -> failwith "Empty stack"
トリックは、hd
およびtl
識別子が変数であることを理解することです(えーと...それらは不変なので、実際には「変数」ではなく「値」です;))。場合はs
種類がありCons
、我々はコンストラクタのうち、その値を引き出すつもりだとバインドそれらの名前の変数へhd
とtl
。
パターンマッチングは、データ構造をその内容ではなくその形状で分解できるので便利です。したがって、次のようにバイナリツリーを定義するとします。
type 'a tree =
| Node of 'a tree * 'a * 'a tree
| Nil
次のようにいくつかのツリーの回転を定義できます。
let rotateLeft = function
| Node(a, p, Node(b, q, c)) -> Node(Node(a, p, b), q, c)
| x -> x
let rotateRight = function
| Node(Node(a, p, b), q, c) -> Node(a, p, Node(b, q, c))
| x -> x
(let rotateRight = function
コンストラクタはの構文シュガーですlet rotateRight s = match s with ...
。)
したがって、データ構造を変数にバインドするだけでなく、データ構造にドリルダウンすることもできます。ノードがあるとしましょうlet x = Node(Nil, 1, Nil)
。を呼び出すと、最初のパターンに対してrotateLeft x
テストx
します。これは、正しい子がのNil
代わりに型を持っているために一致しませんNode
。次のパターンに移動します。これはx -> x
、すべての入力に一致し、変更されずに返されます。
比較のために、C#で上記のメソッドを次のように記述します。
public abstract class Tree<T>
{
public abstract U Match<U>(Func<U> nilFunc, Func<Tree<T>, T, Tree<T>, U> nodeFunc);
public class Nil : Tree<T>
{
public override U Match<U>(Func<U> nilFunc, Func<Tree<T>, T, Tree<T>, U> nodeFunc)
{
return nilFunc();
}
}
public class Node : Tree<T>
{
readonly Tree<T> Left;
readonly T Value;
readonly Tree<T> Right;
public Node(Tree<T> left, T value, Tree<T> right)
{
this.Left = left;
this.Value = value;
this.Right = right;
}
public override U Match<U>(Func<U> nilFunc, Func<Tree<T>, T, Tree<T>, U> nodeFunc)
{
return nodeFunc(Left, Value, Right);
}
}
public static Tree<T> RotateLeft(Tree<T> t)
{
return t.Match(
() => t,
(l, x, r) => r.Match(
() => t,
(rl, rx, rr) => new Node(new Node(l, x, rl), rx, rr))));
}
public static Tree<T> RotateRight(Tree<T> t)
{
return t.Match(
() => t,
(l, x, r) => l.Match(
() => t,
(ll, lx, lr) => new Node(ll, lx, new Node(lr, x, r))));
}
}
真剣に。
パターンマッチングは素晴らしい
ビジターパターンを使用して、C#のパターンマッチングに似たものを実装できますが、複雑なデータ構造を効果的に分解できないため、それほど柔軟ではありません。さらに、パターンマッチングを使用している場合は、大文字と小文字を省略したかどうかがコンパイラーから通知されます。なんてすごい?
パターンマッチングを使用せずに、C#または言語で同様の機能を実装する方法を考えてください。実行時にテストテストとキャストを行わずにそれを行う方法を考えてください。それは確かに難しくはなく、面倒でかさばるだけです。また、すべてのケースをカバーしているかどうかを確認するためのコンパイラチェックはありません。
したがって、パターンマッチングは、非常に便利でコンパクトな構文でデータ構造を分解およびナビゲートするのに役立ちます。これにより、コンパイラはコードのロジックを少なくとも少しだけチェックできます。それは本当にあるキラー機能。
短い答え:関数型言語は等号を代入ではなく等価のアサーションとして扱うため、パターンマッチングが発生します。
長い答え:パターンマッチングは、指定された値の「形状」に基づくディスパッチの一種です。関数型言語では、定義するデータ型は通常、判別共用体または代数的データ型と呼ばれます。たとえば、(リンクされた)リストとは何ですか?List
あるタイプのリンクされたリストはa
、空のリストか、aに編集されたNil
タイプの要素(sのリスト)のいずれかです。Haskell(私が最もよく知っている関数型言語)では、次のように記述しますa
Cons
List a
a
data List a = Nil
| Cons a (List a)
すべての識別された共用体はこのように定義されています。単一の型には、それを作成する固定された数の異なる方法があります。クリエイターは、同様にNil
してCons
、ここで、コンストラクタと呼ばれています。つまり、型の値はList a
2つの異なるコンストラクターで作成された可能性があり、2つの異なる形状を持つ可能性があります。したがってhead
、リストの最初の要素を取得する関数を記述したいとします。Haskellでは、これを次のように書きます。
-- `head` is a function from a `List a` to an `a`.
head :: List a -> a
-- An empty list has no first item, so we raise an error.
head Nil = error "empty list"
-- If we are given a `Cons`, we only want the first part; that's the list's head.
head (Cons h _) = h
以来List a
値は、2つの異なる種類のものとすることができる、私たちは、それぞれを個別に処理する必要があります。これはパターンマッチングです。ではhead x
、x
パターンNil
に一致する場合、最初のケースを実行します。パターンに一致する場合Cons h _
、2番目を実行します。
短い答え、説明:この動作について考える最良の方法の1つは、等号の考え方を変えることです。中括弧の言語で=
は、概して、代入を示します。a = b
つまり、「make a
into b
。」関数型言語の多くでは、しかし、=
平等の主張を表し:let Cons a (Cons b Nil) = frob x
アサート左側のものが、ことCons a (Cons b Nil)
、右、上のものと同じですfrob x
。さらに、左側で使用されているすべての変数が表示されます。これも関数の引数で起こっていることです。最初の引数がのようNil
に見えると断言し、そうでない場合はチェックを続けます。
Cons
意味ですか?
Cons
あるコンス・ヘッドのうち(リンク)リスト(ビルドをtructor a
)とテール(List a
)。名前はLispに由来します。Haskellでは、組み込みのリストタイプの場合、これは:
演算子です(これはまだ「cons」と発音されます)。
それは書く代わりに
double f(int x, int y) {
if (y == 0) {
if (x == 0)
return NaN;
else if (x > 0)
return Infinity;
else
return -Infinity;
} else
return (double)x / y;
}
あなたは書ける
f(0, 0) = NaN;
f(x, 0) | x > 0 = Infinity;
| else = -Infinity;
f(x, y) = (double)x / y;
C ++もパターンマッチングをサポートしています。
static const int PositiveInfinity = -1;
static const int NegativeInfinity = -2;
static const int NaN = -3;
template <int x, int y> struct Divide {
enum { value = x / y };
};
template <bool x_gt_0> struct aux { enum { value = PositiveInfinity }; };
template <> struct aux<false> { enum { value = NegativeInfinity }; };
template <int x> struct Divide<x, 0> {
enum { value = aux<(x>0)>::value };
};
template <> struct Divide<0, 0> {
enum { value = NaN };
};
#include <cstdio>
int main () {
printf("%d %d %d %d\n", Divide<7,2>::value, Divide<1,0>::value, Divide<0,0>::value, Divide<-1,0>::value);
return 0;
};
パターンマッチングは、ステロイドのオーバーロードメソッドのようなものです。最も単純なケースは、Javaで見たものとほぼ同じで、引数は名前付きの型のリストです。呼び出す正しいメソッドは、渡された引数に基づいており、パラメーター名への引数の割り当てを兼ねています。
パターンはさらに一歩進んで、渡された引数をさらに分解することができます。また、引数の値に基づいて実際に照合するためにガードを使用する可能性もあります。実例として、JavaScriptにパターンマッチングがあったようなふりをします。
function foo(a,b,c){} //no pattern matching, just a list of arguments
function foo2([a],{prop1:d,prop2:e}, 35){} //invented pattern matching in JavaScript
foo2では、aが配列であると想定し、2番目の引数を分解し、2つのプロップ(prop1、prop2)を持つオブジェクトを想定し、それらのプロパティの値を変数dおよびeに割り当て、3番目の引数が35。
JavaScriptとは異なり、パターンマッチングを使用する言語では、通常、名前は同じでパターンが異なる複数の関数を使用できます。このように、それはメソッドのオーバーロードのようなものです。私はアーランで例を挙げます:
fibo(0) -> 0 ;
fibo(1) -> 1 ;
fibo(N) when N > 0 -> fibo(N-1) + fibo(N-2) .
あなたの目を少しぼかし、あなたはこれをJavaScriptで想像することができます。このような何か:
function fibo(0){return 0;}
function fibo(1){return 1;}
function fibo(N) when N > 0 {return fibo(N-1) + fibo(N-2);}
ポイントは、fiboを呼び出すときに使用する実装が引数に基づいていることですが、Javaがオーバーロードの唯一の手段として型に制限されている場合、パターンマッチングではさらに多くのことができます。
ここに示されている関数のオーバーロードの他に、同じ原理をケースステートメントや破壊的な割り当てなどの他の場所に適用できます。JavaScriptは1.7でもこれを備えています。
パターンマッチングを使用すると、値(またはオブジェクト)をいくつかのパターンと照合して、コードのブランチを選択できます。C ++の観点からは、switch
ステートメントに少し似ているように見えるかもしれません。関数型言語では、パターンマッチングを使用して、整数などの標準的なプリミティブ値をマッチングできます。ただし、構成タイプの場合はより便利です。
まず、プリミティブ値のパターンマッチングを示します(拡張された疑似C ++を使用switch
)。
switch(num) {
case 1:
// runs this when num == 1
case n when n > 10:
// runs this when num > 10
case _:
// runs this for all other cases (underscore means 'match all')
}
2番目の用途は、タプル(単一の値に複数のオブジェクトを格納できる)や、いくつかのオプションの1つを含むことができる型を作成できる識別された共用体などの機能データ型を扱います。これは、enum
各ラベルがいくつかの値も保持できることを除いて、少し似ています。疑似C ++構文では:
enum Shape {
Rectangle of { int left, int top, int width, int height }
Circle of { int x, int y, int radius }
}
typeの値には、すべての座標をShape
含むか、中心と半径を含むRectangle
a Circle
を含めることができます。パターンマッチングを使用すると、Shape
タイプを操作するための関数を記述できます。
switch(shape) {
case Rectangle(l, t, w, h):
// declares variables l, t, w, h and assigns properties
// of the rectangle value to the new variables
case Circle(x, y, r):
// this branch is run for circles (properties are assigned to variables)
}
最後に、両方の機能を組み合わせたネストされたパターンを使用することもできます。たとえば、Circle(0, 0, radius)
[0、0]に中心があり、任意の半径を持つすべての形状を一致させるために使用できます(半径の値は新しい変数に割り当てられます)radius
)。
これはC ++の観点からは少し不慣れに聞こえるかもしれませんが、私の疑似C ++で説明が明確になることを願っています。関数型プログラミングはまったく異なる概念に基づいているため、関数型言語ではより理にかなっています。
パターンマッチングでは、言語のインタプリタが、指定した引数の構造と内容に基づいて特定の関数を選択します。
これは関数型言語の機能であるだけでなく、さまざまな言語で使用できます。
私がこのアイデアに初めて出会ったのは、プロローグが言語の中心であるところを学んだときでした。
例えば
last([LastItem]、LastItem)。
last([Head | Tail]、LastItem):-last(Tail、LastItem)。
上記のコードは、リストの最後の項目を提供します。入力引数が最初で、結果が2番目です。
リストに項目が1つしかない場合、インタプリタは最初のバージョンを選択し、2番目の引数は最初のバージョンと等しくなるように設定されます。つまり、結果に値が割り当てられます。
リストに先頭と末尾の両方がある場合、インタープリターは2番目のバージョンを選択し、リストに項目が1つだけ残るまで再帰します。
多くの人にとって、いくつかの簡単な例が提供されている場合は、新しい概念を選択する方が簡単です。
3つの整数のリストがあり、最初と3番目の要素を追加したいとします。パターンマッチングがなければ、次のようにすることができます(Haskellの例):
Prelude> let is = [1,2,3]
Prelude> head is + is !! 2
4
さて、これはおもちゃの例ですが、最初の整数と3番目の整数を変数にバインドしてそれらを合計したいとします。
addFirstAndThird is =
let first = head is
third = is !! 3
in first + third
このデータ構造からの値の抽出は、パターンマッチングが行うことです。基本的に、何かの構造を「ミラーリング」して、目的の場所にバインドする変数を指定します。
addFirstAndThird [first,_,third] = first + third
[1,2,3]を引数としてこの関数を呼び出すと、[1,2,3]は[first、_
third]に最初は1に、3番目は3に、2 _
は破棄されます(プレースホルダーあなたが気にしないもののために)。
2番目の要素が2のリストのみを一致させたい場合は、次のように実行できます。
addFirstAndThird [first,2,third] = first + third
これは、2番目の要素が2のリストでのみ機能し、一致しないリストにはaddFirstAndThirdの定義が提供されないため、それ以外の場合は例外がスローされます。
これまでは、バインディングの破壊にのみパターンマッチングを使用していました。その上で、最初の一致する定義が使用される同じ関数の複数の定義を指定できます。したがって、パターンマッチングは「ステレオイドのスイッチステートメント」に少し似ています。
addFirstAndThird [first,2,third] = first + third
addFirstAndThird _ = 0
addFirstAndThirdは、2番目の要素が2であるリストの1番目と3番目の要素を喜んで追加します。それ以外の場合は、「フォールスルー」および「リターン」0を返します。この「スイッチのような」機能は、関数定義でのみ使用できません。たとえば、
Prelude> case [1,3,3] of [a,2,c] -> a+c; _ -> 0
0
Prelude> case [1,2,3] of [a,2,c] -> a+c; _ -> 0
4
さらに、リストに限定されず、他のタイプでも使用できます。たとえば、値を「ラップ解除」するためにMaybeタイプのJustおよびNothing値コンストラクターと一致させることができます。
Prelude> case (Just 1) of (Just x) -> succ x; Nothing -> 0
2
Prelude> case Nothing of (Just x) -> succ x; Nothing -> 0
0
もちろん、あくまでもおもちゃの例であり、正式な説明や徹底的な説明はしていませんが、基本的な考え方は十分理解できているはずです。
かなり良い説明を与えるウィキペディアのページから始めるべきです。次に、Haskell wikibookの関連する章を読みます。
これは上記のウィキブックからの素晴らしい定義です:
したがって、パターンマッチングは、名前に名前を割り当てる(またはそれらの名前をそれらにバインドする)方法であり、式を同時に部分式に分解する(マップの定義のリストで行ったように)可能性があります。
次に、パターンマッチングの有用性を示す非常に短い例を示します。
リストの要素をソートしたいとしましょう:
["Venice","Paris","New York","Amsterdam"]
へ(「ニューヨーク」をソートしました)
["Venice","New York","Paris","Amsterdam"]
より命令的な言語であなたは書くでしょう:
function up(city, cities){
for(var i = 0; i < cities.length; i++){
if(cities[i] === city && i > 0){
var prev = cities[i-1];
cities[i-1] = city;
cities[i] = prev;
}
}
return cities;
}
関数型言語では、代わりに次のように記述します。
let up list value =
match list with
| [] -> []
| previous::current::tail when current = value -> current::previous::tail
| current::tail -> current::(up tail value)
パターンマッチングソリューションの方がノイズが少ないことがわかるので、さまざまなケースが何であり、リストを移動して分解するのがどれほど簡単かを明確に確認できます。