式(Expression)と文(Statement)

式と文の違いは知っておくといろいろ役立ちますので、ご紹介したいと思います。

プログラミング言語の構文の要素は、式(Expression)と文(Statement)に分類されます。プログラマであれば、用語としては把握していなくても、プログラミングの実践の中で、式と文を明確に区別して認識されていると思います。以降では、式と文とは何か、その違いがプログラミング言語の構文に与える影響、プログラムの書き方に与える影響を説明します。

VBで例を挙げると、文には代入文、If文、For文、While文、呼び出し文などがあり、式には条件式(比較式)、呼び出し式、加減算式、定数式などがあります。

1: Dim a As Integer
2: a = 10            ' ←代入文
3: If a = 10 Then    ' ← ここのa = 10は条件式
4:     Console.WriteLine("aは10です。")
5: End If

※左端の1:〜5:は行番号を示すために便宜上付加しているもので、コードの一部ではありません。

上記のコード例では、1行目のDim 〜の部分は宣言文、2行目は代入文、3〜5行目はIf文で、3行目の「a = 10」の部分は条件式で、4行目のConsole.〜の部分は、呼び出し文(戻り値を持たないメソッド呼び出しのため)です。さらに細かく言えば、2行目や3行目のaは、単純名前式で、10は定数式です。

この例から、式、文の違いがぼんやりと見えてきたでしょうか。文はそれ単独で完結する言語要素です。式はそれ単独では基本的に完結せず、文または式の一部として使用される言語要素です。また、式の最大の特徴として、値を返すという点が挙げられます(文は値を返しません)。例えば、定数式はその値そのものを返します。条件式は真偽値(True/False)を返す式です。条件式の一つとして比較式がありますが、条件式そのものはTrue/Falseを返せばなんでもよいため、単なるBooleanのTrue(定数式)もまた条件式になります。比較式は比較演算子を挟んで左辺と右辺の式を比較し、その結果をTrue/Falseで返すため、条件式になることができます。

VBC#を式と文の観点で比較するときに代入は興味深い違いがあります。VBでは代入は文ですが、C#では代入は式です。C#の代入が式なのは、C言語の系譜を継ぐ言語だからでしょう。以下のコード例で、代入文と代入式の違いを示します。

' VBのコード
Dim a As Boolean
Dim b As Integer
a = b = 0
// C#のコード
int a;
int b;
a = b = 0;

上がVB、下がC#のコード例です。3行目は「a = b = 0」という形になっているのは共通しています。しかし、この行の意味はVBC#でまったく異なります。先にC#の意味を説明すると、b = 0は代入式のため、変数bに定数0を代入します。それと同時にこの代入式は代入した値自身を返します。つまり、0を返します。その結果、

b = 0;
a = 0;

と書いた場合と同じ意味になります。それに対してVBの場合、「a = ?」という形式は、代入文の形ですが、?に当たる「b = 0」の部分には、文は来ることができないルールが言語仕様で定められているため、「b = 0」は式であるとみなされ、その結果、比較式であると解釈されます。bはIntegerで宣言だけをしてあるので、暗黙的に値は0です。そのため、「b = 0」の比較式はTrueを返します。よって、a = Trueと記述した場合と同じ結果になります。

では、C#VBの場合と同じ意味のコードを書くとどうなるかというと以下のようになります。

bool a;
int b;
a = b == 0;

VBでは代入演算子も比較演算子の等号もどちらも「=」ですが、C#では代入演算子は「=」、比較演算子の等号は「==」として区別されているため、このようなコードになります。
逆に言えば、C#で「a = b = 0」と記述して、aにもbにも0を代入と記述できるのは、代入演算子と比較演算子の等号とをそれ単独で区別可能な記号体系にしているからと言えます。VBでこのC#の機能を実現しようと思ったら、代入と等号を別の記号にするか、あるいは、連続的に代入するための記号を追加するといった何らかの大きな言語仕様の変更が必要です。

ちなみに、VB9.0ではIf式が追加されましたが、当記事の解説を読むとIf文とIf式の区別の意味がわかります。式と文の違いを理解していると様々な言語とその言語仕様を理解する上で役立ちます。プログラミングをする際にその違いを意識してみてはどうでしょうか。

拡張メソッドのターゲットにIList(T)とIEnumerable(T)のどちらを選ぶか?

.NET Frameworkのクラスライブラリでは、IEnumerable(T)に対して多数の拡張メソッドが用意されています。これは非常に便利です。ここで自作の拡張メソッドを作成する場合に悩むことがあります。集合に対する拡張をしたい場合、IEnumerable(T)向けに拡張メソッドを書くと柔軟性が高まることは確かですが、本質的にIEnumerable(T)のメソッドであるべきだろうか?と悩むことがあります。具体例を挙げたいと思います。私は順列を生成する以下のようなメソッドを定義しました。

/// <summary>
/// 順列を生成する
/// </summary>
/// <param name="elements">順列の要素</param>
/// <param name="selectionCount">要素から何個選択するか</param>
/// <param name="permitRepeatedUse">要素から取る際に同じ要素を重複して取ることを許可するか</param>
/// <returns>順列のIEnumerable(T)</returns>
public static IEnumerable<T[]> CreatePermutation<T>(this IList<T> elements, int selectionCount, bool permitRepeatedUse)

第一引数は順列を生成するための要素のデータで、例えば、new string[]{"a","b","c"}などです。
順列を生成する対象データは通常、要素数が最終的には確定している配列かリストです。(IList(T)を実装していないコレクションもありますが、ここではとりあえず気にしません) 実用上はIList(T)を対象にすれば問題ないだろうと考えました。もしIEnumerable(T)を対象にしたければ、IEnumrable(T)の拡張メソッドToArray、ToListを使えばよいだけです。

逆にIEnumerable(T)を対象とする拡張メソッドとしなかった理由は以下です。

  • 順列を生成する対象の集合は、生成する時点では要素数が固定であるはず
  • 順列を生成するには要素数とインデックスによるアクセスが必要
  • IEnumerable(T)では要素数を取るのはCount、インデックスによるアクセスはElementAtといった拡張メソッドに頼らざるを得ないが、これらのメソッドはたぶん遅いはず。
    • なぜなら、IEnumerable(T)は順番に要素を挙げていくことしかできないから。
    • といいつつ、ひょっとしたらCountやElementAtメソッドは内部でインスタンスの型を見てIList(T)だったらこうみたいな場合わけをしているかもしれないけれど。


つまり、IEnumerable(T)を対象とすることで得られる柔軟性よりも、IList(T)を対象とすることであるべき姿に近いのではないか、処理効率がよいのではないか、という考えが勝りました。でも、他の拡張メソッドと使い方が異なるという面で一貫性がないなぁなどと思ったりもするのですよね。(異なったものはあえて異なって見せるべきという意見もあるけれど、今回の場合、"異なった"と言えるかどうかが自信なし) みなさんはどう思いますか?

以下は順列生成メソッドのソースです。

using System;
using System.Collections.Generic;

namespace YKLib.Extentions
{
    public static class IListExtentions
    {
        /// <summary>
        /// 順列を生成する
        /// </summary>
        /// <param name="elements">順列の要素</param>
        /// <param name="selectionCount">要素から何個選択するか</param>
        /// <param name="permitRepeatedUse">要素から取る際に同じ要素を重複して取ることを許可するか</param>
        /// <returns>順列のIEnumerable(T)</returns>
        public static IEnumerable<T[]> CreatePermutation<T>(this IList<T> elements, int selectionCount, bool permitRepeatedUse)
        {
            if (elements == null) throw new ArgumentNullException("elements");
            if (selectionCount < 0) throw new ArgumentOutOfRangeException("selectionCount");
            if (!permitRepeatedUse && elements.Count < selectionCount) throw new ArgumentException("重複許可のない順列では要素数以上のselectionCountは指定できません。");

            return permitRepeatedUse
                ? CreateRepeatedPermutation<T>(elements, selectionCount)
                : CreatePermutation<T>(elements, selectionCount);
        }

        /// <summary>
        /// 重複を許可しない順列を生成する
        /// </summary>
        /// <param name="elements">順列の要素</param>
        /// <param name="selectionCount">要素から何個選択するか</param>
        /// <returns>順列のIEnumerable(T)</returns>
        public static IEnumerable<T[]> CreatePermutation<T>(this IList<T> elements, int selectionCount)
        {
            if (elements == null) throw new ArgumentNullException("elements");
            if (selectionCount < 0) throw new ArgumentOutOfRangeException("selectionCount");
            if (elements.Count < selectionCount) throw new ArgumentException("要素数以上のselectionCountは指定できません。");

            int elementCount = elements.Count;
            int totalCountOfPermutation = (int)Math.Pow(elementCount, selectionCount);
            // 要素数elementCountの0〜selectionCount-1までのべき乗をあらかじめ求めておく
            // 後の計算の高速化のため
            int[] powers = new int[selectionCount];
            for (int i = 0; i < selectionCount; i++)
            {
                powers[i] = (int)Math.Pow(elementCount, i);
            }

            HashSet<int> hashSet = new HashSet<int>();
            T[] permutation = new T[selectionCount];
            for (int i = 0; i < totalCountOfPermutation; i++)
            {
                hashSet.Clear();
                for (int j = 0; j < selectionCount; j++)
                {
                    int index = (i / powers[j]) % elementCount;
                    if (!hashSet.Add(index))
                    {
                        break;
                    }                    
                    permutation[selectionCount - 1 - j] = elements[index];
                }
                if (hashSet.Count == selectionCount)
                {
                    yield return permutation;
                }
            }
        }

        /// <summary>
        /// 重複を許可する順列を生成する
        /// </summary>
        /// <param name="elements">順列の要素</param>
        /// <param name="selectionCount">要素から何個選択するか</param>
        /// <returns>順列のIEnumerable(T)</returns>
        public static IEnumerable<T[]> CreateRepeatedPermutation<T>(this IList<T> elements, int selectionCount)
        {
            if (elements == null) throw new ArgumentNullException("elements");
            if (selectionCount < 0) throw new ArgumentOutOfRangeException("selectionCount");

            int elementCount = elements.Count;
            int totalCountOfPermutation = (int)Math.Pow(elementCount, selectionCount);
            // 要素数elementCountの0〜selectionCount-1までのべき乗をあらかじめ求めておく
            // 後の計算の高速化のため
            int[] powers = new int[selectionCount];
            for (int i = 0; i < selectionCount; i++)
            {
                powers[i] = (int)Math.Pow(elementCount, i);
            }

            T[] permutation = new T[selectionCount];
            for (int i = 0; i < totalCountOfPermutation; i++)
            {
                for (int j = 0; j < selectionCount; j++)
                {
                    int index = (i / powers[j]) % elementCount;                    
                    permutation[selectionCount - 1 - j] = elements[index];
                }
                yield return permutation;
            }
        }

    }
}

拡張メソッドは対象とする型の提供者以外は作成するべきではありません

非常に魅惑的な拡張メソッドですが、そのリスクについてはあまり語られていないように思えたので(百も承知だから誰も書いてないだけかもしれません) 、書いてみます。拡張メソッドとは、C#3.0、VB9.0で取り入れられた言語の機能で、詳細は下記のリンクを参照してください。

拡張メソッド (C# プログラミング ガイド)
拡張メソッド (Visual Basic)

端的に言うと、拡張メソッドだよと印を付けた静的メソッド/共有メソッドは、その静的メソッド/共有メソッドの第一引数に指定した型のインスタンスメソッドのように振舞う、という機能です。あまり意味のある使い方ではないですが、C#VBでの使用例を以下に示します。

using System;

class Program
{
    static void Main(string[] args)
    {
        "あいう".Print();
    }
}

static class StringExtenstions
{
    public static void Print(this string target)
    {
        Console.WriteLine(target);
    }
}
Imports System.Runtime.CompilerServices

Module Program
    Sub Main()
        Dim s As String = "あいう"
        s.Print()
    End Sub
End Module

<Extension()>  _
Module StringExtenstions
    <Extension()> _
        Sub Print(ByVal target As String)
            Console.WriteLine(target)
    End Sub
End Module

拡張メソッドは非常にわくわくする機能ですが、同時に危険な香りがぷんぷんします。上記のURLにはそのリスクについても記載がありますが、私なりにまとめると以下のようになります。

○あなたが拡張メソッドを作成した場合のリスク


サードパーティのクラスライブラリ提供者が拡張メソッドを提供した場合のリスク

  • A社の拡張メソッドとB社の拡張メソッドのシグネチャが同じだったら、どちらの拡張メソッドも使用できません。
    • 名前空間が別であれば、片方だけをインポートすることは可能。
  • A社がインタフェースA向けの拡張メソッドを用意し、B社がインタフェースB向けの拡張メソッドを用意した場合、対象となる型(インタフェースAとインタフェースB)以外がまったく同じシグネチャだった場合、インタフェースAとインタフェースBを実装したクラスCに対してはこれらの拡張メソッドはどちらも使用できません。
    • →IEnumerable(T)向けのメソッドXとIList(T)向けのメソッドXが定義されていたらどうでしょうか?

○拡張メソッド全体のリスク

  • 上位のクラス(例えば、極端に言えばObjectクラス)に拡張メソッドを提供した場合、ありとあらゆる派生クラスにその拡張メソッドが存在することになります。派生クラスで同じシグネチャのメソッドを定義した場合、その定義したインスタンスメソッドが有効になるため動作上の問題はありませんが、間違いなく混乱のもとになります。(後、瑣末な話ですが、VisualStudioを使っている場合、常にインテリセンスにその拡張メソッドが表示されて、結構うっとおしいです。)


○拡張メソッドを提供してもよいパターン

  • 自分が作成したインタフェース、列挙型などインスタンスメソッドを提供できない型に対する拡張メソッド
    • →(例) Microsoft社のIEnumerable(T)に対するMicrosoft社による拡張メソッドの提供
  • 自分の汎用的でないプロジェクトに限定して使う場合で、使用しているクラスライブラリ等の今後のバージョンアップの場合に発生するリスクを見越してOKがだせる場合でしょう。


○既存の型のために拡張メソッドを用意したいと思ったら・・・

  • ぜひその型を提供しているベンダーに××なメソッドが欲しい!と要望を出してみるのがよいと思います。可能であれば拡張メソッドを作成するのではなく、その型のインスタンスメソッドを作るべきであり、拡張メソッドを作成するにしても、その型の提供者でなければ、今後のバージョンアップを見据えて安全に拡張メソッドを提供することはできないからです。


いろいろ悪い使い方を考えて見ると楽しいと思いますが、あくまで実験に留めておいてくださいね。お仕事ではあまり使えないなぁというのが個人的な印象です。趣味のプログラミングではがんがん使ってます(笑) すごく便利です。

数式を解くプログラム その6(最後) - 構文解析3

id:yone-ken:20090223:p1 の続きです。さっそく、Expressionクラスから見ていきます。

class Expression : Node
{
    public override void Evaluate(Context<Token> context)
    {
        Node left = new Term();
        left.Evaluate(context);
        this.Value = left.Value;

        while (true)
        {
            Func<decimal, decimal, decimal> func = null;
            switch (context.Current.Kind)
            {
                case TokenKind.OpAdd: func = (d1, d2) => d1 + d2; break;
                case TokenKind.OpSub: func = (d1, d2) => d1 - d2; break; 
                case TokenKind.Invalid:
                    throw new EvaluateException("無効な文字が含まれています。", context.Current);
                case TokenKind.Eof:
                    return;     // 正常終了
                default:
                    return;     // Expressionを終了して、処理は継続
            }

            context.MoveNext();
            Node right = new Term();
            right.Evaluate(context);
            this.Value = func(this.Value, right.Value);

        }
    }
}

ExpressionクラスはNodeクラスのEvaluateメソッドをオーバーライドしています。で始まるため、最初にTermクラスのインスタンスを生成し、TermのEvalutateメソッドを実行します。実行後、TermクラスのValueプロパティに評価結果が入っていますので、これをExpressionのValueプロパティに反映します。続いて、加算/減算記号の有無をチェックします。トークンが加算/減算記号の場合、decimalの引数2つ、戻り値decimalのデリゲートに加算用または減算用の匿名メソッド(例えば「(d1, d2) => d1 + d2」と記述している箇所。)をセットします。ここではわざわざ匿名メソッドを使っていますが、decimal.Addとdecimal.SubtractでもOKです。

加算/減算記号以外で無効なトークンの場合は評価の例外としてEvaluateExceptionを発生させます。それ以外のトークンだった場合はの評価は終了のため、メソッドを抜けます。加算/減算記号だった場合は引き続き、右辺側のを評価するため、加算/減算記号のトークンを読み飛ばして、TermクラスのEvaluateメソッドを実行します。処理結果は、デリゲートの処理を使ってExpressionのValueプロパティに反映しておきます。以上、加算/減算記号が登場する限り、右辺の計算を続けます。次にTermクラスを見ていきます。

class Term : Node
{
    public override void Evaluate(Context<Token> context)
    {
        Node left = new Factor();
        left.Evaluate(context);
        this.Value = left.Value;

        while (true)
        {
            Func<decimal, decimal, decimal> func = null;
            switch (context.Current.Kind)
            {
                case TokenKind.OpMul: func = (d1, d2) => d1 * d2; break;
                case TokenKind.OpDiv: func = (d1, d2) => d1 / d2; break; 
                default:
                    return;     // Termを終了して、処理は継続
            }
            // 計算の例外発生時のためにトークンを保持しておく。
            Token snapshot = context.Current;

            context.MoveNext();
            Node right = new Factor();
            right.Evaluate(context);
            try
            {
                this.Value = func(this.Value, right.Value);
            }
            catch (DivideByZeroException)
            {
                throw new EvaluateException("0で除算できません。", snapshot);
            }

        }

    }
}

基本的にはの関係が、の関係にスライドしただけです。Expressionクラスとで異なるところは加算/減算に対して乗算/除算を扱っているため、除算のときの0割り例外の発生を考慮している点です。続いてFactorクラスを見ていきます。

class Factor : Node
{
    public override void Evaluate(Context<Token> context)
    {
        Node exp = null;
        switch (context.Current.Kind)
        {
            case TokenKind.LParen:
                context.MoveNext();
                exp = new Expression();
                exp.Evaluate(context);
                if (context.Current.Kind != TokenKind.RParen)
                {
                    throw new EvaluateException("')'が足りません。", context.Current);
                }
                this.Value = exp.Value;
                break;
            case TokenKind.OpAdd:
            case TokenKind.OpSub:
                decimal sign = (context.Current.Kind == TokenKind.OpSub) ? -1 : 1;
                context.MoveNext();
                exp = new Expression();
                exp.Evaluate(context);
                this.Value = sign * exp.Value;
                break;
            case TokenKind.Number:
                this.Value = (decimal)context.Current.Value;
                break;
            case TokenKind.Invalid:
                throw new EvaluateException("無効な文字が含まれています。", context.Current);
            case TokenKind.Eof:
                throw new EvaluateException("式が途中で終わっています。", context.Current);
            default:
                throw new EvaluateException("不正な式です。", context.Current);
        }
        context.MoveNext();
    }
}

Factorクラスでは先頭のトークンにより処理が分岐します。左括弧'('で始まる場合は、'(' ')'、加算/減算記号で始まる場合は'+' 、'-' '、数値トークンの場合はです。どのパターンであるかが1つ目のトークンで判断できるため、単純な分岐で済みます。の処理を行う段階では、Eofトークンが来ることは正常な式ではありえません。与えられた式がまったくの空である場合は、Eofトークンしかないため、ExpressionクラスのEvaluateメソッドでEofの場合をはじいており、FactorクラスのEvaluateメソッドは呼ばれません。それ以外では、必ずTermクラスのEvaluateメソッドから呼ばれます。数式が単体の数値だけの場合は別として、は乗算/除算/加算/減算の演算子の後に処理される要素のため、の評価でEofトークンが来るということは、式が演算子で終わっているということです。(たとえば、「1+1*」のようなパターン)。

また、Evaluateメソッドの最後に「context.MoveNext()」で処理済みのトークンを読み飛ばしていますが、これは重要です。のすべてで共通ですが、Evaluateメソッドの処理開始時点では、その構文定義で処理を行うべきトークンがcontext.Currentにセットされていることを前提としており、そのため、Evaluateメソッドの終了時点では、その構文定義で処理すべきトークンはすべて読み飛ばされている状態でなければなりません。

最後にExpressionEvaluatorクラスを掲載します。今までに紹介した字句解析、構文解析を行うためのクラスを使って数式を評価するための外部向けインタフェースとなるクラスです。特に難しいことはしていませんので説明は端折ります。ざっとクラスを眺めてください。このクラスの使用例は最後に掲載します。

public class ExpressionEvaluator
{
    public String Expression { get; private set; }

    private Expression exp;
    public decimal Value
    {
        get 
        {
            if (this.LastError)
            {
                throw new InvalidOperationException("最後の評価にエラーがあります。");
            }
            return this.exp.Value; 
        }
    }

    public bool LastError { get; private set; }

    private string lastErrorMessage;
    public string LastErrorMessage
    {
        get 
        {
            if (!this.LastError)
            {
                throw new InvalidOperationException("最後の評価にエラーはありません。");
            }
            return this.lastErrorMessage;
        }
        private set { this.lastErrorMessage = value; }
    }

    private int lastErrorColumn;
    public int LastErrorColumn
    {
        get 
        {
            if (!this.LastError)
            {
                throw new InvalidOperationException("最後の評価にエラーはありません。");
            }
            return this.lastErrorColumn; 
        }
        private set { this.lastErrorColumn = value; }
    }

    public ExpressionEvaluator()
    {
        this.exp = new Expression();
    }

    public bool Evaluate(string expression)
    {
        this.Expression = expression;
        try
        {
            Context<Token> context = new Tokenizer(this.Expression);
            if (!context.MoveNext()) return false;
            exp.Evaluate(context);
            this.LastError = false;
            this.LastErrorColumn = 0;
            this.LastErrorMessage = string.Empty;
            return true;
        }
        catch (EvaluateException e)
        {
            this.LastError = true;
            this.LastErrorColumn = e.Token.Column;
            this.LastErrorMessage = e.Message;
            return false;
        }
    }
}

ExpressionEvaluatorクラスのEvaluateメソッドに数式を渡すと評価を行います。数式の評価の成否は戻り値でtrue/falseで示しますので、成功していたら、Valueプロパティで結果を参照できます。評価できなかった場合は、LastErrorMessageで失敗内容をLastErrorColumnで失敗位置を取得できます。以上で、数式を解くプログラムは完成です。呼び出し部分は例えば、以下のように書けます。

class Program
{
    static void Main(string[] args)
    {
        ExpressionEvaluator eval = new ExpressionEvaluator();
        string expression = "(1+4)*3/5-2";
        Console.Write(expression.PadRight(10));
        if (eval.Evaluate(expression)
        {
            Console.WriteLine("= {0}", eval.Value);
        }
        else
        {
            Console.WriteLine();
            Console.WriteLine(new String(' ', eval.LastErrorColumn) + "^ " + eval.LastErrorMessage);
        }
        Console.Read();  // Enter押下でコマンドプロンプトを閉じてください。
    }
}

数式を解くプログラムの解説はここまでです。ぼんやりとですが、このプログラムにはいろいろと課題があるなと思っています。このプログラムを元に拡張していくとその問題点も見えてくると思いますが、私自身はまだその問題点も一部しか見えていませんし、問題点に対する解決策も持っていません。徐々に研究を深めていきたいと思います。

数式を解くプログラム その5 - 構文解析2

id:yone-ken:20090220:p1 の続きです。再度、拡張BNFによる構文定義を確認します。

   ::=  ('+' )* |  ('-' )*
         ::=  ('*' )* |  ('/' )*
       ::= '('  ')' | '+'  | '-'  | 

構文解析を実装するに当たり、どんなクラスが必要でしょうか?また、そのクラスにはどんなメンバが必要でしょうか?私の考えた方針は次の通りです。

  • 構文定義の要素である各非終端記号が自分の担当箇所の解析を行えばよい。
  • 今回は構文解析で、解析+計算まで行うインタープリターを実装する。


この方針により、Expressionクラス、Termクラス、Factorクラスを作成することにします。また、これらが解析+計算を行うメソッドをEvaluateメソッドとし、戻り値はなしで、引数には字句解析処理を渡すことにします。計算結果はValueプロパティを用意してそこから取得することにします。(今考えるとValueプロパティは不要で、単純にEvaluateメソッドの戻り値で返すので十分な気もします。)これら3つのクラスは共通のインタフェースを持つのでこれらの抽象クラスとしてNodeクラスを用意します。

また、与えられる数式は正しい式ばかりとは限りませんし、問題なく計算できるとは限りませんので、Evaluateメソッドで解析エラー、もしくは、計算エラーがあった場合には、EvaluateExceptionを発生させることにします。

NodeクラスとEvaluateExceptionクラスを以下に掲載します。

abstract class Node
{
    public decimal Value { get; protected set; }
    public abstract void Evaluate(Context<Token> context);
}
class EvaluateException : Exception
{
    private Token token;
    public Token Token
    {
        get { return this.token; }
    }
    public EvaluateException(string messgae, Token token)
        : base(messgae)
    {
        this.token = token;
    }
}

次回はExpressionクラス、Termクラス、Factorクラスと外向きのインターフェースとしてのExpressionEvaluatorクラスを紹介します。

数式を解くプログラム その4 - 構文解析1

id:yone-ken:20090219:p1 の続きです。例によって一般的な説明はwikipedia:構文解析を参照して頂くとして、「数式を解くプログラム」の場合の構文解析を具体的に説明します。そのために以前に定義した文法の抜粋を載せます。

   ::=  ('+' )* |  ('-' )*
         ::=  ('*' )* |  ('/' )*
       ::= '('  ')' | '+'  | '-'  | 

上記の定義での部分は、字句解析の処理で単なる数値として既に処理済みです。そのため、文法として意識しなければならないのは、の3つの非終端記号です。非終端記号とは文法を構成するルールに名前を付けたもので、そのルールは::=で区切られた右辺に表現されています。実際に数式と文法を比較して解析するには、まず与えられた数式ととを比較します。そして、右辺の形に展開します。右辺の中に非終端記号が出てくる場合は、同様にその非終端記号の右辺の形に展開していきます。それらを繰り返すことで、最終的に非終端記号は消えてなくなり、(,),+,-,*,/,数値といった終端記号に置き換えられたら、構文解析は正常終了です。

コンピュータに行わせる処理はその前に人間が机上で同じことができなければ実装できません。まずは机上で、さまざまな式を文法に照らし合わせてみましょう。

例1

1+2

これを解析すると以下の図1のようになります。この図では、式と構文定義を上から順に照らし合わせて、非終端記号を右辺に展開していきます。

図1 式「1+2」の解析

              // ではじまる。
→             // ではじまる。
→             // で、今のトークンはNumber(1)のため、である。
                       // 「1」は構文定義に合致した。
→  +    // 次のトークンはOpAddのため、その次はではなくである。
                       // 「+」は構文定義に合致した。
→  +  // ではじまる
→  +  // で、今のトークンはNumber(2)のため、である。
                       // 「2」は構文定義に合致した。
                       // 次のトークンはEofのため、構文解析は終了。

式「1+2」の字句解析では、Number、OpAdd、Number、Eofのトークンの並びが得られます。構文解析の手順は次の通りです。

  1. 式はです。
  2. で始まります。
  3. で始まります。
  4. (、+、-のどれかで始まるか、数値です。式の最初のトークンはNumber(1)のため、文法定義の中のが該当します。ここでの解析は終了です。
  5. 1つ戻って、の解析を続けます。の解析のうち、始めのの解析は終わっているので、次のトークンとして想定されるのは、*、/のどちらかか、そうでなければは終了です。Numberの次のトークンはOpAddで*、/のどちらでもないので、ここでの解析は終了です。
  6. 1つ戻って、の解析を続けます。の解析のうち、始めのの解析は終わっているので、次のトークンは+、-のどちらか、そうでなければの解析は終了です。次のトークンはOpAddのため、さらにその次のトークンはが期待されます。
  7. では始まります。
  8. (、+、-のどれかで始まるか、数値です。次のトークンはNumber(2)のため、文法定義の中のが該当します。ここでの解析は終了です。
  9. 次のトークンはEofのため、これで構文解析は完了です。


式が複雑になっても、考え方は同じです。もう1例挙げます。次の例2について、自分でコンピュータになったつもりで解析してみてください。

例2

1+(2-3)*4/5

図2 式「1+(2-3)*4/5」の解析

 +  +  + (  + (  + (  + (  + (  -  + (  -  + (  -  + (  -  )
→  + (  -  ) *  + (  -  ) *  + (  -  ) *  /  + (  -  ) *  / 

今回はここまでです。次回も引き続き構文解析についての説明です

数式を解くプログラム その2 - 字句解析1

id:yone-ken:20090218:1234954345 の続きです。前回記載した数式の文法を踏まえた話をしますので、必要に応じて参照してください。「数式を解く」ためにはまず字句解析という処理が必要です。
wikipedia:字句解析
例によって細かい話は上記を参照して頂くとして、字句解析というのはざっくり言うと字句に分解することです。字句は文法上で意味のある一塊です。具体的には以下の数式で見てみましょう。

20+9*(10-2)/8+1.2

このような式があったとすると、字句に分解すると

20, +, 9, *, (, 10, -, 2, ), /, 8, +, 1.2

となります(ここでの,は単なる区切りで、字句ではありません)。つまり、数値なのか演算子なのか(演算子であればどの演算子なのか)といったことを区別する作業です。これを踏まえて、これらの字句を区別するために列挙型を作成します。以下にその列挙型の定義を記載します。ちなみに、字句のことを格好よく言うとトークン(token)といいますので、トークンの種類ということでTokenKindと命名しています。さらに余談を書くとIronPythonの同様の役目を負っている列挙型もTokenKindと命名されています。

enum TokenKind
{
    LParen,
    RParen,
    OpAdd,
    OpSub,
    OpMul,
    OpDiv,
    Number,
    Invalid,
    Eof
}

上から順に(, ), +, -, *, /, 数値, 無効な字句、式の終わり、を意味するものとします。丸括弧は英単語ではParenthesisとなりますが、長ったらしく、かつ、日本人はタイプミスしまくりそうなので、ここでは今時の命名ではなく短縮した名前にしています。いわずもがなですがL/RはLeft/Rightの意味です。Opと頭についているものは、演算子のグループです。Invalid(無効な字句)を用意しているのは、与えられる式が正しい文法に則ったものとは限らないため、想定外の字句を発見した場合に使います。EofはEnd Of Fileの略でファイルの終端を表す言葉のため正しい命名ではありませんが、ここではそんな細かいことは気にしないことにします(正確にはEnd Of ExpressionだとかEnd Of Lineでしょう)。この命名がいまいちだなと思ったら、リファクタリングで名称を好きに変更しましょう。Eofはとにかく式の終わりを意味します。

この列挙型とは別にトークンを表すためにTokenクラスを作成します。Tokenクラスを作る目的は、TokenKindだけでは後で困るからです。例えばTokenKind.Numberのトークンだった場合、後の処理で実際の数値が必要になります。そのため、TokenKind.Numberで数値だと判別できるだけでなく、実際の値も情報として保持しておく必要がありそうです。また、数式を評価するときに無効なトークンを見つけた場合は、後で数式のどの場所が不正なのかを判断できるようにしたいところです。というわけで、以下のようなTokenクラスを作成します。

class Token
{
    public int Column { get; private set; }
    public TokenKind Kind { get; private set; }
    public object Value { get; private set; }

    public Token(TokenKind kind, int column)
        : this(kind, column, null)
    {
    }

    public Token(TokenKind kind, int column, object value)
    {
        this.Kind = kind;
        this.Column = column;
        this.Value = value;
    }
}

TokenクラスはColumn、Kind、Valueの3つのプロパティを持ちます。Valueトークンが数値だった場合に実際の値を保持します。また、Columnは数式を表す文字列でのトークンの開始位置を保持します。想定している数式は単一行のため、行番号は考慮しません。トークンの終了位置もあまり重要でないので考慮しません。

ここまでで字句解析を行うための準備ができました。字句解析のメイン処理は次回に説明します。