답변:
패턴 일치를 이해하려면 다음 세 부분을 설명해야합니다.
한마디로 대수 데이터 유형
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 # 코드는 다소 기만적입니다. 구현 세부 정보를 손으로 흔들며 적어주십시오 :))
간단히 말해서 데이터 구조 분해
좋아, peek 방법으로 돌아가 봅시다.
let peek s =
match s with
| Cons(hd, tl) -> hd
| Nil -> failwith "Empty stack"
트릭은 hd및 tl식별자가 변수 라는 것을 이해하고 있습니다 (errm ... 불변이기 때문에 실제로는 "변수"가 아니라 "값";)). 경우 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 ....)
따라서 데이터 구조를 변수에 바인딩하는 것 외에도 드릴 다운 할 수 있습니다. 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 # 또는 언어로 유사한 기능을 구현하는 방법을 생각해보십시오. 런타임에 테스트 테스트 및 캐스트없이 어떻게 할 것인지 생각하십시오. 확실히 어렵지 않고 번거롭고 부피가 큽니다. 그리고 모든 경우를 다룰 수 있는지 확인하는 컴파일러가 없습니다.
따라서 패턴 일치는 매우 편리하고 간결한 구문으로 데이터 구조를 분해하고 탐색하는 데 도움이되며, 컴파일러는 코드 의 논리 를 조금이라도 확인할 수 있습니다. 정말 이다 킬러 기능입니다.
짧은 대답 : 기능적 언어는 등호 를 대입 대신 동등성 주장 으로 취급하기 때문에 패턴 일치가 발생 합니다.
긴 대답 : 패턴 일치는 주어진 값의 "모양"을 기반으로하는 디스패치 형태입니다. 기능적 언어에서 사용자가 정의한 데이터 유형은 일반적으로 차별적 조합 또는 대수 데이터 유형입니다. 예를 들어 (연결된) 목록은 무엇입니까? List어떤 유형의 것들에 대한 링크 된 목록 은 a빈 목록 Nil이거나 ( s 목록)에 a Consed 유형의 일부 요소입니다 . Haskell (가장 익숙한 기능 언어)에서 다음과 같이 씁니다.List aa
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은 "make ainto "를 의미 b합니다. 그러나 많은 기능적 언어에서 =평등의 주장을 나타냅니다 . 왼쪽 에있는 것은 오른쪽에있는 것과 같다고 let Cons a (Cons b Nil) = frob x 주장 합니다 . 또한 왼쪽에 사용 된 모든 변수가 표시됩니다. 이것은 또한 함수 인수에서 일어나는 일입니다. 우리는 첫 번째 인수가 다음과 같다고 주장하고, 그렇지 않으면 계속 확인합니다.Cons a (Cons b Nil)frob xNil
Cons뜻입니까?
Cons는 IS 죄수의 머리 밖으로 (연결) 목록을 작성 tructor합니다 ( a)와 꼬리합니다 ( List a). 이름은 Lisp에서 나왔습니다. Haskell에서 내장 목록 유형의 경우 :연산자입니다 (여전히 "cons"라고 발음 됨).
그것은 쓰는 대신에
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;
};
패턴 매칭은 스테로이드에 오버로드 된 방법과 비슷합니다. 가장 간단한 경우는 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에서도 이것을 가지고 있습니다.
패턴 일치를 사용하면 일부 패턴과 값 (또는 객체)을 일치시켜 코드 분기를 선택할 수 있습니다. 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 ++이 설명을 명확하게하기를 바랍니다. 함수형 프로그래밍은 매우 다른 개념을 기반으로하므로 함수형 언어로 이해하는 것이 좋습니다!
패턴 일치는 언어의 인터프리터가 사용자가 제공 한 인수의 구조와 내용에 따라 특정 기능을 선택하는 곳입니다.
이 기능은 기능적 언어 기능 일뿐만 아니라 다양한 언어로 제공됩니다.
제가이 아이디어를 처음 접했을 때 언어의 중심에있는 프롤로그를 배웠습니다.
예 :
last ([LastItem], LastItem).
last ([Head | Tail], LastItem) :-last (Tail, LastItem)입니다.
위의 코드는 목록의 마지막 항목을 제공합니다. 입력 arg가 첫 번째이고 결과가 두 번째입니다.
목록에 하나의 항목 만있는 경우 인터프리터는 첫 번째 버전을 선택하고 두 번째 인수는 첫 번째 버전과 동일하게 설정됩니다. 즉, 값이 결과에 지정됩니다.
목록에 머리와 꼬리가 모두 있으면 통역사는 두 번째 버전을 선택하고 목록에 항목이 하나만 남을 때까지 재귀합니다.
많은 사람들에게 쉬운 예제가 제공되면 새로운 개념을 선택하는 것이 더 쉬워집니다.
세 개의 정수 목록이 있고 첫 번째와 세 번째 요소를 추가하려고한다고 가정 해 봅시다. 패턴 일치가 없으면 다음과 같이 할 수 있습니다 (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
물론, 그것들은 단순한 장난감의 예일 뿐이며, 공식적이거나 철저한 설명조차하지 않았지만 기본 개념을 이해하기에 충분해야합니다.
꽤 좋은 설명을 제공하는 Wikipedia 페이지로 시작해야합니다 . 그런 다음 Haskell wikibook 의 관련 장을 읽으십시오 .
위의 위키 북에서 좋은 정의입니다.
따라서 패턴 일치는 이름을 사물에 할당하거나 그 이름을 그 사물에 바인딩하는 방법이며, 맵 정의에서 목록에서와 같이 표현식을 동시에 하위 표현식으로 분류 할 수 있습니다.
다음은 패턴 일치 유용성을 보여주는 매우 짧은 예입니다.
목록에서 요소를 정렬한다고 가정 해 봅시다.
["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)
패턴 일치 솔루션의 소음이 적다는 것을 알 수 있듯이 다양한 사례가 무엇인지, 목록을 쉽게 이동하고 구조화하는 것이 얼마나 쉬운 지 알 수 있습니다.
여기 에 대한 자세한 블로그 게시물을 작성했습니다 .