Hiểu khớp mẫu đòi hỏi phải giải thích ba phần:
- Các loại dữ liệu đại số.
- Khớp mẫu là gì
- Tại sao nó tuyệt vời.
Các loại dữ liệu đại số một cách ngắn gọn
Các ngôn ngữ chức năng giống như ML cho phép bạn xác định các loại dữ liệu đơn giản được gọi là "tách rời các hiệp hội" hoặc "các loại dữ liệu đại số". Các cấu trúc dữ liệu này là các thùng chứa đơn giản và có thể được định nghĩa đệ quy. Ví dụ:
type 'a list =
| Nil
| Cons of 'a * 'a list
định nghĩa một cấu trúc dữ liệu giống như ngăn xếp. Hãy nghĩ về nó tương đương với C # này:
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;
}
}
}
Vì vậy, các định danh Cons
và Nil
định nghĩa đơn giản một lớp đơn giản, trong đó of x * y * z * ...
định nghĩa một hàm tạo và một số kiểu dữ liệu. Các tham số cho hàm tạo không được đặt tên, chúng được xác định theo vị trí và loại dữ liệu.
Bạn tạo các thể hiện của a list
lớp như vậy:
let x = Cons(1, Cons(2, Cons(3, Cons(4, Nil))))
Điều này giống như:
Stack<int> x = new Cons(1, new Cons(2, new Cons(3, new Cons(4, new Nil()))));
Kết hợp mẫu trong một tóm tắt
Mẫu phù hợp là một loại thử nghiệm. Vì vậy, giả sử chúng ta đã tạo một đối tượng ngăn xếp như ở trên, chúng ta có thể thực hiện các phương thức để xem và bật ngăn xếp như sau:
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ác phương thức trên là tương đương (mặc dù không được triển khai như vậy) với C # sau:
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();
}
(Hầu như luôn luôn, các ngôn ngữ ML triển khai khớp mẫu mà không cần kiểm tra kiểu hoặc thời gian chạy, do đó, mã C # có phần lừa đảo. Hãy gạt chi tiết triển khai sang một bên bằng cách vẫy tay :))
Phân rã cấu trúc dữ liệu một cách ngắn gọn
Ok, chúng ta hãy quay lại với phương thức peek:
let peek s =
match s with
| Cons(hd, tl) -> hd
| Nil -> failwith "Empty stack"
Bí quyết là hiểu rằng các định danh hd
và tl
định danh là các biến (errm ... vì chúng không thay đổi, chúng không thực sự là "biến", mà là "giá trị";)). Nếu s
có kiểu Cons
, thì chúng ta sẽ rút các giá trị của nó ra khỏi hàm tạo và liên kết chúng với các biến có tên hd
và tl
.
Khớp mẫu rất hữu ích vì nó cho phép chúng ta phân tách cấu trúc dữ liệu theo hình dạng của nó thay vì nội dung của nó . Vì vậy, hãy tưởng tượng nếu chúng ta định nghĩa một cây nhị phân như sau:
type 'a tree =
| Node of 'a tree * 'a * 'a tree
| Nil
Chúng ta có thể định nghĩa một số phép quay của cây như sau:
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
(Hàm let rotateRight = function
tạo là cú pháp đường cho let rotateRight s = match s with ...
.)
Vì vậy, ngoài việc ràng buộc cấu trúc dữ liệu với các biến, chúng ta cũng có thể đi sâu vào nó. Hãy nói rằng chúng ta có một nút let x = Node(Nil, 1, Nil)
. Nếu chúng tôi gọi rotateLeft x
, chúng tôi sẽ kiểm tra x
mẫu đầu tiên không khớp vì mẫu đúng có loại Nil
thay vìNode
. Nó sẽ chuyển sang mẫu tiếp theo x -> x
, sẽ khớp với bất kỳ đầu vào nào và trả lại nó không được sửa đổi.
Để so sánh, chúng tôi sẽ viết các phương thức trên trong C # là:
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))));
}
}
Đối với nghiêm túc.
Kết hợp mẫu là tuyệt vời
Bạn có thể triển khai một cái gì đó tương tự như khớp mẫu trong C # bằng cách sử dụng mẫu khách truy cập , nhưng nó gần như không linh hoạt vì bạn không thể phân tách hiệu quả các cấu trúc dữ liệu phức tạp. Hơn nữa, nếu bạn đang sử dụng khớp mẫu, trình biên dịch sẽ cho bạn biết nếu bạn bỏ qua một trường hợp . Làm thế nào là tuyệt vời?
Hãy suy nghĩ về cách bạn triển khai chức năng tương tự trong C # hoặc ngôn ngữ mà không khớp mẫu. Hãy suy nghĩ về cách bạn làm điều đó mà không cần kiểm tra thử nghiệm và diễn xuất trong thời gian chạy. Nó chắc chắn không khó , chỉ cồng kềnh và cồng kềnh. Và bạn không có trình biên dịch kiểm tra để đảm bảo rằng bạn đã bao quát mọi trường hợp.
Vì vậy, khớp mẫu giúp bạn phân tách và điều hướng các cấu trúc dữ liệu theo một cú pháp nhỏ gọn, rất thuận tiện, nó cho phép trình biên dịch kiểm tra logic của mã của bạn, ít nhất là một chút. Nó thực sự là một tính năng sát thủ.