액티브 패턴

액티브 패턴 (Active Patterns)

액티브 패턴은 들어오는 데이터를 여러 개의 이름 붙은 구역(partition)으로 나눠주는 기능이에요. 이렇게 만든 이름은 패턴 매칭 표현식에서 식별 공용체(discriminated union)처럼 그대로 쓸 수 있죠. 데이터를 각 구역에 맞게 원하는 방식으로 분해하고 싶을 때 액티브 패턴을 활용하면 돼요.

출처: https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/active-patterns

본문

구문 (Syntax)

// Active pattern of one choice.
let (|identifier|) [arguments] valueToMatch = expression

// Active Pattern with multiple choices.
// Uses a FSharp.Core.Choice<_,...,_> based on the number of case names. In F#, the limitation n <= 7 applies.
let (|identifier1|identifier2|...|) valueToMatch = expression

// Partial active pattern definition.
// Can use FSharp.Core.option<_>, FSharp.Core.voption<_> or bool to represent if the type is satisfied at the call site.
let (|identifier|_|) [arguments] valueToMatch = expression

설명 (Remarks)

위 구문에서 identifiers(식별자)는 arguments로 표현되는 입력 데이터를 나눈 구역의 이름, 다시 말해 인자의 전체 값 집합 중 부분집합들의 이름이에요. 액티브 패턴 정의에는 최대 7개까지 구역을 만들 수 있어요. expression은 데이터를 어떤 형태로 분해할지를 나타내죠. 인자로 주어진 값이 각 이름 붙은 구역 중 어디에 속하는지 판단하는 규칙을 액티브 패턴 정의로 만들 수 있어요. (||) 기호는 **바나나 클립(banana clips)**이라고 부르고, 이런 let 바인딩으로 만들어진 함수는 **액티브 리코그나이저(active recognizer)**라고 해요.

예를 들어 인자를 하나 받는 다음 액티브 패턴을 볼게요.

let (|Even|Odd|) input = if input % 2 = 0 then Even else Odd

이 액티브 패턴은 다음과 같은 패턴 매칭 표현식에서 쓸 수 있어요.

let TestNumber input =
   match input with
   | Even -> printfn "%d is even" input
   | Odd -> printfn "%d is odd" input

TestNumber 7
TestNumber 11
TestNumber 32

이 프로그램의 출력은 다음과 같아요.

7 is odd
11 is odd
32 is even

액티브 패턴의 또 다른 용도는 같은 기반 데이터를 여러 가지 방식으로 분해하는 거예요. 예를 들어 Color 객체를 RGB 표현으로 분해할 수도 있고, HSB 표현으로 분해할 수도 있죠.

open System.Drawing

let (|RGB|) (col : System.Drawing.Color) =
     ( col.R, col.G, col.B )

let (|HSB|) (col : System.Drawing.Color) =
   ( col.GetHue(), col.GetSaturation(), col.GetBrightness() )

let printRGB (col: System.Drawing.Color) =
   match col with
   | RGB(r, g, b) -> printfn " Red: %d Green: %d Blue: %d" r g b

let printHSB (col: System.Drawing.Color) =
   match col with
   | HSB(h, s, b) -> printfn " Hue: %f Saturation: %f Brightness: %f" h s b

let printAll col colorString =
  printfn "%s" colorString
  printRGB col
  printHSB col

printAll Color.Red "Red"
printAll Color.Black "Black"
printAll Color.White "White"
printAll Color.Gray "Gray"
printAll Color.BlanchedAlmond "BlanchedAlmond"

위 프로그램의 출력은 다음과 같아요.

Red
 Red: 255 Green: 0 Blue: 0
 Hue: 360.000000 Saturation: 1.000000 Brightness: 0.500000
Black
 Red: 0 Green: 0 Blue: 0
 Hue: 0.000000 Saturation: 0.000000 Brightness: 0.000000
White
 Red: 255 Green: 255 Blue: 255
 Hue: 0.000000 Saturation: 0.000000 Brightness: 1.000000
Gray
 Red: 128 Green: 128 Blue: 128
 Hue: 0.000000 Saturation: 0.000000 Brightness: 0.501961
BlanchedAlmond
 Red: 255 Green: 235 Blue: 205
 Hue: 36.000000 Saturation: 1.000000 Brightness: 0.901961

이 두 가지 액티브 패턴 용법을 조합하면 데이터를 계산에 꼭 맞는 형태로 분할·분해하고, 그 형태가 가장 편리한 데이터에 알맞은 계산을 수행할 수 있어요.

그렇게 만들어진 패턴 매칭 표현식은 코드를 읽기 편하면서도 간결하게 작성하게 해줘요. 복잡해지기 쉬운 분기나 데이터 분석 코드를 크게 단순화할 수 있죠.

부분 액티브 패턴 (Partial Active Patterns)

때로는 입력 공간의 일부만 나눠야 할 때가 있어요. 그럴 땐 각각 어떤 입력은 매칭하지만 다른 입력은 매칭하지 못하는 부분 패턴들을 여러 개 작성하면 돼요. 항상 값을 만들어내지는 않는 액티브 패턴을 **부분 액티브 패턴(partial active patterns)**이라고 하는데, 반환 값이 option 형식이에요. 부분 액티브 패턴을 정의할 때는 바나나 클립 안의 패턴 목록 끝에 와일드카드 문자(_)를 붙여요. 다음 코드가 부분 액티브 패턴의 사용 예시예요.

let (|Integer|_|) (str: string) =
   let mutable intvalue = 0
   if System.Int32.TryParse(str, &intvalue) then Some(intvalue)
   else None

let (|Float|_|) (str: string) =
   let mutable floatvalue = 0.0
   if System.Double.TryParse(str, &floatvalue) then Some(floatvalue)
   else None

let parseNumeric str =
   match str with
   | Integer i -> printfn "%d : Integer" i
   | Float f -> printfn "%f : Floating point" f
   | _ -> printfn "%s : Not matched." str

parseNumeric "1.1"
parseNumeric "0"
parseNumeric "0.0"
parseNumeric "10"
parseNumeric "Something else"

앞 예시의 출력은 다음과 같아요.

1.100000 : Floating point
0 : Integer
0.000000 : Floating point
10 : Integer
Something else : Not matched.

부분 액티브 패턴을 쓸 때 개별 선택지는 서로 배타적(disjoint)일 때도 있지만, 꼭 그래야 하는 건 아니에요. 다음 예시에서 Square 패턴과 Cube 패턴은 서로 배타적이지 않아요. 64처럼 제곱수이면서 세제곱수인 숫자도 있기 때문이죠. 아래 프로그램은 AND 패턴으로 Square 패턴과 Cube 패턴을 결합해요. 1000까지의 정수 중 제곱수이면서 세제곱수인 것과 세제곱수만인 것을 모두 출력해요.

let err = 1.e-10

let isNearlyIntegral (x:float) = abs (x - round(x)) < err

let (|Square|_|) (x : int) =
  if isNearlyIntegral (sqrt (float x)) then Some(x)
  else None

let (|Cube|_|) (x : int) =
  if isNearlyIntegral ((float x) ** ( 1.0 / 3.0)) then Some(x)
  else None

let findSquareCubes x =
   match x with
   | Cube x & Square _ -> printfn "%d is a cube and a square" x
   | Cube x -> printfn "%d is a cube" x
   | _ -> ()
   

[ 1 .. 1000 ] |> List.iter (fun elem -> findSquareCubes elem)

출력은 다음과 같아요.

1 is a cube and a square
8 is a cube
27 is a cube
64 is a cube and a square
125 is a cube
216 is a cube
343 is a cube
512 is a cube
729 is a cube and a square
1000 is a cube

매개변수화된 액티브 패턴 (Parameterized Active Patterns)

액티브 패턴은 매칭 대상 항목을 위한 인자를 하나 이상은 항상 받는데, 추가 인자를 더 받을 수도 있어요. 이 경우 매개변수화된 액티브 패턴(parameterized active pattern)이라는 이름이 붙어요. 추가 인자를 쓰면 일반적인 패턴을 특수화할 수 있어요. 예를 들어 정규식을 이용해 문자열을 파싱하는 액티브 패턴은 정규식을 추가 매개변수로 받는 경우가 많아요. 다음 코드가 그 예다운 예시인데, 앞선 코드 예시에서 정의한 부분 액티브 패턴 Integer도 함께 사용해요. 이 예시에서는 다양한 날짜 형식에 쓰는 정규식을 문자열로 넘겨서 일반적인 ParseRegex 액티브 패턴을 맞춤화해요. Integer 액티브 패턴은 매칭된 문자열을 정수로 변환해서 DateTime 생성자에 넘길 수 있게 해줘요.

open System.Text.RegularExpressions

// ParseRegex parses a regular expression and returns a list of the strings that match each group in
// the regular expression.
// List.tail is called to eliminate the first element in the list, which is the full matched expression,
// since only the matches for each group are wanted.
let (|ParseRegex|_|) regex str =
   let m = Regex(regex).Match(str)
   if m.Success
   then Some (List.tail [ for x in m.Groups -> x.Value ])
   else None

// Three different date formats are demonstrated here. The first matches two-
// digit dates and the second matches full dates. This code assumes that if a two-digit
// date is provided, it is an abbreviation, not a year in the first century.
let parseDate str =
   match str with
   | ParseRegex "(\d{1,2})/(\d{1,2})/(\d{1,2})$" [Integer m; Integer d; Integer y]
          -> new System.DateTime(y + 2000, m, d)
   | ParseRegex "(\d{1,2})/(\d{1,2})/(\d{3,4})" [Integer m; Integer d; Integer y]
          -> new System.DateTime(y, m, d)
   | ParseRegex "(\d{1,4})-(\d{1,2})-(\d{1,2})" [Integer y; Integer m; Integer d]
          -> new System.DateTime(y, m, d)
   | _ -> new System.DateTime()

let dt1 = parseDate "12/22/08"
let dt2 = parseDate "1/1/2009"
let dt3 = parseDate "2008-1-15"
let dt4 = parseDate "1995-12-28"

printfn "%s %s %s %s" (dt1.ToString()) (dt2.ToString()) (dt3.ToString()) (dt4.ToString())

앞 코드의 출력은 다음과 같아요.

12/22/2008 12:00:00 AM 1/1/2009 12:00:00 AM 1/15/2008 12:00:00 AM 12/28/1995 12:00:00 AM

액티브 패턴은 패턴 매칭 표현식에만 쓸 수 있는 게 아니라 let 바인딩에서도 사용할 수 있어요.

let (|Default|) onNone value =
    match value with
    | None -> onNone
    | Some e -> e

let greet (Default "random citizen" name) =
    printfn "Hello, %s!" name

greet None
greet (Some "George")

앞 코드의 출력은 다음과 같아요.

Hello, random citizen!
Hello, George!

다만 매개변수화할 수 있는 건 단일 케이스(single-case) 액티브 패턴뿐이라는 점을 기억해야 해요.

// A single-case partial active pattern can be parameterized
let (| Foo|_|) s x = if x = s then Some Foo else None
// A multi-case active patterns cannot be parameterized
// let (| Even|Odd|Special |) (s: int) (x: int) = if x = s then Special elif x % 2 = 0 then Even else Odd

부분 액티브 패턴의 반환 형식 (Return Type for Partial Active Patterns)

부분 액티브 패턴은 매칭에 성공하면 Some ()을 반환하고, 그 외에는 None을 반환해요.

다음 매칭을 생각해 볼게요.

match key with
| CaseInsensitive "foo" -> ...
| CaseInsensitive "bar" -> ...

이에 대응하는 부분 액티브 패턴은 다음과 같아요.

let (|CaseInsensitive|_|) (pattern: string) (value: string) =
    if String.Equals(value, pattern, StringComparison.OrdinalIgnoreCase) then
        Some ()
    else
        None

F# 9부터는 이런 패턴이 bool을 반환해도 돼요.

let (|CaseInsensitive|_|) (pattern: string) (value: string) =
    String.Equals(value, pattern, StringComparison.OrdinalIgnoreCase)

부분 액티브 패턴의 구조체 표현 (Struct Representations for Partial Active Patterns)

기본적으로 부분 액티브 패턴이 option을 반환하면 매칭에 성공한 경우 Some 값을 위해 할당(allocation)이 발생해요. 이를 피하려면 Struct 특성을 사용해서 반환 값을 value option으로 만들면 돼요.

open System

[<return: Struct>]
let (|Int|_|) str =
   match Int32.TryParse(str) with
   | (true, n) -> ValueSome n
   | _ -> ValueNone

이 특성은 반드시 지정해야 해요. 반환 형식을 ValueOption으로 바꾸는 것만으로는 구조체 반환이 추론되지 않거든요. 자세한 내용은 RFC FS-1039를 참고하세요.

Null 액티브 패턴 (Null active patterns)

F# 9에서 nullability와 관련된 액티브 패턴이 추가됐어요.

먼저 | Null | NonNull x |가 있는데, 가능한 null을 다루는 권장 방식이에요. 다음 예시에서 매개변수 s는 이 액티브 패턴을 사용함으로써 nullable로 추론돼요.

 let len s =
    match s with
    | Null -> -1
    | NonNull s -> String.length s

대신 NullReferenceException을 자동으로 던지게 하고 싶다면 | NonNullQuick | 패턴을 쓸 수 있어요.

let len (NonNullQuick str) =  // throws if the argument is null
    String.length str

더 알아보기 (Learn more)