前置きをスキップするには「Eventモジュールの関数」に進んでください。
F#はMicrosoftのDon Syme氏が開発したプログラミング言語で、命令型、オブジェクト指向、関数型の要素を併せ持つマルチパラダイム言語と位置付けられています。実装はオープンソースのMITライセンスのもと、githubのリポジトリで公開されています。
実行環境は.NET Frameworkおよび.NET CoreやMonoといったマルチプラットフォームに対応していますが、サポート状況については各プラットフォームごとの情報を参照してください(「F# の概要」など)。
なお、ここで対象とするF#のバージョンは4.1(FSharp.Compiler.Tools 10.0.1)とします。
イベントとイベント処理
イベントとは「何かが起きた」ことを表します。「キーが入力された」や「ボタンがクリックされた」といった画面操作などが代表的です。そして「ボタンがクリックされたらログイン処理を行う」というように、イベントが起きた(発火)後に行う処理をイベント処理といいます。これから紹介するEventモジュールの関数は、こうしたイベントとイベント処理の関連付けを行うものです。
.NETのクラスで体験
イベントとイベント処理がどのように対応付けられているかをSystem.Timers.Timerクラスを使ったアラームを例に見ていきます。ポイントは以下の3つです。
1. イベントの種類
Timer
クラスでは、イベントとして「タイマーのスタート時点から一定の時間間隔」(Elapsed)が定義されていますので、これをアラームの処理のために利用します。
2. イベント処理を行う関数
アラームに必要なイベント処理として、設定時刻に到達したことを示すメッセージを画面に表示する関数を用意します。設定時刻は関数の引数から得られるargs.SignalTime
をそのまま利用します。
3. イベント処理の設定
イベント処理の関数をElapsed
イベントに設定します。設定はAdd
メソッドで行います。
また、イベントを1度しか発火させないようにTimer
のAutoResetプロパティをfalse
に設定しておきます。
Timerクラスによるアラーム
これらのポイントを踏まえて、アラームの処理を行うクラスを以下のようにしてみました。アラームが設定時刻に到達したときに表示する時刻はSignalTimeプロパティから得ています。
なおTimer
オブジェクトは非同期に実行されるため、起動するとすぐにプログラムが終了しますが、バックグラウンドではアラームの処理が継続しています。
type Alarm() =
member this.Start(dt: System.DateTime) =
if (dt - System.DateTime.Now) > System.TimeSpan(0L) then
printfn "現在時刻: %A" <| System.DateTime.Now
printfn "%Aにアラームが起動します" dt
(* アラームの設定 *)
let timer = new System.Timers.Timer(AutoReset = false) // Elapsedイベントは1回だけ発火(AutoReset = false)
timer.Elapsed.Add (fun args ->
printfn "%Aになりました" args.SignalTime) // 設定された時刻になったらその時刻(args.SignalTime)を表示
timer.Interval <- (dt - System.DateTime.Now).TotalMilliseconds // アラーム起動までの時間
(* アラームの起動(非同期) *)
timer.Start()
else
printfn "設定時刻を過ぎました"
(* 実行 *)
let alarm = new Alarm()
alarm.Start(System.DateTime.Now.AddSeconds(3.0)) // 現在時刻から3秒後にアラーム起動
(* 結果
現在時刻: 2018/03/01 11:14:00
2018/02/25 14:14:03にイベント処理が起動します
2018/02/25 14:14:03になりました
Alarmは終了しました
*)
イベントの定義と発火
ここからは、アラームの処理を行うイベントとその発火の処理を、さきほどの3つのポイントに沿って定義してみます。
1. イベントの種類
アラームが設定時刻に到達した時を表すイベントとしてAlarm
クラスのメンバーにRing
を定義します。
2. イベント処理
アラームのためのイベント処理はさきほどと同じく、設定時刻に到達したことを知らせる関数を定義します。関数の引数はイベントを発火するときに引き渡すものと同じでなければなりません。
3. イベント処理の設定
ここではイベントそのものを定義するところから始めます。
F#ではイベントをEvent<'T>で表します。これを使ってイベントを定義したり発火させたりできます。最も簡単に定義するにはnew Event<_>()
とします。型変数<'T>
にはイベント処理を行う関数が持つ引数の型を定義することもできます。
そしてEvent
のPublishプロパティをRing
に設定すると、そこにイベント処理を設定できるようになります。その方法はプロパティとよく似ていますが、[<CLIEvent>]
という属性をつけるところが違います。
type Alarm() =
let ev = new Event<Alarm * System.DateTime>() // 型変数はTriggerメソッド(イベント処理の関数)の引数に合わせる
[<CLIEvent>]
member this.Ring = ev.Publish // イベントの定義
// ... 略
これでイベント処理を行う関数を設定できます。
let alarm = new Alarm()
alarm.Ring.Add (fun (_, t) -> printfn "%Aになりました" t) // イベント処理の設定
このようにイベントを定義したら発火の処理も必要になります。それはEvent
のTriggerメソッドで行います。このメソッドの引数がイベント処理を行う関数に引き渡されます。
ev.Trigger(this, dateTime) // イベントの発火(Alarmオブジェクトと設定時刻をイベント処理に引き渡す)
全体では以下のようにしてみました。こちらもアラームの処理を非同期で起動するため、プログラムはすぐに終了しますが、処理はバックグラウンドで継続しています。
// 設定した時刻になるとイベント処理が行われるアラーム
type Alarm() =
let ev = new Event<Alarm * System.DateTime>() // 型変数はTriggerメソッド(イベント処理の関数)の引数に合わせる
[<CLIEvent>]
member this.Ring = ev.Publish // イベントの定義
member this.Start(dateTime: System.DateTime) = // 設定された時刻になったらイベントを発火
if (dateTime - System.DateTime.Now) > System.TimeSpan(0L) then
printfn "%Aにイベント処理が起動します" dateTime
// 設定された時刻まで待ってイベントを発火
async {
System.Threading.Thread.Sleep(dateTime - System.DateTime.Now)
ev.Trigger(this, dateTime) // イベントの発火(Alarmオブジェクトと設定時刻をイベント処理に引き渡す)
} |> Async.Start // 非同期で起動
else
printfn "設定時刻を過ぎました"
(* アラームの実行 *)
let alarm = new Alarm()
// イベント処理の設定
alarm.Ring.Add (fun (_, t) -> printfn "%Aになりました" t)
alarm.Ring.Add (fun (s, _) -> printfn "%sによるイベント処理が起動しました"
<| s.GetType().Name)
// アラームのスタート(設定された時刻まで待つ)
alarm.Start(System.DateTime.Now.AddSeconds(3.0))
(* 結果 - イベント処理により表示されたメッセージ(例)
2018/02/25 14:03:25にイベント処理が起動します
2018/02/25 14:03:25になりました
Alarmによるイベント処理が起動しました
*)
Eventモジュールの関数
ここまではイベントやイベント処理の設定について見てきましたが、ここからは、こうした設定を手助けするEventモジュールの関数を紹介します。
Event.add イベント処理の関数 イベント
イベントにイベント処理の関数を設定します。これはAdd
メソッドと同じです。
(* イベント処理を行うクラス *)
type EventFunction() =
let ev = new Event<_>() // イベントの本体
[<CLIEvent>]
member this.Handler = ev.Publish // イベント処理の設定
member this.Fire (x: int) = ev.Trigger x // イベント発火
(* イベント処理の設定 *)
let ef = EventFunction()
ef.Handler |> Event.add (fun x -> printfn "add %d" x)
(* イベント処理の実行 *)
ef.Fire 2 // "add 2"
ef.Fire 3 // "add 3"
さきほどのAlarm
のRing
イベントにイベント処理を設定する部分は以下のように書き換えても等価になります。
alarm.Ring |> Event.add (fun (_, t) -> printfn "%Aになりました" t)
alarm.Ring |> Event.add (fun (s, _) -> printfn "%sによるイベント処理が起動しました"
<| s.GetType().Name)
Event.choose 判定関数 イベント
イベント処理を実行するかどうかをSome args
かNone
で判定します。Some args
であればargs
をイベント処理に引き渡して実行します。None
であればイベント処理を実行しません。
こうした判定を行うものにはEvent.filter
もあります。こちらはbool (true/false)
で判定します。
let ef = EventFunction()
ef.Handler
|> Event.choose (fun x -> if x % 2 = 0 then Some x else None) // Someならイベント処理を行う
|> Event.add (fun x -> printfn "choose %d" x) // 実行されるイベント処理
ef.Fire 2 // "choose 2"
ef.Fire 3 // イベント処理は実行されず
Event.filter 判定関数 イベント
イベント処理を行うかどうかをtrue
かfalse
で判定します。true
ならイベント処理を行います。false
ならイベント処理は行われません。
こうした判定を行うものにはEvent.choose
もあります。こちらはoption (Some/None)
で判定します。
let ef = EventFunction()
ef.Handler
|> Event.filter (fun x -> x % 2 = 0) // イベント処理を行うかどうか判定
|> Event.add (fun x -> printfn "filter %d" x)
ef.Fire 2 // "filter 2"
ef.Fire 3 // イベント処理は実行されず
Event.map 変更関数 イベント
イベント処理の結果を別の値に変更します。イベント処理には変更された値が引き渡されます。
let ef = EventFunction()
ef.Handler
|> Event.map (fun x -> if x % 2 = 0 then "even" else "odd") // 値を変更
|> Event.add (fun x -> printfn "map %s" x) // 変更された値が引き渡される
ef.Fire 2 // "map even"
ef.Fire 3 // "map odd"
Event.merge イベント1 イベント2
イベント1とイベント2のどちらにも同じイベント処理を設定します。
let ef1, ef2 = EventFunction(), EventFunction()
Event.merge ef1.Handler ef2.Handler // 2つのイベントに同じイベント処理が設定される
|> Event.add (fun x -> printfn "merge %d" x)
ef1.Fire 2 // "merge 2"
ef2.Fire 3 // "merge 3"
Event.pairwise イベント
イベント処理に前回発火したときの引数と今回発火した時の引数の両方を同時に引き渡します。初回に発火した時は引数の保存だけが行われ、イベント処理は行われません。
let ef = EventFunction()
ef.Handler
|> Event.pairwise
|> Event.add (fun (x, y) -> printfn "pairwise %d %d" x y) // x: 前回の引数, y: 今回の引数
ef.Fire 2 // 引数の保存のみ
ef.Fire 3 // "pairwise 2 3"
ef.Fire 4 // "pairwise 3 4"
Event.partition 判定関数 イベント
判定関数の結果がtrue
のときとfalse
のときとでその後のイベント処理を変えられるように、別々のイベントを新たに定義します。
新たなイベントを定義するものにはアクティブパターンでイベント処理を分けるEvent.split
もあります。
let ef = EventFunction()
let evenEvent, oddEvent = // Event.partitionによって分けられるイベント
ef.Handler
|> Event.partition (fun n -> n % 2 = 0) // 結果がtrueのとき: evenEvent, falseのとき: oddEvent
evenEvent |> Event.add (fun x -> printfn "partition even %d" x) // 判定関数の結果がtrueのときに行われる処理
oddEvent |> Event.add (fun x -> printfn "partition odd %d" x) // 判定関数の結果がfalseのときに行われる処理
ef.Fire 2 // "partition even 2"
ef.Fire 3 // "partition odd 3"
Event.scan 累積関数 初期値 イベント
イベント処理の引数に対して累積的な処理を行い、その結果をイベント処理に引き渡します。
let ef = EventFunction()
ef.Handler
|> Event.scan (fun s x -> s + x) 0 // 前回までの合計に今回の引数の値を足す
|> Event.add (fun x -> printfn "scan %d" x) // scanの関数の結果が引数となる
ef.Fire 2 // "scan 2"
ef.Fire 3 // "scan 5" (2 + 3 = 5)
ef.Fire 4 // "scan 9" (5 + 4 = 9)
Event.split アクティブパターン イベント
アクティブパターンごとのイベントを新たに定義します。パターンは2つまでです。イベント処理の引数はアクティブパターンが持つ値となります。
新たなイベントを定義するものにはbool
の値でイベント処理を分けるEvent.partition
もあります。
let ef = EventFunction()
// アクティブパターン
let (|Even|Odd|) (n: int) = if n % 2 = 0 then Even n else Odd n
// パターンごとのイベントを定義
let evenEvent, oddEvent = ef.Handler |> Event.split (|Even|Odd|)
// パターンごとのイベント処理
evenEvent |> Event.add (fun x -> printfn "split even %d" x)
oddEvent |> Event.add (fun x -> printfn "split odd %d" x)
ef.Fire 2 // "split even 2"
ef.Fire 3 // "split odd 3"
ここまでご覧いただいた方々の参考になればと思います。