この記事は 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|_|)
アクティブパターンはgrade
とattr
の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.の要件を実現するためには内部変数に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部分でもね。
例えば、今やレガシーと言えるかもしれないHttpWebRequest
やWebClient
を使用して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 にあります。