19
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

アクティブパターンでアクティブになる

Last updated at Posted at 2014-12-15

この記事は F# Advent Calendar の15日目の記事です。14日目の記事は F# から Fortran の関数を呼び出そうでした。

F#の便利な機能、アクティブパターンについて紹介します。ネタは F# のネタリスト から拝借しました。ありがとうございます!

アクティブパターンの基本

アクティブパターン #とは

アクティブパターンについての説明を MSDN から引用します。

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

要するに判別共用体っぽくパターンマッチをカスタマイズできるもののようです。

コード例を見た方がわかりやすいと思うので、おなじみFizzBuzzをアクティブパターンで実装してみます。

module FizzBuzzPattern =
    let (|Fizz|Buzz|FizzBuzz|Number|) x =
        match x % 3, x % 5 with
        | 0, 0 -> FizzBuzz
        | 0, _ -> Fizz
        | _, 0 -> Buzz
        | _ -> Number x

(|Fizz|Buzz|FizzBuzz|Number|)という謎の表記の関数がアクティブパターンです。

上記アクティブパターンでは「Fizz」「Buzz」「FizzBuzz」「それ以外の数字(Number)」の4パターンを定義しています。
関数名で|記号でセパレートした名前が各パターン名となり、関数本体にはどの条件の場合にどのパターンとなるかを定義します。

アクティブパターンを使用する場合は、アクティブパターンを定義したモジュールをopenした状態で、アクティブパターンの引数の型の値をパターンマッチした時にパターン名を判別共用体っぽく書きます。

以下に使用例を示します。

module FizzBuzz =
    open FizzBuzzPattern

    let fizzBuzz x =
        match x with
        | Fizz -> printfn "Fizz"
        | Buzz -> printfn "Buzz"
        | FizzBuzz -> printfn "FizzBuzz"
        | Number n -> printfn "%d" n

    (* 実行結果
    > [1 .. 20] |> List.iter fizzBuzz;;
    1
    2
    Fizz
    4
    Buzz
    Fizz
    7
    8
    Fizz
    Buzz
    11
    Fizz
    13
    14
    FizzBuzz
    16
    17
    Fizz
    19
    Buzz
    val it : unit = ()
    *)

上記コードのfizzBuzz関数内でアクティブパターンを使用しています。Fizzなどのパターンをあたかも判別共用体を定義しているかのように使用しています。

「判別共用体を定義すればいい話では?」と思うかもしれませんが、わざわざFizzBuzzの実装のために判別共用体を定義しなくてもアクティブパターンを使えば可読性を保ったまま同等のコードが書けるというメリットがあります。

アクティブパターンの正体

アクティブパターンは変な記号(| |)に囲まれた関数のような文法で定義しましたが、実際のところ本当に紛れも無い関数だったりします。

module FizzBuzzChoice =
    open FizzBuzzPattern

    let fizzBuzzChoice x =
        let result = (|Fizz|Buzz|FizzBuzz|Number|) x
        match result with
        | Choice1Of4 () -> printfn "Fizz"
        | Choice2Of4 () -> printfn "Buzz"
        | Choice3Of4 () -> printfn "FizzBuzz"
        | Choice4Of4 n -> printfn "%d" n

このように変な記号(| |)も含めて普通の関数として記述すると本当に関数として呼べます。
関数の戻り値はパターン数に対応したChoice型となります。当然、Choice型に対するパターンマッチを行うことでパターンの判別や値の取り出しができます。

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

上記のFizzBuzzアクティブパターンはいずれかのパターンに必ずマッチするものでしたが、「あるパターンにマッチするか否か」を表すアクティブパターンを定義することもできます。これをパーシャルアクティブパターンと呼びます。

パーシャルアクティブパターンの例を示します。

let (|MultiplesOf2|_|) x = match x % 2 with 0 -> Some(x / 2) | _ -> None

let (|One|_|) = function 1 -> Some() | _ -> None
// 以下のような書き方も可能。どちらも戻り値の型はunit optionとなる
// let (|One|_|) = function 1 -> Some One | _ -> None

let multiplesOf2OrOne n =
    match n with
    | MultiplesOf2 x -> printfn "%d = MultiplesOf2 : %d" n x
    | One -> printfn "%d = ONE" n          // unit optionの場合は結果を受け取る変数を省略可能
//  | One x -> printfn "%d = ONE %A" n x   // あえてunitを受け取るような書き方も可能。この出力結果は「1 = ONE <null>」
    | _ -> printfn "%d = どちらでもない" n

(* 実行結果
> [1 .. 5] |> List.iter multiplesOf2OrOne;;
1 = ONE
2 = MultiplesOf2 : 1
3 = どちらでもない
4 = MultiplesOf2 : 2
5 = どちらでもない
val it : unit = ()
*)

(|MultiplesOf2|_|)のように、(|PatternName|_|)の形の関数がパーシャルアクティブパターンとなります。

パーシャルアクティブパターンの関数の戻り値の型は、何らかの型のoptionとなるように定義します。
実際にパーシャルアクティブパターンを使用した場合、結果がSome(x)の場合はパターン名にマッチし、xの値が返されます。逆に結果がNoneの場合はそのパターンにはマッチせず、後続のパターンマッチが試行されます。

アクティブパターンに引数を渡す

アクティブパターンでは、パターン部分で引数を渡すことが可能です。

// 最後以外の引数 x, y にはパターンマッチの中に書く引数が、 最後の引数の z にはmatch式の対象となる値が来る
let (|Between|_|) x y z = if x < z && z < y then Some() else None
let betweenTest n =
    match 2 with
    | Between 1 2 -> failwith "ここには来ない"
    | Between 2 3 -> failwith "ここには来ない"
    | Between 1 n -> printfn "2 is between 1 and %d" n
    | _ -> printfn "2 is not between 1 and %d" n

(* 実行結果
> [1 .. 4] |> List.iter betweenTest;;
2 is not between 1 and 1
2 is not between 1 and 2
2 is between 1 and 3
2 is between 1 and 4
val it : unit = ()
*)

(|Between|_|)パーシャルアクティブパターンは3引数を取る関数として定義されています。このように複数の引数を取る関数としてアクティブパターンを定義した場合、最後の引数以外の引数はパターンの部分で渡されることとなり、最後の引数にはパターンマッチ対象の値が渡されます。

betweenTest関数では(|Between|_|)パーシャルアクティブパターンを使用していますが、マッチ部分でBetween 1 nのように記述しています。
こう書いた場合、(|Between|_|) x y zアクティブパターンの引数xには1が渡され、引数yには変数nの値が渡されます。
最後の引数zにはパターンマッチ対象の2が渡されます。その結果Some()となった場合はこのパターンにマッチすることとなり、Noneとなった場合にはマッチしません。

このように、アクティブパターンではパターン部分に変数も含めた値を記述することができるため、(|Between|_|)アクティブパターン1つだけで多様なパターンに対応することが可能となります。

もちろん、引数を複数とるアクティブパターンであっても値を返すことが可能です。

type SchoolIdolAttribute = Smile | Pure | Cool
[<Measure>] type 年生
let (|SchoolIdol|_|) grade attr =
    match grade, attr with
    | 2<年生>, Smile -> Some "穂乃果"
    | 2<年生>, Pure -> Some "ことり"
    | 2<年生>, Cool -> Some "海未"
    | 1<年生>, Smile -> Some "凛"
    | 1<年生>, Pure -> Some "花陽"
    | 1<年生>, Cool -> Some "真姫"
    | 3<年生>, Smile -> Some "にこ"
    | 3<年生>, Pure -> Some "希"
    | 3<年生>, Cool -> Some "絵里"
    | _ -> None

let schoolIdolTest() =
    match Cool with
    // SchoolIdolパターンの最初の値はgradeに渡す値、最後の値はアクティブパターンの結果を受け取る変数
    | SchoolIdol 4<年生> _ -> failwith "ここには来ない"
    | SchoolIdol 2<年生> name -> printfn "%s" name
    | _ -> failwith "ここには来ない"

(* 実行結果
> schoolIdolTest();;
海未
val it : unit = ()
*)

上記の例ではアクティブパターンの引数に値を渡しつつ、結果を変数に受け取っています。

(|SchoolIdol|_|)アクティブパターンはgradeattrの2引数を受け取りますが、最初の引数gradeにはマッチ部分で指定した最初の値が来ます。例えばSchoolIdol 2<年生> nameというパターンであれば2<年生>gradeに渡されます。最後の引数attrにはパターンマッチ対象の値Coolが渡されます。

その結果はSome("海未")となるためSchoolIdol 2<年生> nameパターンにマッチし、結果のSomeの中の値"海未"SchoolIdol 2<年生> nameパターンの最後の変数nameに入ってきます。

一見わかりにくい書き方となりますが、パターンの最後の引数の部分にアクティブパターンの結果の値が入ってくるようなイメージとなります。

単一パターンのアクティブパターン

今までのアクティブパターンでは(|Fizz|Buzz|FizzBuzz|Number|)のように複数のパターンのいずれかか、もしくは(|MultiplesOf2|_|)のようにマッチするか否かの2択のどちらかでしたが、単一のパターンしかないパターンマッチも定義可能です。その場合、任意の型の結果を返すことができます。

let (|Hoge|) x = sprintf "hoge:%s" x
let printHoge = function Hoge x -> printfn "%s" x

(* 実行結果
> printHoge "foo";;
hoge:foo
val it : unit = ()
*)

これが何の役に立つのかというと、以下の様なことが可能となります。

例えば自然数を表す型を定義したいとします。自然数型としては以下の要件を満たしたいところです。

  1. 非常に大きな正の整数を表したい
  2. コンストラクタで自然数ではない値が渡された場合はエラーとしたい
  3. 代数的データ型のように扱いたい(値での同値比較や大小比較、パターンマッチ適用可能としたい)

1.の要件を実現するためには内部変数にbigint型を使えばいいですが、単なるbigintには負の値も含まれているためコンストラクタで負の値が渡された場合にエラーとする2.の要件が必要となります。
また、せっかくF#で実装するので、できれば3.も実現したいところです。

上記の要件を満たす自然数型を判別共用体やレコードで単純に実現しようとしても2.のコンストラクタでのチェックができないため実現不可能です。
逆にクラスで実現しようとすると3.の代数的データ型のような扱いができなくなります。

そこで登場するのが、単一パターンのアクティブパターンです。

/// 自然数
type NaturalNumber = private NaturalNumberImpl of bigint

[<CompilationRepresentation(CompilationRepresentationFlags.ModuleSuffix)>]
module NaturalNumber =
    /// 「0は自然数」派の人のためのコンストラクタ
    let create n =
        if n < 0I then invalidArg "n" "自然数は0以上です!!!"
        NaturalNumberImpl n

    /// NaturalNumberの値を取り出すためのアクティブパターン
    let (|NaturalNumber|) (NaturalNumberImpl n) = n

自然数型および自然数型のためのモジュールを上記のように定義します。

NaturalNumber判別共用体では、パターンのアクセス修飾子をprivateと定義します。こうすることでスコープ外からはNaturalNumberImplパターンへのアクセスができなくなります。すなわち、直接NaturalNumber判別共用体の値を作成することができなくなり、パターンマッチでNaturalNumberImplパターンにマッチすることもできなくなります。

このままではNaturalNumberの値を作成できないため、スコープ外からNaturalNumberの値を作成するためのコンストラクタとなるcreate関数を定義しています。create関数では引数が負の値の場合にエラーとするようになっています。

最後に(|NaturalNumber|)アクティブパターンを定義しています。このアクティブパターンではNaturalNumberImplパターンにマッチした値を取り出すことを行っています。このアクティブパターンのおかげで外部スコープでもNaturalNumber判別共用体からパターンマッチで実際のbigintの値を取り出すことが可能となります。

スコープ外からの自然数型の使用例を以下に示します。

module OtherModule =
    // アクティブパターンを定義したモジュールをopenするとアクティブパターンが使用可能
    open NaturalNumber

    let zero = NaturalNumber.create 0I
    // エラー 型 'NaturalNumber' の共用体ケースまたはフィールドは、このコードの場所からアクセスできません
    // let n = NaturalNumber 1I

    // match式以外でもパターンマッチ可能
    let isEven (NaturalNumber n) = n % 2I = 0I

この記述方法のメリットは、判別共用体やレコードにコンストラクタを設けつつパターンマッチ可能とする点だけではありません。
判別共用体などの実装とパターンマッチのパターンが切り離されるため、判別共用体などの内部表現を変更したとしてもその影響が他のスコープに漏れないようにすることができるのです。

例外もアクティブパターンで

アクティブパターンはパターンマッチの使える場所ではどこでも使用可能です。そう、例外のcatch部分でもね。

例えば、今やレガシーと言えるかもしれないHttpWebRequestWebClientを使用してWebアクセスをしようとしてエラーとなった場合、WebExceptionという例外が投げられます。この例外をcatchしてどのようなエラーだったかによってエラー処理を分岐することになるのですが、WebExceptionは複雑な構造をしており、単にNotFoundの場合の処理をしたいだけでも手間のかかるコードを書く羽目になります。(例: http://dobon.net/vb/dotnet/internet/httpstatuscode.html )

なるべく処理の本質でないエラー処理は簡潔に書きたいところです。そこでアクティブパターンが威力を発揮します。

open System.Net
    
let (|WebProtocolError|_|) (e : exn) =
    match e with
    | :? WebException as e when e.Status = WebExceptionStatus.ProtocolError ->
        use response = e.Response :?> HttpWebResponse
        Some response.StatusCode
    | _ -> None

let webProtocolErrorTest() =
    use client = new WebClient()
    // 例外のcatchもパターンマッチなのでアクティブパターンが適用可能
    try
        client.DownloadString("http://www.lovelive-anime.jp/notfound") |> ignore
    with
    | WebProtocolError HttpStatusCode.NotFound -> printfn "404 NotFound でした!"
    | WebProtocolError statusCode -> printfn "NotFoundではなくて %A でした" statusCode
    | :? WebException as e -> printfn "ProtocolError以外のWebExceptonでした : %O" e
    | e -> printfn "WebExcepton以外の例外でした : %O" e

(*
> webProtocolErrorTest();;
404 NotFound でした!
val it : unit = ()    
*)

(|WebProtocolError|_|)アクティブパターンでは、WebExceptionが投げられた場合、かつエラーの種類がProtocolErrorだった場合のみにマッチし、その場合はHTTPステータスコードを返します。
try ~ with式のwith以下にWebProtocolErrorパターンを指定することで、煩雑な処理を行うことなく自然な記述でNotFoundの場合のエラー処理を書くことができます。

雑なまとめ

アクティブパターンは超便利! いろいろな場面で活用できます!

今回の例で使用したコードは https://github.com/adacola/ActivePattern にあります。

19
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
19
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?