기능적 언어에서 '패턴 일치'란 무엇입니까?


답변:


141

패턴 일치를 이해하려면 다음 세 부분을 설명해야합니다.

  1. 대수 데이터 유형.
  2. 어떤 패턴 일치
  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;
        }
    }
}

따라서 ConsNil식별자는 간단한 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 # 코드는 다소 기만적입니다. 구현 세부 정보를 손으로 흔들며 적어주십시오 :))

간단히 말해서 데이터 구조 분해

좋아, peek 방법으로 돌아가 봅시다.

let peek s =
    match s with
    | Cons(hd, tl) -> hd
    | Nil -> failwith "Empty stack"

트릭은 hdtl식별자가 변수 라는 것을 이해하고 있습니다 (errm ... 불변이기 때문에 실제로는 "변수"가 아니라 "값";)). 경우 s유형이있다 Cons, 우리는 생성자에서 그 값을 끌어가는 바인드 그들라는 이름의 변수로하고 hdtl.

패턴 일치는 데이터 구조를 내용 대신 모양으로 분해 할 수 있기 때문에 유용합니다 . 따라서 다음과 같이 이진 트리를 정의한다고 상상해보십시오.

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 ....)

따라서 데이터 구조를 변수에 바인딩하는 것 외에도 드릴 다운 할 수 있습니다. node가 있다고 가정 해 봅시다 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
변수 "하지만,"값 "그들이 불변있어 이후 ERRM ..., 그들은 정말이야" ";)"그들은 이다 변수; 라벨이 잘못 지정된 가변 품종입니다 . 그럼에도 불구하고 훌륭한 답변입니다!
Doval

3
"언제나 ML 언어는 런타임 유형 테스트 나 캐스트없이 패턴 일치를 구현합니다."<-어떻게 작동합니까? 내가 뇌관을 알려줄 수 있습니까?
David Moles

1
@DavidMoles : 타입 시스템을 통해 패턴 일치가 철저하고 중복되지 않도록 모든 런타임 검사를 제거 할 수 있습니다. SML, OCaml 또는 F #과 같은 언어를 철저하게 사용하지 않거나 중복성을 포함하는 패턴 일치를 제공하려고하면 컴파일러가 컴파일 타임에 경고합니다. 이 기능은 코드를 다시 정렬하여 런타임 검사를 제거 할 수있게 해주는 매우 강력한 기능입니다. 즉, 코드의 측면이 올바른 것으로 입증 될 수 있습니다. 또한 이해하기 쉽습니다!
JD

@ JonHarrop 작동 방식을 볼 수 있지만 (동적 메시지 디스패치와 유사) 런타임에 유형 테스트없이 분기를 선택하는 방법을 볼 수는 없습니다.
David Moles

33

짧은 대답 : 기능적 언어는 등호 를 대입 대신 동등성 주장 으로 취급하기 때문에 패턴 일치가 발생 합니다.

긴 대답 : 패턴 일치는 주어진 값의 "모양"을 기반으로하는 디스패치 형태입니다. 기능적 언어에서 사용자가 정의한 데이터 유형은 일반적으로 차별적 조합 또는 대수 데이터 유형입니다. 예를 들어 (연결된) 목록은 무엇입니까? List어떤 유형의 것들에 대한 링크 된 목록 은 a빈 목록 Nil이거나 ( s 목록)에 a Consed 유형의 일부 요소입니다 . Haskell (가장 익숙한 기능 언어)에서 다음과 같이 씁니다.List aa

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

모든 차별적 노동 조합은 이런 식으로 정의된다 : 단일 유형은 그것을 생성하는 고정 된 수의 다른 방법을 가진다; 제작자는 같은 NilCons여기에 생성자라고합니다. 이것은 타입의 값이 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은 "make ainto "를 의미 b합니다. 그러나 많은 기능적 언어에서 =평등의 주장을 나타냅니다 . 왼쪽 에있는 것은 오른쪽에있는 것과 같다고 let Cons a (Cons b Nil) = frob x 주장 합니다 . 또한 왼쪽에 사용 된 모든 변수가 표시됩니다. 이것은 또한 함수 인수에서 일어나는 일입니다. 우리는 첫 번째 인수가 다음과 같다고 주장하고, 그렇지 않으면 계속 확인합니다.Cons a (Cons b Nil)frob xNil


등호에 대해 얼마나 재미있는 생각입니까? 공유해 주셔서 감사합니다!
jrahhali

2
무슨 Cons뜻입니까?
Roymunson 2019 년

2
@Roymunson는 : Cons는 IS 죄수의 머리 밖으로 (연결) 목록을 작성 tructor합니다 ( a)와 꼬리합니다 ( List a). 이름은 Lisp에서 나왔습니다. Haskell에서 내장 목록 유형의 경우 :연산자입니다 (여전히 "cons"라고 발음 됨).
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
스칼라에서 : Import Double._ def split = {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는 배열이 될 것으로 예상하고 두 개의 props (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를 호출 할 때 fibo를 사용하는 구현은 인수를 기반으로하지만 오버로드의 유일한 수단으로 Java가 유형으로 제한되는 경우 패턴 일치로 더 많은 작업을 수행 할 수 있습니다.

여기에 표시된 기능 오버로드 외에도 사례 설명 또는 구조적 어설 션 제거와 같은 다른 위치에 동일한 원칙을 적용 할 수 있습니다. JavaScript는 1.7에서도 이것을 가지고 있습니다.


8

패턴 일치를 사용하면 일부 패턴과 값 (또는 객체)을 일치시켜 코드 분기를 선택할 수 있습니다. 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')
}

두 번째 용도는 여러 객체를 단일 값으로 저장할 수있는 튜플 및 여러 옵션 중 하나를 포함 할 수있는 유형을 만들 수있는 구별 된 공용체 와 같은 기능적 데이터 유형을 다룹니다 . 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

패턴 일치는 언어의 인터프리터가 사용자가 제공 한 인수의 구조와 내용에 따라 특정 기능을 선택하는 곳입니다.

이 기능은 기능적 언어 기능 일뿐만 아니라 다양한 언어로 제공됩니다.

제가이 아이디어를 처음 접했을 때 언어의 중심에있는 프롤로그를 배웠습니다.

예 :

last ([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

데이터 구조에서 값을 추출하면 패턴 일치가 수행됩니다. 기본적으로 무언가의 구조를 "미러링 (mirror)"하여 관심있는 장소에 바인딩 할 변수를 제공합니다.

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

[1,2,3]을 인수로 사용하여이 함수를 호출하면 [1,2,3]이 [first _,, third] 로 통일되어 1에서 1, 3에서 3으로 바인딩되고 2를 버립니다 ( _자리 표시 자임) 걱정하지 않는 것).

이제 두 번째 요소가 2 인 목록 만 일치 시키려면 다음과 같이하십시오.

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

일치하지 않는 목록에 대해 addFirstAndThird에 대한 정의가 제공되지 않으므로 이는 두 번째 요소가 2 인 목록에 대해서만 작동하며 그렇지 않으면 예외가 발생합니다.

지금까지는 바인딩 바인딩을 제거 할 때만 패턴 일치를 사용했습니다. 그 위에, 첫 번째 일치하는 정의가 사용되는 동일한 함수에 대한 여러 정의를 제공 할 수 있으므로 패턴 일치는 "스테레오 이드의 switch 문"과 비슷합니다.

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

addFirstAndThird는 두 번째 요소로 2를 사용하여 목록의 첫 번째 및 세 번째 요소를 행복하게 추가하고 그렇지 않으면 "fall through"및 "return"0을 추가합니다.이 "switch-like"기능은 함수 정의에서만 사용할 수 없습니다. 예 :

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

물론, 그것들은 단순한 장난감의 예일 뿐이며, 공식적이거나 철저한 설명조차하지 않았지만 기본 개념을 이해하기에 충분해야합니다.


3

꽤 좋은 설명을 제공하는 Wikipedia 페이지로 시작해야합니다 . 그런 다음 Haskell wikibook 의 관련 장을 읽으십시오 .

위의 위키 북에서 좋은 정의입니다.

따라서 패턴 일치는 이름을 사물에 할당하거나 그 이름을 그 사물에 바인딩하는 방법이며, 맵 정의에서 목록에서와 같이 표현식을 동시에 하위 표현식으로 분류 할 수 있습니다.


3
다음에 나는 이미 wikipedia를 읽었으며 매우 나쁜 설명을 제공한다는 질문에 언급 할 것입니다.
로마

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.