'Pattern Matching' ในภาษาที่ใช้งานได้คืออะไร?


128

ฉันกำลังอ่านเกี่ยวกับการเขียนโปรแกรมเชิงฟังก์ชันและฉันสังเกตเห็นว่าการจับคู่รูปแบบมีการกล่าวถึงในหลายบทความว่าเป็นหนึ่งในคุณสมบัติหลักของภาษาที่ใช้งานได้

ใครช่วยอธิบายสำหรับนักพัฒนา Java / C ++ / JavaScript ว่าหมายความว่าอย่างไร


คำตอบ:


142

การทำความเข้าใจการจับคู่รูปแบบจำเป็นต้องอธิบายสามส่วน:

  1. ประเภทข้อมูลเกี่ยวกับพีชคณิต
  2. รูปแบบที่ตรงกันคืออะไร
  3. ทำไมมันยอดเยี่ยม

ประเภทข้อมูลพีชคณิตโดยสรุป

ภาษาที่ใช้งานได้เหมือน ML ช่วยให้คุณกำหนดประเภทข้อมูลง่ายๆที่เรียกว่า "disjoint unions" หรือ "algebraic data types" โครงสร้างข้อมูลเหล่านี้เป็นคอนเทนเนอร์อย่างง่ายและสามารถกำหนดซ้ำได้ ตัวอย่างเช่น:

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 # หรือภาษาที่ไม่มีรูปแบบที่ตรงกัน ลองนึกถึงวิธีที่คุณทำได้โดยไม่ต้องทดสอบการทดสอบและแคสต์ที่รันไทม์ มันไม่ยากอย่างแน่นอนเพียงแค่ยุ่งยากและใหญ่โต และคุณไม่มีการตรวจสอบคอมไพเลอร์เพื่อให้แน่ใจว่าคุณได้ครอบคลุมทุกกรณี

ดังนั้นการจับคู่รูปแบบจะช่วยให้คุณแยกย่อยและนำทางโครงสร้างข้อมูลในรูปแบบที่สะดวกและกะทัดรัดช่วยให้คอมไพเลอร์สามารถตรวจสอบตรรกะของโค้ดของคุณได้อย่างน้อยก็เล็กน้อย มันเป็นคุณสมบัติของนักฆ่าจริงๆ


+1 แต่อย่าลืมเกี่ยวกับภาษาอื่น ๆ ที่มีการจับคู่รูปแบบเช่น Mathematica
JD

1
"เอ่อ ... เนื่องจากไม่เปลี่ยนรูปจึงไม่ใช่" ตัวแปร "จริงๆ แต่เป็น" ค่า ";)" เป็นตัวแปร มันเป็นความหลากหลายที่ไม่แน่นอนที่เรียกไม่ถูก อย่างไรก็ตามคำตอบที่ยอดเยี่ยม!
Doval

3
"เกือบทุกครั้งภาษา ML จะใช้การจับคู่รูปแบบโดยไม่มีการทดสอบประเภทรันไทม์หรือการร่าย" <- วิธีนี้ทำงานอย่างไร? ช่วยชี้ไพรเมอร์หน่อยได้ไหม
David Moles

1
@DavidMoles: ระบบ type ทำให้สามารถตรวจสอบเวลาทำงานทั้งหมดได้โดยพิสูจน์ว่ารูปแบบการจับคู่มีความครบถ้วนสมบูรณ์และไม่ซ้ำซ้อน หากคุณพยายามป้อนภาษาเช่น SML, OCaml หรือ F # การจับคู่รูปแบบที่ไม่ครบถ้วนสมบูรณ์หรือมีความซ้ำซ้อนคอมไพเลอร์จะเตือนคุณในเวลาคอมไพล์ นี่เป็นคุณสมบัติที่ทรงพลังอย่างยิ่งเนื่องจากช่วยให้คุณสามารถกำจัดการตรวจสอบเวลาทำงานได้โดยการจัดเรียงรหัสของคุณใหม่กล่าวคือคุณสามารถมีส่วนต่างๆของรหัสที่พิสูจน์แล้วว่าถูก นอกจากนี้ยังเข้าใจง่าย!
JD

@JonHarrop ฉันเห็นว่ามันทำงานอย่างไร (มีประสิทธิภาพคล้ายกับการส่งข้อความแบบไดนามิก) แต่ฉันไม่เห็นว่าคุณเลือกสาขาที่ไม่มีการทดสอบประเภทในเวลาทำงานได้อย่างไร
David Moles

33

คำตอบสั้น ๆ : การจับคู่รูปแบบเกิดขึ้นเนื่องจากภาษาที่ใช้งานได้ถือว่าเครื่องหมายเท่ากับเป็นการยืนยันความเท่าเทียมกันแทนการกำหนด

คำตอบแบบยาว: การจับคู่รูปแบบเป็นรูปแบบของการจัดส่งตาม "รูปร่าง" ของค่าที่กำหนด ในภาษาที่ใช้งานได้ประเภทข้อมูลที่คุณกำหนดมักเป็นสิ่งที่เรียกว่าสหภาพแรงงานที่เลือกปฏิบัติหรือประเภทข้อมูลเกี่ยวกับพีชคณิต ตัวอย่างเช่นรายการ (เชื่อมโยง) คืออะไร? รายการที่เชื่อมโยงListของบางสิ่งบางประเภทaอาจเป็นรายการว่างNilหรือองค์ประกอบบางส่วนของประเภทa Consed ลงในList a(รายการas) ใน Haskell (ภาษาที่ใช้งานได้ฉันคุ้นเคยมากที่สุด) เราเขียนสิ่งนี้

data List a = Nil
            | Cons a (List a)

สหภาพแรงงานที่เลือกปฏิบัติทั้งหมดถูกกำหนดด้วยวิธีนี้: ประเภทเดียวมีจำนวนคงที่ของวิธีต่างๆในการสร้าง ผู้สร้างเช่นNilและConsที่นี่เรียกว่าผู้สร้าง ซึ่งหมายความว่าค่าของประเภทList aสามารถถูกสร้างขึ้นด้วยตัวสร้างที่แตกต่างกันสองตัวซึ่งอาจมีรูปร่างที่แตกต่างกันสองแบบ สมมติว่าเราต้องการเขียน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ค่าสามารถมีได้สองประเภทที่แตกต่างกันเราจึงต้องจัดการแต่ละค่าแยกกัน นี่คือการจับคู่รูปแบบ ในhead xถ้าxตรงกับรูปแบบNilเราจะเรียกใช้กรณีแรก ถ้าตรงกับรูปแบบCons h _เราจะเรียกใช้อันที่สอง

คำตอบสั้น ๆ อธิบาย:ฉันคิดว่าหนึ่งในวิธีที่ดีที่สุดในการคิดเกี่ยวกับพฤติกรรมนี้คือการเปลี่ยนวิธีคิดของเครื่องหมายเท่ากับ ในภาษาหยิกวงเล็บโดยและขนาดใหญ่=หมายถึงการได้รับมอบหมาย: a = bหมายถึง“ให้aเข้าb.” ในจำนวนมากของภาษาการทำงาน แต่=หมายถึงการยืนยันของความเสมอภาค: let Cons a (Cons b Nil) = frob x อ้างว่าสิ่งที่อยู่ทางด้านซ้ายที่Cons a (Cons b Nil)เทียบเท่ากับสิ่งที่ทางด้านขวาที่frob x; นอกจากนี้ตัวแปรทั้งหมดที่ใช้ทางด้านซ้ายจะปรากฏให้เห็น นี่คือสิ่งที่เกิดขึ้นกับอาร์กิวเมนต์ของฟังก์ชัน: เรายืนยันว่าอาร์กิวเมนต์แรกดูเหมือนNilและถ้าไม่เป็นเช่นนั้นเราจะตรวจสอบต่อไป


ช่างเป็นวิธีคิดที่น่าสนใจเกี่ยวกับเครื่องหมายเท่ากับ ขอบคุณสำหรับการแบ่งปัน!
jrahhali

2
อะไรConsหมายถึง?
Roymunson

2
@Roymunson: Consเป็นผู้รักษาข้อเสียที่สร้างรายการ (เชื่อมโยง) จากส่วนหัว (the a) และส่วนหาง (the List a) ชื่อนี้มาจาก Lisp ใน Haskell สำหรับประเภทรายการในตัวจะเป็นตัว:ดำเนินการ (ซึ่งยังคงออกเสียงว่า "ข้อเสีย")
Antal Spector-Zabusky

23

มันหมายความว่าแทนที่จะเขียน

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;
};

1
ใน Scala: import Double._ def div = {values: (Double, Double) => ค่าตรงกับ {case (0,0) => NaN case (x, 0) => if (x> 0) PositiveInfinity else กรณี NegativeInfinity (x, y) => x / y}}
fracca

12

การจับคู่รูปแบบเป็นเหมือนวิธีการที่ใช้สเตียรอยด์มากเกินไป กรณีที่ง่ายที่สุดจะเหมือนกับที่คุณเห็นใน 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 จะเป็นอาร์เรย์โดยจะแยกอาร์กิวเมนต์ที่สองออกจากกันโดยคาดว่าอ็อบเจ็กต์ที่มีอุปกรณ์ประกอบฉากสองตัว (prop1, prop2) และกำหนดค่าของคุณสมบัติเหล่านั้นให้กับตัวแปร d และ e จากนั้นคาดว่าอาร์กิวเมนต์ที่สามจะเป็น 35

ซึ่งแตกต่างจาก JavaScript ตรงที่ภาษาที่มีการจับคู่รูปแบบมักจะอนุญาตหลายฟังก์ชันที่มีชื่อเดียวกัน แต่มีรูปแบบที่แตกต่างกัน ด้วยวิธีนี้ก็เหมือนกับวิธีการโอเวอร์โหลด ฉันจะยกตัวอย่างใน erlang:

fibo(0) -> 0 ;
fibo(1) -> 1 ;
fibo(N) when N > 0 -> fibo(N-1) + fibo(N-2) .

เบลอตาของคุณเล็กน้อยและคุณสามารถจินตนาการได้ในจาวาสคริปต์ สิ่งนี้อาจจะ:

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


8

การจับคู่รูปแบบช่วยให้คุณสามารถจับคู่ค่า (หรือวัตถุ) กับรูปแบบบางอย่างเพื่อเลือกสาขาของรหัส จากมุมมองของ C ++ อาจฟังดูคล้ายกับswitchคำสั่งเล็กน้อย ในภาษาที่ใช้งานได้การจับคู่รูปแบบสามารถใช้สำหรับการจับคู่กับค่าดั้งเดิมมาตรฐานเช่นจำนวนเต็ม อย่างไรก็ตามมีประโยชน์มากกว่าสำหรับประเภทประกอบ

ก่อนอื่นมาสาธิตการจับคู่รูปแบบกับค่าดั้งเดิม (โดยใช้ pseudo-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')
}

ข้อเสนอที่สองใช้กับชนิดข้อมูลทำงานเช่นtuples (ซึ่งช่วยให้คุณสามารถจัดเก็บวัตถุหลายในค่าเดียว) และสหภาพการเลือกปฏิบัติที่ช่วยให้คุณสร้างชนิดที่สามารถมีหนึ่งในหลายตัวเลือก ดูเหมือน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 ++ หลอกของฉันจะทำให้คำอธิบายชัดเจน การเขียนโปรแกรมเชิงฟังก์ชันขึ้นอยู่กับแนวคิดที่แตกต่างกันมากดังนั้นจึงมีความหมายที่ดีกว่าในภาษาที่ใช้งานได้!


5

การจับคู่รูปแบบเป็นที่ที่ล่ามสำหรับภาษาของคุณจะเลือกฟังก์ชันเฉพาะโดยพิจารณาจากโครงสร้างและเนื้อหาของอาร์กิวเมนต์ที่คุณให้ไว้

ไม่เพียง แต่เป็นคุณสมบัติภาษาที่ใช้งานได้เท่านั้น แต่ยังมีให้สำหรับภาษาต่างๆอีกมากมาย

ครั้งแรกที่ฉันได้พบกับความคิดนี้คือเมื่อฉันได้เรียนรู้ prolog ซึ่งเป็นศูนย์กลางของภาษาจริงๆ

เช่น

สุดท้าย ([LastItem], LastItem)

last ([Head | Tail], LastItem): - last (Tail, LastItem)

รหัสด้านบนจะให้รายการสุดท้ายของรายการ อินพุต arg เป็นครั้งแรกและผลลัพธ์คือวินาที

หากมีเพียงรายการเดียวในรายการล่ามจะเลือกเวอร์ชันแรกและอาร์กิวเมนต์ที่สองจะถูกกำหนดให้เท่ากับค่าแรกคือค่าจะถูกกำหนดให้กับผลลัพธ์

หากรายการมีทั้งส่วนหัวและส่วนท้ายล่ามจะเลือกเวอร์ชันที่สองและเรียกคืนจนกว่าจะเหลือเพียงรายการเดียวในรายการ


ดังที่คุณเห็นจากตัวอย่างล่ามยังสามารถแยกอาร์กิวเมนต์เดียวออกเป็นหลายตัวแปรโดยอัตโนมัติ (เช่น [Head | Tail])
charlieb

4

สำหรับหลาย ๆ คนการหยิบแนวคิดใหม่จะง่ายกว่าหากมีตัวอย่างง่ายๆดังนั้นเราไปที่นี่:

สมมติว่าคุณมีรายการจำนวนเต็มสามตัวและต้องการเพิ่มองค์ประกอบแรกและองค์ประกอบที่สาม หากไม่มีการจับคู่รูปแบบคุณสามารถทำได้เช่นนี้ (ตัวอย่างใน Haskell):

Prelude> let is = [1,2,3]
Prelude> head is + is !! 2
4

ตอนนี้แม้ว่านี่จะเป็นตัวอย่างของเล่น แต่ลองนึกภาพว่าเราต้องการผูกจำนวนเต็มแรกและสามเข้ากับตัวแปรและรวมเข้าด้วยกัน:

addFirstAndThird is =
    let first = head is
        third = is !! 3
    in first + third

การแยกค่าจากโครงสร้างข้อมูลนี้เป็นการจับคู่รูปแบบ โดยพื้นฐานแล้วคุณ "สะท้อน" โครงสร้างของบางสิ่งโดยให้ตัวแปรเชื่อมโยงกับสถานที่ที่น่าสนใจ:

addFirstAndThird [first,_,third] = first + third

เมื่อคุณเรียกใช้ฟังก์ชันนี้โดยมี [1,2,3] เป็นอาร์กิวเมนต์ [1,2,3] จะรวมเข้าด้วยกันกับ [แรก_, สาม], การเชื่อมโยงครั้งแรกกับ 1, สามถึง 3 และทิ้ง 2 ( _เป็นตัวยึด สำหรับสิ่งที่คุณไม่สนใจ)

ตอนนี้หากคุณต้องการจับคู่รายการที่มี 2 เป็นองค์ประกอบที่สองคุณสามารถทำได้ดังนี้:

addFirstAndThird [first,2,third] = first + third

สิ่งนี้จะใช้ได้เฉพาะกับรายการที่มี 2 เป็นองค์ประกอบที่สองเท่านั้นและจะมีข้อยกเว้นเป็นอย่างอื่นเนื่องจากไม่มีการกำหนดคำจำกัดความสำหรับ addFirstAndThird สำหรับรายการที่ไม่ตรงกัน

จนถึงตอนนี้เราใช้การจับคู่รูปแบบสำหรับการผูกแบบทำลายโครงสร้างเท่านั้น เหนือสิ่งนั้นคุณสามารถให้คำจำกัดความของฟังก์ชันเดียวกันได้หลายคำโดยใช้คำจำกัดความการจับคู่แรกดังนั้นการจับคู่รูปแบบจึงคล้ายกับ "คำสั่งสวิตช์บนสเตอริโอ":

addFirstAndThird [first,2,third] = first + third
addFirstAndThird _ = 0

addFirstAndThird จะเพิ่มองค์ประกอบที่หนึ่งและสามของรายการโดยมี 2 เป็นองค์ประกอบที่สองอย่างมีความสุขมิฉะนั้นจะ "ผ่าน" และ "ส่งคืน" 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

นอกจากนี้ยังไม่ จำกัด เฉพาะรายการ แต่สามารถใช้กับประเภทอื่น ๆ ได้เช่นการจับคู่ตัวสร้างค่า 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

แน่นอนว่าสิ่งเหล่านี้เป็นเพียงตัวอย่างของเล่นเท่านั้นและฉันไม่ได้พยายามอธิบายอย่างเป็นทางการหรือละเอียดถี่ถ้วน แต่ควรพอที่จะเข้าใจแนวคิดพื้นฐาน


3

คุณควรเริ่มต้นด้วยหน้า Wikipediaที่ให้คำอธิบายที่ดีทีเดียว จากนั้นอ่านบทที่เกี่ยวข้องของWikibook Haskell

นี่เป็นคำจำกัดความที่ดีจากวิกิตำราด้านบน:

ดังนั้นการจับคู่รูปแบบจึงเป็นวิธีการกำหนดชื่อให้กับสิ่งต่างๆ (หรือผูกชื่อกับสิ่งเหล่านั้น) และอาจแยกนิพจน์ออกเป็นนิพจน์ย่อยในเวลาเดียวกัน (เช่นเดียวกับที่เราทำกับรายการในนิยามของแผนที่)


3
ครั้งหน้าฉันจะพูดถึงคำถามที่ว่าฉันอ่านวิกิพีเดียไปแล้วและมันให้คำอธิบายที่แย่มาก
โรมัน

2

นี่คือตัวอย่างสั้น ๆ ที่แสดงประโยชน์ในการจับคู่รูปแบบ:

สมมติว่าคุณต้องการจัดเรียงองค์ประกอบในรายการ:

["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)

ดังที่คุณเห็นโซลูชันที่จับคู่รูปแบบมีสัญญาณรบกวนน้อยลงคุณจะเห็นได้ชัดเจนว่าอะไรคือกรณีที่แตกต่างกันและความสะดวกในการเดินทางและจัดโครงสร้างรายการของเรา

ผมเคยเขียนบล็อกโพสต์รายละเอียดเพิ่มเติมเกี่ยวกับมันนี่

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.