ようこそ。睡眠不足なプログラマのチラ裏です。

F#でもオブジェクト指向したい。手始めにC#で書いたUndo,Redoの実装を移植してみました。あれ?F#にはprotectedアクセシビリティないの?ぐはぁ

F#は.NETの第一級言語だからオブジェクト指向だって余裕でできるよ

関数型の考え方に基づいて、汎用的に使えるUndo,Redo機能の実装しようと考えたとき、
関数型言語のノウハウがない自分には、シックリくる良い案が全く思い浮かびませんでした。


F#は関数型パラダイムを中心とした言語ですが、
.NET向けの第一級言語ということもあり、オブジェクト指向もしっかりサポートされています。
ということで、現時点では「それオブジェクト指向で書けるなら、
別に関数型のパラダイムにこだわらなくてもいんじゃね?」という結論に至りました。
せっかくのマルチパラダイムなんだから、F#でオブジェクト指向したっていいよね、と。


初心者なりにF#で多少プログラミングを勉強してきましたが、
F#を使ってオブジェクト指向的に、インターフェイスやクラスを定義したことは
ほとんどありませんでしたので、練習を兼ねて以前C#で書いた
Undo,Redoの実装って何回かしかやってない気がする。ジェネリックなCommandパターンとMementoパターンの応用で大体いけそうな気がする。 をF#に移植してみました。


F#でオブジェクト指向的にUndo,Redoを実装してみよう

F#の仕様上、protectedなアクセシビリティはないようなので、
非対称アクセサのアクセシビリティを用いてprotectedを設定できませんでしたが、
その他については、C#版からそれなりに忠実に移植できていると思います。
以下、C#版から移植したF#のサンプルコードです。

#light
namespace Library1
open System
open System.Collections.Generic 

///ICommandインターフェイス
type ICommand =
  ///呼び出し
  abstract Invoke: unit -> unit
  ///元に戻す
  abstract Undo: unit -> unit
  ///やり直し
  abstract Redo: unit -> unit

[<AbstractClass>]
///記念品()抽象クラス
type Memento<'a,'b>() =
  ///思い出データを取得または設定します。
  [<DefaultValue(false)>]val mutable _MementoData : 'a
  member this.MementoData with get() = this._MementoData and set(value) = this._MementoData <- value

  ///データ反映対象オブジェクトを取得または設定します。
  [<DefaultValue(false)>]val mutable _Target : 'b
  member this.Target with get() = this._Target and set(value) = this._Target <- value

  ///思い出を反映させます。
  abstract SetMemento: 'a -> unit
    
[<Sealed>]
///思い出更新コマンド
type MementoCommand<'a,'b> = 
  val mutable private _memento : Memento<'a,'b> 
  val mutable private _prev : 'a
  val mutable private _next : 'a
  new(prev : Memento<'a,'b>, next: Memento<'a,'b>) = 
    { _memento = prev;
      _prev = prev.MementoData;
      _next = next.MementoData;} 
  interface ICommand with
     ///呼び出し
     member this.Invoke() = 
       this._prev <- this._memento.MementoData 
       this._memento.SetMemento(this._next)
     ///元に戻す
     member this.Undo() =
       this._memento.SetMemento(this._prev)
     ///やり直し
     member this.Redo() = 
       this._memento.SetMemento(this._next)
 
[<Sealed>] 
///CommandManager
type CommandManager = 
  val private _maxStack  : int
  val private _undoStack : Stack<ICommand> 
  val private _redoStack : Stack<ICommand>

  ///コンストラクタ
  new() = 
    {_maxStack  = System.Int32.MaxValue;
     _undoStack = new Stack<ICommand>();
     _redoStack = new Stack<ICommand>();
    } 
     
  ///コンストラクタ
  new(maxStack) = 
    {_maxStack = maxStack;
     _undoStack = new Stack<ICommand>();
     _redoStack = new Stack<ICommand>();
    }
    
  ///呼び出し
  member this.Invoke (command : ICommand)  = 
           if this._undoStack.Count >= this._maxStack then false
           else
             command.Invoke ()
             this._redoStack.Clear ()
             this._undoStack.Push command
             true
  ///元に戻す
  member this.Undo = 
           if this._undoStack.Count = 0 then ()
           else
             let command = this._undoStack.Pop ()
             command.Undo ()
             this._redoStack.Push command

  ///やり直し
  member this.Redo = 
           if this._redoStack.Count = 0 then ()
           else 
             let command = this._redoStack.Pop ()
             command.Redo ()
             this._undoStack.Push command
  
  member this.Refresh =
           this._undoStack.Clear ()
           this._redoStack.Clear () 


短っ。なんかC#に比べてやたら簡潔に書けた気がするんだが。気のせいですかねw


クライアント側の実装

F#で画面作るのは面倒くさいのでw、クライアント側はC#で。
C#版のをちょこっと変えただけです。

using System;
using System.Windows.Forms;
using Library1;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        private Memento<string, TextBox> _memento;
        private CommandManager _cmdManager = new CommandManager();
        public Form1()
        {
            InitializeComponent();
            _memento = new TextBoxMemento(this.textBox1.Text, this.textBox1);
        }

        private void btnSave_Click(object sender, EventArgs e)
        {
            var current = new TextBoxMemento(this.textBox1.Text,this.textBox1);
            var cmd = new MementoCommand<string,TextBox>(_memento,current);
            if (!_cmdManager.Invoke(cmd))
            {
                MessageBox.Show("状態の最大保存数を超えました。");
                return;
            }
            _memento = current;
        }

        private void btnRefresh_Click(object sender, EventArgs e)
        {
            _cmdManager.get_Refresh();
        }

        private void btnUndo_Click(object sender, EventArgs e)
        {
            _cmdManager.get_Undo();
        }

        private void btnRedo_Click(object sender, EventArgs e)
        {
            _cmdManager.get_Redo();
        }
    }
}

CommandManagerに対する呼び出しが、
get_Refresh();get_Undo();get_Redo();に変わっているところに注目です。

using System.Windows.Forms;
using Library1;

namespace WindowsFormsApplication1
{
    /// <summary>
    /// TextBoxの思い出具象クラス
    /// </summary>
    public sealed class TextBoxMemento : Memento<string, TextBox>
    {
        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="mementoData">思い出データ</param>
        /// <param name="txtBox">データ反映対象オブジェクト</param>
        public TextBoxMemento(string mementoData, TextBox target)
        {
            this.MementoData = mementoData;
            this.Target = target;
        }

        /// <summary>
        /// ターゲットに思い出を反映させます。
        /// </summary>
        public override void SetMemento(string mementoData)
        {
            this.MementoData = mementoData;
            this.Target.Text = mementoData;
        }
    }
}


変更はありません。

using System;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    static class Program
    {
        /// <summary>
        /// アプリケーションのメイン エントリ ポイントです。
        /// </summary>
        [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            Application.Run(new Form1());
        }
    }
}

変更はありません。


以上です。お疲れさまでした。いい練習になりました。
関数型言語のF#で、わざわざオブジェクト指向なんてしなくていいだろう常考
なーんて意見もありそうな気もしますが、私はそうは思いません。
まぁ、protectedなアクセシビリティがないのはちょっと致命的かもしれませんけどね(;´ω`)