読者です 読者をやめる 読者になる 読者になる

【お知らせ】プログラミング記事の投稿はQiitaに移行しました。

アクティブパターンをどう捉えるか

F# Advent Calendar 2012 第16日目の参加記事です。

今年のF# Advent Calendarは実用がテーマです。私はアクティブパターンをなかなか実用できないでいましたが、ようやく端緒がつかめたという話を書きます。その前に実用関連として、勉強会でF#を使おうとしたときの話を書きます。

勉強会でF#を使おうとしたときの話

私は以前から、中高生の数学学習に数式処理プログラムの自作が応用できないかということに興味を持っていました。Expert F# 2.0の第12章に数式処理についての言及があったため、それをテーマとした勉強会を開催しました。

当初の想定では数式処理が目的で、F#の学習が目的ではなかったため、簡単にF#の読み方を説明するだけに留める予定でした。ですがF#の説明を始めると想像以上に時間が掛かり、本題の数式処理まであまり手が回りませんでした。

言語の説明を適当に飛ばしてすぐに本題に入った方が無難なため、C#に切り替えることにしました。C#自体を知らなくてもJavaなどの知識から類推が利くのは大きいです。

でもやっぱりF#に比べると冗長だと感じます。理想としては、F#の知識を前提とせずにすんなり手段として使い始められるようなうまい導入をしたいです。そうすればサンプル記述用の言語としてどんどん実用できそうだという予感はあります。この方面も機会があればまた取り組んでみたいと思います。

アクティブパターン

本題のアクティブパターンに入ります。

初めてアクティブパターンを見たとき、構文の意味自体は分かりましたが、それをどういうときに使うと嬉しいのかピンと来ませんでした。そのためF#でプログラムを書くときにもアクティブパターンはまったく使いませんでした。

こういうとき、自分の書いたコードの中でアクティブパターンが適用できる箇所を指摘できれば、ぐっと実用に近付きそうです。つまり新しい知識として追加するのではなく、既存の知識の延長線上として位置付けるという発想です。判別共用体を抽象クラスの継承として捉えることでようやく腑に落ちたという前例があります。

同様に、既存のコードをアクティブパターンで書き直してみます。

文字の判定 (C#)

例として字句解析を取り上げます。

字句解析する際に、文字の種類を判定する必要があります。C#で例を挙げます。

private void Parse(TextReader tr)
{
    int ch = tr.Read();
    if (ch < 0)
        Console.WriteLine("終端");
    else if (ch == '\r' || ch == '\n')
        Console.WriteLine("改行");
    else if (ch == ' ' || ch == '\t')
        Console.WriteLine("空白");
    else if ('0' <= ch && ch <= '9')
        Console.WriteLine("数字");
    else if (ch == '_'
        || ('A' <= ch && ch <= 'Z')
        || ('a' <= ch && ch <= 'z'))
        Console.WriteLine("文字");
    else
        Console.WriteLine("不明");
}

※全角文字を対象外とするためchar.IsDigitなどは使っていません。

同じような条件分岐を複数個所で行う場合、毎回条件を書くのは無駄です。条件を修正するときに複数個所で同じ修正をする羽目になります。

このような場合は種類を列挙型で定義して、判定するメソッドを1つだけに絞ることで対応できます。

enum CharType { End, NewLine, Space, Digit, Letter, Unknown }

private CharType GetCharType(int ch)
{
    if (ch < 0)
        return CharType.End;
    else if (ch == '\r' || ch == '\n')
        return CharType.NewLine;
    else if (ch == ' ' || ch == '\t')
        return CharType.Space;
    else if ('0' <= ch && ch <= '9')
        return CharType.Digit;
    else if (ch == '_'
        || ('A' <= ch && ch <= 'Z')
        || ('a' <= ch && ch <= 'z'))
        return CharType.Letter;
    else
        return CharType.Unknown;
}

private void Parse(TextReader tr)
{
    switch (GetCharType(tr.Read()))
    {
        case CharType.End:
            Console.WriteLine("終端");
            break;
        case CharType.NewLine:
            Console.WriteLine("改行");
            break;
        case CharType.Space:
            Console.WriteLine("空白");
            break;
        case CharType.Digit:
            Console.WriteLine("数字");
            break;
        case CharType.Letter:
            Console.WriteLine("文字");
            break;
        case CharType.Unknown:
            Console.WriteLine("不明");
            break;
    }
}

文字の判定 (F#)

C#をそのままF#で書き直してみます。

type CharType =
    | End     = 0
    | NewLine = 1
    | Space   = 2
    | Digit   = 3
    | Letter  = 4
    | Unknown = 5

let getCharType ch =
    if ch < 0 then CharType.End else
        let ch = char(ch)
        if ch = '\r' || ch = '\n' then
            CharType.NewLine
        elif ch = ' ' || ch = '\t' then
            CharType.Space
        elif ch = '_'
            || ('A' <= ch && ch <= 'Z')
            || ('a' <= ch && ch <= 'z') then
            CharType.Letter
        else
            CharType.Unknown

let parse (tr:TextReader) =
    match getCharType(tr.Read()) with
    | CharType.End     -> printfn "終端"
    | CharType.NewLine -> printfn "改行"
    | CharType.Space   -> printfn "空白"
    | CharType.Digit   -> printfn "数字"
    | CharType.Letter  -> printfn "文字"
    | _                -> printfn "不明"

ほとんど直訳ですが、普通こんなコードは書かないでしょう。列挙型を判別共用体にして、判定をパターンマッチで書き換えてみます。

type CharType =
    | End
    | NewLine
    | Space
    | Digit
    | Letter
    | Unknown

let getCharType ch =
    if ch < 0 then End else
    match char(ch) with
    | '\r' | '\n' -> NewLine
    | ' '  | '\t' -> Space
    | ch when '0' <= ch && ch <= '9'
        -> Digit
    | ch when ch = '_'
           || ('A' <= ch && ch <= 'Z')
           || ('a' <= ch && ch <= 'z')
        -> Letter
    | _ -> Unknown

let parse (tr:TextReader) =
    printfn <|
        match getCharType(tr.Read()) with
        | End     -> "終端"
        | NewLine -> "改行"
        | Space   -> "空白"
        | Digit   -> "数字"
        | Letter  -> "文字"
        | Unknown -> "不明"

構造はほとんど同じですが、書式が簡潔になりました。簡潔になるというのは「直訳でも書けるけど、なるべくこういう書き方をして欲しい」という示唆かと思います。

判別共用体と判定関数を別々にしていますが、アクティブパターンを使えば1つにまとめることができます。

let (|End|NewLine|Space|Digit|Letter|Unknown|) ch =
    if ch < 0 then End else
    match char(ch) with
    | '\r' | '\n' -> NewLine
    | ' '  | '\t' -> Space
    | ch when '0' <= ch && ch <= '9'
        -> Digit
    | ch when ch = '_'
           || ('A' <= ch && ch <= 'Z')
           || ('a' <= ch && ch <= 'z')
        -> Letter
    | _ -> Unknown

let parse (tr:TextReader) =
    printfn <|
        match tr.Read() with
        | End     -> "終端"
        | NewLine -> "改行"
        | Space   -> "空白"
        | Digit   -> "数字"
        | Letter  -> "文字"
        | Unknown -> "不明"

かなり簡潔になりました。こういう風に書けるようにすることがアクティブパターンの着想なのかなと思います。

ちなみに(|...|)の部分を単なる名前とみなして関数として呼ぶこともできます。

let parse (tr:TextReader) =
    printfn <|
        match (|End|NewLine|Space|Digit|Letter|Unknown|) (tr.Read()) with
        | End     -> "終端"
        | NewLine -> "改行"
        | Space   -> "空白"
        | Digit   -> "数字"
        | Letter  -> "文字"
        | Unknown -> "不明"

わざわざ明示的に呼ばなくても自動的に呼ばれる点がアクティブという名前の由来なのかなと思います。

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

マッチしない _ を含めたものをパーシャルアクティブパターンと呼びます。これを利用すれば個々のパターンを別々に定義することができます。

let (|End|_|) ch =
    if ch < 0 then Some End else None
let (|NewLine|_|) =
    function '\r' | '\n' -> Some NewLine | _ -> None
let (|Space|_|) =
    function ' ' | '\t' -> Some Space | _ -> None
let (|Digit|_|) ch =
    if '0' <= ch && ch <= '9' then Some Digit else None
let (|Letter|_|) ch =
    if ch = '_' || ('A' <= ch && ch <= 'Z') || ('a' <= ch && ch <= 'z')
    then Some Letter else None

let parse (tr:TextReader) =
    printfn <|
        match tr.Read() with
        | End -> "終端"
        | ch -> match char(ch) with
                | NewLine -> "改行"
                | Space   -> "空白"
                | Digit   -> "数字"
                | Letter  -> "文字"
                | _       -> "不明"

前の例とどちらが読みやすいかは好みが分かれそうです。Unknownのようなものを定義しなくて済むのと、個々の定義が局所化されるため個別に利用できるのはメリットだと思います。終端だけ型が異なるためぎこちない書き方になっていますが、これは題材の問題です。

最後に

「新しい枠組みとして捉えて、既存のものにとらわれずに考え方を変える」のが理想ですが、現実的にはなかなか難しいです。既存の延長線上で変形して徐々に慣れれば、いずれ変形を意識しなくても実用できるようになると期待しています。

そういう意味で「既存の考え方でも書ける」というF#の特徴は、私にとってはとてもありがたいです。うまくハマったときにとても簡潔になるのは、パズルのようです。最初は泥臭く書いておいて、後で最適化するようなイメージでしょうか。

今回の記事を書くにあたってMicrosoftのF#言語リファレンスを参照しました。いつの間にか日本語訳されていて、説明も結構分かりやすいです。

これは是非目を通さねば!というわけで勉強会を企画しました。

一通りリファレンスに目を通したらfscのソースコードも読みたいですね。