Memahami pencocokan pola memerlukan penjelasan tiga bagian:
- Tipe data aljabar.
- Apa itu pencocokan pola
- Kenapa ini luar biasa.
Singkatnya, tipe data aljabar
Bahasa fungsional mirip-ML memungkinkan Anda menentukan tipe data sederhana yang disebut "disjoint unions" atau "tipe data aljabar". Struktur data ini adalah wadah sederhana, dan dapat didefinisikan secara rekursif. Sebagai contoh:
type 'a list =
| Nil
| Cons of 'a * 'a list
mendefinisikan struktur data seperti stack. Anggap itu setara dengan C # ini:
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;
}
}
}
Jadi, Cons
dan Nil
pengidentifikasi mendefinisikan kelas sederhana, di mana of x * y * z * ...
mendefinisikan konstruktor dan beberapa tipe data. Parameter ke konstruktor tidak disebutkan namanya, mereka diidentifikasi oleh posisi dan tipe data.
Anda membuat instance a list
kelas Anda seperti itu:
let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))
Yang sama dengan:
Stack<int> x = new Cons(1, new Cons(2, new Cons(3, new Cons(4, new Nil()))));
Pencocokan pola singkatnya
Pencocokan pola adalah sejenis pengujian tipe. Jadi katakanlah kita membuat objek stack seperti yang di atas, kita dapat menerapkan metode untuk mengintip dan meletupkan stack sebagai berikut:
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"
Metode di atas setara (meskipun tidak diterapkan seperti itu) dengan C # berikut:
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();
}
(Hampir selalu, bahasa ML menerapkan pencocokan pola tanpa menjalankan jenis-tes atau gips, sehingga kode C # agak menipu. Mari menyikat detail implementasi dengan beberapa lambaian tangan :))
Singkatnya, struktur data dekomposisi
Ok, mari kembali ke metode mengintip:
let peek s =
match s with
| Cons(hd, tl) -> hd
| Nil -> failwith "Empty stack"
Kuncinya adalah memahami bahwa hd
dan tl
pengidentifikasi adalah variabel (errm ... karena mereka tidak berubah, mereka tidak benar-benar "variabel", tetapi "nilai";)). Jika s
memiliki tipe Cons
, maka kita akan menarik nilainya keluar dari konstruktor dan mengikatnya ke variabel bernama hd
dan tl
.
Pencocokan pola berguna karena memungkinkan kita menguraikan struktur data berdasarkan bentuknya, bukan kontennya . Jadi bayangkan jika kita mendefinisikan pohon biner sebagai berikut:
type 'a tree =
| Node of 'a tree * 'a * 'a tree
| Nil
Kita dapat mendefinisikan beberapa rotasi pohon sebagai berikut:
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
Konstruktornya adalah gula sintaksis untuk let rotateRight s = match s with ...
.)
Jadi selain mengikat struktur data ke variabel, kita juga bisa menelusuri ke dalamnya. Katakanlah kita memiliki simpul let x = Node(Nil, 1, Nil)
. Jika kami memanggil rotateLeft x
, kami menguji x
terhadap pola pertama, yang gagal mencocokkan karena anak yang tepat memiliki jenis, Nil
bukan Node
. Ini akan pindah ke pola berikutnya x -> x
,, yang akan cocok dengan input apa pun dan mengembalikannya tanpa dimodifikasi.
Sebagai perbandingan, kami akan menulis metode di atas dalam C # sebagai:
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))));
}
}
Untuk yang serius.
Pencocokan pola mengagumkan
Anda dapat menerapkan sesuatu yang mirip dengan pencocokan pola dalam C # menggunakan pola pengunjung , tetapi tidak hampir fleksibel karena Anda tidak dapat secara efektif menguraikan struktur data yang kompleks. Selain itu, jika Anda menggunakan pencocokan pola, kompiler akan memberi tahu Anda jika Anda meninggalkan kasing . Seberapa hebat itu?
Pikirkan tentang bagaimana Anda akan menerapkan fungsionalitas serupa dalam C # atau bahasa tanpa pencocokan pola. Pikirkan tentang bagaimana Anda akan melakukannya tanpa tes-tes dan gips saat runtime. Ini tentu tidak sulit , hanya rumit dan tebal. Dan Anda tidak memiliki pemeriksa memeriksa untuk memastikan Anda telah menutupi setiap kasus.
Jadi pencocokan pola membantu Anda menguraikan dan menavigasi struktur data dalam sintaksis yang sangat nyaman dan ringkas, memungkinkan kompiler untuk memeriksa logika kode Anda, setidaknya sedikit. Ini benar - benar fitur pembunuh.