memo.txt

教えていただいたこと、勉強したことのメモです。

アクティブパターン

「match式だけがパターンマッチだと思うなよ」 - memo.txt

本当はこの記事に入るといいねって言われていたアクティブパターンについて。

アクティブ パターン (F#)

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

アクティブパターンは関数

定義する

  • (||)をバナナクリップと呼ぶ。
  • let (|パターン識別子|) 引数 = パターン識別子(戻り値)の書式で定義する。
  • パターン識別子は大文字始まり。
  • (戻り値)部分は任意。
  • パターン識別子が1つの場合はlet (|パターン識別子|) 引数 = 戻り値でも定義できる。
let (|Upper|)(str:string) = Upper(str.ToUpper())

引数の文字列を大文字にするstring -> string の関数です。

match式の分岐で使う

  • match x with のxを暗黙的に引数にする。
  • アクティブパターンの戻り値とパターンを比較する。
let f (str:string) =
    match str with
    | Upper "HOGE" -> "HOGE"
    | Upper "PIYO" -> "PIYO"
    | x -> "other"

Upperの引数にstrが渡され、大文字になったstrとパターンを比較します。

f "hoge" (* "HOGE" *)
f "HOGE" (* "HOGE" *)
f "Hoge" (* "HOGE" *)
f "pIyO" (* "PIYO" *)
f "Foo" (* "other" *)
f "" (* "other" *)

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

  • 最後以外の引数は前から順に部分適用される。
  • 最後の引数はmatch x with のxを暗黙的に使う。
let (|Reg|) (pattern:string) (str:string) =
   Regex.IsMatch(str, pattern)

let regex (str:string) =
    match str with
    | Reg "^.{2}$" true -> "2文字"
    | Reg "^.{3}$" true -> "3文字"
    | _ ->"not match"

regex "" (* "not match" *)
regex "a" (* "not match" *)
regex "aa" (* "2文字" *)
regex "aaa" (* "3文字" *)
regex "aaaa" (* "not match" *)

パーシャルアクティブパターン

  • バナナクリップの中にパターン識別子と_(アンダースコア)を|で区切って書く。
  • Opotion<'T>の戻り値がないとエラー。
  • 戻り値がSomeの場合、直接値が取れる。
let (|Multiple|_|) (m:int) (n:int) =
    if n <> 0 && n % m = 0 then Some(n.ToString())
    else None

let fizzbuzz(n:int) =
    match n with
    | n when n < 1 -> "illegal input : " + n.ToString()
    | Multiple 15 x -> "FizzBuzz : " + x
    | Multiple 3 x -> "Fizz : " + x
    | Multiple 5 x -> "Buzz : " + x
    | _ -> n.ToString()

fizzbuzz 3 (* "Fizz : 3" *)
fizzbuzz 5 (* "Buzz : 5" *)
fizzbuzz 15 (* "FizzBuzz : 15" *)
fizzbuzz 1 (* "1" *)
fizzbuzz 0 (* "illegal input : 0" *)
fizzbuzz -1 (* "illegal input : -1" *)

複数アクティブパターン

  • バナナクリップの中に|で区切って複数のパターン識別子を書く。(7つまで)
  • すべてのパターン識別子が返されるように定義しないとエラー。
  • すべてのパターン識別子にmatchさせないと警告。
  • パターン識別子毎に別の戻り値の型を定義できる。
let (|Name|Age|) (input:string) =
    if Regex.IsMatch(input, "^[0-9]{1,3}$") then Age(int input)
    else Name(input)
        
let profileCheck(input:string) =
    match input with
    |Name x -> x
    |Age x -> if 10 < x && x < 20 then "target" else "not target"

profileCheck "Ann" (* "Ann" *)
profileCheck "15" (* "target" *)
profileCheck "10" (* "not target" *)
let (|Folder|File|) (input:string) =
    let attr = File.GetAttributes(input)
    if attr.HasFlag(FileAttributes.Directory) then Folder
    else File

let pathCheck (input:string) =
    match input with
    |File -> "File"
    |Folder -> "Folder"

pathCheck @"c:\" (* "Folder" *)
pathCheck @"c:\temp" (* "Folder" *)
pathCheck @"c:\temp\MyTest.txt" (* "File" *)

種類と使い分け

1要素のアクティブパターン

  • 入力データに名前を付ける。
  • すべての入力データに対して行う処理を隠蔽するのに使う。
  • 利用者は必ず成功する処理しか書けない。

パーシャルアクティブパターン

  • 入力データをある条件に当てはまるデータと、それ以外のデータに分け、当てはまるデータのみに名前を付ける。
  • 「それ以外」のデータに興味がない場合に使う。

複数アクティブパターン

  • 入力データを分別して、それぞれに名前を付ける。
  • データをすべて分別したい場合に使う。
  • 利用する際に、使われていないパターンがあると警告が出るので、実装漏れを検知しやすい。


書いたコードを残しておきます。

namespace StudyFSharp

open System
open System.Text.RegularExpressions

module Library =

    let (|Upper|) (str:string) =
        Upper(str.ToUpper())

    let f (str:string) =
        match str with
        | Upper "HOGE" -> "HOGE"
        | Upper "PIYO" -> "PIYO"
        | x -> "other"

    let (|Reg|) (pattern:string) (str:string) =
       Regex.IsMatch(str, pattern)

    let regex (str:string) =
        match str with
        | Reg "^.{2}$" true -> "2文字"
        | Reg "^.{3}$" true -> "3文字"
        | _ ->"not match"

    let (|Multiple|_|) (m:int) (n:int) =
        if n <> 0 && n % m = 0 then Some(string n)
        else None

    let fizzbuzz(n:int) =
        match n with
        | n when n < 1 -> sprintf "illegal input : %d" n
        | Multiple 15 x -> "FizzBuzz : " + x
        | Multiple 3 x -> "Fizz : " + x
        | Multiple 5 x -> "Buzz : " + x
        | _ -> string n

    let (|Name|Age|) (input:string) =
        if Regex.IsMatch(input, "^[0-9]{1,3}$") then Age(int input)
        else Name(input)
        
    let profileCheck(input:string) =
        match input with
        |Name x -> x
        |Age x -> if 10 < x && x < 20 then "target" else "not target"

    let (|Folder|File|) (input:string) =
        if File.GetAttributes(input).HasFlag(FileAttributes.Directory) then Folder
        else File

    let pathCheck (input:string) =
        match input with
        |File -> "File"
        |Folder -> "Folder"

テストコード。

namespace StudyFSharp

open System
open NUnit.Framework
open FsUnit

[<TestFixture>]
module Test =
    open Library

    [<TestCase("hoge", "HOGE")>]
    [<TestCase("HOGE", "HOGE")>]
    [<TestCase("HogE", "HOGE")>]
    [<TestCase("pIyO", "PIYO")>]
    [<TestCase("Foo", "other")>]
    [<TestCase("", "other")>]
    let ``function with ToUpper`` input expected =
        f input |> should equal expected

    [<TestCase("", "not match")>]
    [<TestCase("a", "not match")>]
    [<TestCase("aa", "2文字")>]
    [<TestCase("aaa", "3文字")>]
    [<TestCase("aaaa", "not match")>]
    let ``regex with Reg`` input expected =
        regex input |> should equal expected

    [<TestCase(3, "Fizz : 3")>]
    [<TestCase(6, "Fizz : 6")>]
    [<TestCase(5, "Buzz : 5")>]
    [<TestCase(10, "Buzz : 10")>]
    [<TestCase(15, "FizzBuzz : 15")>]
    [<TestCase(30, "FizzBuzz : 30")>]
    [<TestCase(1, "1")>]
    [<TestCase(7, "7")>]
    [<TestCase(0, "illegal input : 0")>]
    [<TestCase(-1, "illegal input : -1")>]
    let ``fizzbuzz with Multiple`` input expected =
        fizzbuzz input |> should equal expected

    [<TestCase("Ann", "Ann")>]
    [<TestCase("15", "target")>]
    [<TestCase("10", "not target")>]
    let ``profileCheck with Name and Age`` input expected =
        profileCheck input |> should equal expected

    [<TestCase(@"c:\", "Folder")>]
    [<TestCase(@"c:\temp", "Folder")>]
    [<TestCase(@"c:\temp\MyTest.txt", "File")>]
    let ``pathCheck with File and Folder`` input expected =
        pathCheck input |> should equal expected