🔥

逆ポーランド記法電卓をF#とC#で実装してみる

2024/03/18に公開

「すごいHaskell 楽しく学ぼう!」の10章で取り上げられている逆ポーランド記法電卓を少しアレンジしたものを、F#とC#を使って実装してみました。

  • 与えられた逆ポーランド記号で書かれた文字列を数値と演算子に分解して、listをスタックに見立てて、詰める
  • スタックを畳み込み込む

現状、入力エラーなどの不測の事態は考慮していません。後に、monadを使ってエラーハンドリングできるものに修正する予定。

以下、実行のイメージ

let input = "10 4 3 + 2 * -"
let ans = solveRPN input
printfn "%A" ans
// => -4

型を定義する

「すごいHaskell 楽しく学ぼう!」では、文字列のまま処理を進めていましたが、関数型プログラミングらしく型を作ってみました。

F#

// 演算子
type Operator = 
    | Addition
    | Subtraction
    | Multiplication
    | Division

// スタックに積むアイテム
type Item = 
    | Number of double
    | Operator of Operator

C#

C#には判別共用体がないので、record型を使って定義する

// 演算子
public abstract record Operator();

public record Addition() : Operator;
public record Subtraction() : Operator;
public record Multiplication() : Operator;
public record Division() : Operator;

// スタックに積むアイテム
public abstract record Item();

public record Number(double value) : Item;
public record Ope(Operator ope) : Item;

関数を実装する

F#

type SolveRPN = string -> double

let solveRPN: SolveRPN = 
    fun expression ->
        // 文字列をItemへ変換してスタックへ詰める
        let createStack = 
            fun (exp: string) ->
                let splited =  Array.toList(exp.Split(' '))
                let parse str = 
                    match str with
                    | "+" -> Addition |> Operator
                    | "-" -> Subtraction |> Operator
                    | "*" -> Multiplication |> Operator
                    | "/" -> Division |> Operator
                    | n -> double n |> Number
                List.map parse splited

        // スタックを畳み込む関数
        let foldingFunction stack item = 
            match stack, item with
            | ((Number x)::(Number y)::ys), Operator o -> 
                match o with
                    | Addition -> (Number (y + x)) :: ys
                    | Subtraction -> (Number (y - x)) :: ys
                    | Multiplication -> (Number (y * x)) :: ys
                    | Division -> (Number (y / x)) :: ys
            | xs, n -> n :: xs
        expression 
        |> createStack 
        |> List.fold foldingFunction [] 
        |> List.head 
        |> function 
            | Number x -> x
            | _ -> 500.0

C#

var solveRPN = (string exp) =>
    {
        // 文字列をItemへ変換してスタックへ詰める
        var createStack = (string exp) =>
        {
            var splited = exp.Split(' ').ToList();
            static Item parse(string str)
            {
                return str switch
                {
                    "+" => new Ope(new Addition()),
                    "-" => new Ope(new Subtraction()),
                    "*" => new Ope(new Multiplication()),
                    "/" => new Ope(new Division()),
                    _ => new Number(double.Parse(str))
                };
            }
            return splited.Select(x => parse(x)).ToList();
        };

        // スタックを畳み込む関数
        var foldingFunction = (ImmutableArray<Item> stack, Item item)
            => (new { A = stack, B = item }) switch
                {
                    { A: [var x, var y, .. var xs], B: Ope o } =>
                        new { X = x, Y = y, O = o.ope } switch
                        {
                            { X: Number n2, Y: Number n1, O: Addition } => xs.Insert(0, new Number(n1.value + n2.value)),
                            { X: Number n2, Y: Number n1, O: Subtraction } => xs.Insert(0, new Number(n1.value - n2.value)),
                            { X: Number n2, Y: Number n1, O: Multiplication } => xs.Insert(0, new Number(n1.value * n2.value)),
                            { X: Number n2, Y: Number n1, O: Division } => xs.Insert(0, new Number(n1.value / n2.value)),
                            { X: _, Y: _, O: _ } => xs.Insert(0, item)
                        },
                    { A: [.. var xs], B: _ } => xs.Insert(0, item)
                };
        var ans = createStack(exp).Aggregate([], foldingFunction).First();
        return ans switch { Number n => n.value, _ => 500 };
    };

まとめ

関数型プログラミングに寄せた書き方をしているので、当たり前といえば当たり前なのですが、全く同じことをするとF#の方が圧倒的にすっきりしたコードになっています。C#の方は、ヘルパー関数をいくつか用意すれば、もう少しはすっきりさせることはできますが、いずれにせよひと手間かかるということになります。

また、C#の方はパターンマッチの使い方が今一つ分からず、かなりぎこちない実装になってしまいました。※この辺もっとこうした方が良いなどのご意見がありましたら、教えていただけると助かります。

Discussion