私は関数型プログラミングについて読んでいますが、多くの記事で関数型言語のコア機能の1つとしてPattern Matchingが言及されていることに気付きました。
誰かがJava/C++/JavaScript開発者のためにそれが何を意味するのか説明できますか?
パターンマッチングを理解するには、次の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#コードはやや欺cept的です。 :))
簡単なデータ構造の分解
では、ピークメソッドに戻りましょう。
_let peek s =
match s with
| Cons(hd, tl) -> hd
| Nil -> failwith "Empty stack"
_
トリックは、hd
およびtl
識別子が変数であることを理解することです(不変であるため、実際には「変数」ではなく「値」;))。 s
の型がCons
の場合、コンストラクターからその値を引き出し、hd
およびtl
という名前の変数にバインドします。 。
パターンマッチングは、contentsではなく、shapeによってデータ構造を分解できるため便利です。したがって、次のようにバイナリツリーを定義するとします。
_type 'a tree =
| Node of 'a tree * 'a * 'a tree
| Nil
_
次のように、いくつかの tree rotations を定義できます。
_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))));
}
}
_
真剣に。
パターンマッチングは素晴らしい
visitor pattern を使用して、C#のパターンマッチングに何かsimilarを実装できますが、効果的に分解できないため、それほど柔軟ではありません複雑なデータ構造。さらに、パターンマッチングを使用している場合、コンパイラは、ケースを除外したかどうかを通知します。それはどれほど素晴らしいですか?
パターンマッチングなしでC#または言語で同様の機能をどのように実装するかを考えてください。実行時にテストとキャストを行わずにどのように実行するかを考えてください。確かにhardではなく、単に扱いにくくてかさばります。そして、すべてのケースをカバーしていることを確認するためのコンパイラーのチェックはありません。
したがって、パターンマッチングは、非常に便利でコンパクトな構文でデータ構造を分解およびナビゲートするのに役立ち、コンパイラがコードのlogicを少なくとも少しだけチェックできるようにします。本当にはキラー機能です。
短い答え:関数型言語は等号を代入ではなく等価のアサーションとして扱うため、パターンマッチングが発生します。
長答:パターンマッチングは、指定された値の「形状」に基づくディスパッチの形式です。関数型言語では、定義するデータ型は通常、識別された共用体または代数的データ型と呼ばれるものです。たとえば、(リンクされた)リストとは何ですか?タイプList
の物のリンクリストa
は、空のリストNil
またはタイプa
Cons
edの要素のいずれかです。 _List a
_(a
sのリスト)。 Haskell(私が最もよく知っている関数型言語)で、これを書きます
_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
_は、「a
をb
にする」ことを意味します。 、_=
_は同等のアサーションを示します。let Cons a (Cons b Nil) = frob x
asserts左側のものCons a (Cons b Nil)
は右側のもの、_frob x
_;さらに、左側で使用されているすべての変数が表示されます。これは、関数の引数でも発生します。最初の引数はNil
のように見えると断言し、そうでない場合はチェックを続けます。
書く代わりに
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;
};
パターンマッチングを使用すると、値(またはオブジェクト)を一部のパターンと照合して、コードのブランチを選択できます。 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番目の使用法は、タプル(複数のオブジェクトを単一の値に格納できる)や差別化された共用体などの機能データ型を扱います。いくつかのオプションのいずれかが含まれています。これはenum
に少し似ていますが、各ラベルはいくつかの値を保持できる点が異なります。疑似C++構文の場合:
_enum Shape {
Rectangle of { int left, int top, int width, int height }
Circle of { int x, int y, int radius }
}
_
タイプShape
の値には、すべての座標を持つRectangle
または中心と半径を持つ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++で説明が明確になることを願っています。関数型プログラミングはまったく異なる概念に基づいているため、関数型言語ではより意味があります!
パターンマッチングは、ステロイドのオーバーロードメソッドのようなものです。最も単純なケースは、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つのprop(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はオーバーロードの唯一の手段として型に制限され、パターンマッチングはそれ以上を行うことができます。
ここに示すような関数のオーバーロードを超えて、caseステートメントやアサインメントの破壊など、同じ原則を他の場所に適用できます。 JavaScriptでは1.7でもこれがあります 。
パターンマッチングでは、言語のインタープリターが、指定した引数の構造と内容に基づいて特定の関数を選択します。
機能的な言語機能だけでなく、多くの異なる言語で使用できます。
私がこのアイデアに初めて出会ったのは、プロローグを学んだときでした。
例えば.
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の定義が与えられないため、例外をスローします。
これまで、バインディングの構造化にのみパターンマッチングを使用していました。その上で、最初の一致する定義が使用される同じ関数の複数の定義を与えることができます。したがって、パターンマッチングは「ステレオイドのswitchステートメント」に少し似ています。
addFirstAndThird [first,2,third] = first + third
addFirstAndThird _ = 0
addFirstAndThirdは、2番目の要素として2を使用して、リストの最初と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 の関連する章を読んでください。
これは上記のウィキブックからの素晴らしい定義です:
そのため、パターンマッチングは、名前を物に割り当てる(またはそれらの名前をそれらのものにバインドする)方法であり、同時に式を部分式に分解する可能性があります(mapの定義のリストで行ったように)。
パターンマッチングの有用性を示す非常に短い例を次に示します。
リスト内の要素をソートしたいとしましょう:
["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)
パターンマッチソリューションのノイズが少ないことがわかるように、さまざまなケースが何であるか、リストを移動して構造を解除することがどれほど簡単かを明確に確認できます。
私はそれについてより詳細なブログ記事を書きました here 。