답변:
패턴 일치를 이해하려면 다음 세 부분을 설명해야합니다.
한마디로 대수 데이터 유형
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
Cons
ed 유형의 일부 요소입니다 . Haskell (가장 익숙한 기능 언어)에서 다음과 같이 씁니다.List a
a
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 a
into "를 의미 b
합니다. 그러나 많은 기능적 언어에서 =
평등의 주장을 나타냅니다 . 왼쪽 에있는 것은 오른쪽에있는 것과 같다고 let Cons a (Cons b Nil) = frob x
주장 합니다 . 또한 왼쪽에 사용 된 모든 변수가 표시됩니다. 이것은 또한 함수 인수에서 일어나는 일입니다. 우리는 첫 번째 인수가 다음과 같다고 주장하고, 그렇지 않으면 계속 확인합니다.Cons a (Cons b Nil)
frob x
Nil
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)
패턴 일치 솔루션의 소음이 적다는 것을 알 수 있듯이 다양한 사례가 무엇인지, 목록을 쉽게 이동하고 구조화하는 것이 얼마나 쉬운 지 알 수 있습니다.
여기 에 대한 자세한 블로그 게시물을 작성했습니다 .