アクティブ パターン

"アクティブ パターン" を使用すると、入力データを分割する名前付きパーティションを定義できます。これにより、判別共用体の場合と同様に、パターン マッチング式でこれらの名前を使用できるようになります。 アクティブ パターンを使用すると、パーティションごとにカスタマイズした方法でデータを分解できます。

構文

// 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.
// Uses a FSharp.Core.option<_> to represent if the type is satisfied at the call site.
let (|identifier|_|) [arguments] valueToMatch = expression

解説

前の構文では、識別子は "引数" によって表される入力データのパーティションの名前、つまり引数のすべての値のセットのサブセットの名前です。 アクティブ パターンの定義で、最大 7 つのパーティションを指定できます。 "" では、データを分解する形式を記述します。 アクティブ パターン定義を使用すると、引数として指定された値を割り当てる名前付きパーティションを決定するためのルールを定義できます。 (| と |) の記号は "バナナ クリップ" と呼ばれ、この型の let バインドによって作成される関数は、"アクティブ認識エンジン" と呼ばれます。

例として、引数を持つ次のアクティブ パターンについて考えます。

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

アクティブ パターンのもう 1 つの用途は、基になるデータは同じでも、さまざまな表現の可能性がある場合のように、データ型を複数の方法で分解することです。 たとえば、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

アクティブ パターンのこれら 2 つの使用方法を組み合わせることで、データを適切な形式に分割および分解し、計算に最も便利な形式を使用して、適切なデータに対して適切な計算を実行できます。

結果として得られるパターン マッチング式により、非常に読みやすい方法でデータを書き込むことができるため、複雑になる可能性のある分岐やデータ分析のコードを大幅に簡素化できます。

部分的なアクティブ パターン

場合によっては、入力領域の一部だけをパーティション分割する必要があります。 その場合は、一部の入力には一致しても、他の入力とは一致しない、部分パターンのセットを記述します。 値が生成されないことのあるアクティブ パターンは、"部分的なアクティブ パターン" と呼ばれます。これらには、オプションの型である戻り値があります。 部分的なアクティブ パターンを定義するには、バナナ クリップ内のパターンのリストの末尾にワイルドカード文字 (_) を使用します。 次のコードは、部分的なアクティブ パターンの使用方法を示したものです。

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.

部分的なアクティブ パターンを使用する場合、個別の選択肢を互いに素または相互に排他的にすることができますが、そうする必要はありません。 次の例では、パターン 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

パラメーター化されたアクティブ パターン

アクティブ パターンは、常に、照合対象の項目について少なくとも 1 つの引数を受け取りますが、追加の引数を受け取る可能性があります。この場合は、"パラメーター化されたアクティブ パターン" と呼ばれます。 追加の引数を使用すると、一般的なパターンを特殊化できます。 たとえば、正規表現を使用して文字列を解析するアクティブ パターンでは、次のコードに示すように、正規表現を追加のパラメーターとして含めることがよくあります。これには、前のコード例で定義した部分的なアクティブ パターン Integer も使用されています。 この例では、さまざまな日付形式の正規表現を使用する文字列が、一般的な ParseRegex アクティブ パターンをカスタマイズするために指定されています。 一致した文字列を DateTime コンストラクターに渡すことができる整数に変換するために、Integer アクティブ パターンが使用されています。

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!

ただし、パラメーター化できるのは、単一ケースのアクティブ パターンだけであることに注意してください。

// 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

部分的なアクティブ パターンの構造体表現

部分的なアクティブ パターンでは、一致が見つかった場合に Some 値を割り当てる option 値を既定で返します。 これの代わりに、Struct 属性を使用して、値のオプションを戻り値として使用することもできます。

open System

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

構造体の戻り値の使用は、単に戻り値の型を ValueOption に変更するだけでは推論されないため、この属性を指定する必要があります。 詳細については、「RFC FS-1039」を参照してください。

関連項目