LoginSignup
0

More than 5 years have passed since last update.

Eventをmergeしたりsplitしたりってどういうこと? - Eventモジュールの関数

Posted at

前置きをスキップするには「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度しか発火させないようにTimerAutoResetプロパティを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>にはイベント処理を行う関数が持つ引数の型を定義することもできます。

そしてEventPublishプロパティを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)  // イベント処理の設定

このようにイベントを定義したら発火の処理も必要になります。それはEventTriggerメソッドで行います。このメソッドの引数がイベント処理を行う関数に引き渡されます。

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メソッドと同じです。

Event.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"

さきほどのAlarmRingイベントにイベント処理を設定する部分は以下のように書き換えても等価になります。

イベントのAddメソッドとEvent.addは等価
alarm.Ring |> Event.add (fun (_, t) -> printfn "%Aになりました" t)
alarm.Ring |> Event.add (fun (s, _) -> printfn "%sによるイベント処理が起動しました"
                                       <| s.GetType().Name)

Event.choose 判定関数 イベント

イベント処理を実行するかどうかをSome argsNoneで判定します。Some argsであればargsをイベント処理に引き渡して実行します。Noneであればイベント処理を実行しません。

こうした判定を行うものにはEvent.filterもあります。こちらはbool (true/false)で判定します。

Event.choose
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 判定関数 イベント

イベント処理を行うかどうかをtruefalseで判定します。trueならイベント処理を行います。falseならイベント処理は行われません。

こうした判定を行うものにはEvent.chooseもあります。こちらはoption (Some/None)で判定します。

Event.filter
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 変更関数 イベント

イベント処理の結果を別の値に変更します。イベント処理には変更された値が引き渡されます。

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のどちらにも同じイベント処理を設定します。

Event.merge
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 イベント

イベント処理に前回発火したときの引数と今回発火した時の引数の両方を同時に引き渡します。初回に発火した時は引数の保存だけが行われ、イベント処理は行われません。

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もあります。

Event.partition
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 累積関数 初期値 イベント

イベント処理の引数に対して累積的な処理を行い、その結果をイベント処理に引き渡します。

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もあります。

Event.split
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"

ここまでご覧いただいた方々の参考になればと思います。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0