かたちづくり

つれづれに、だらだらと、おきらくに

処理の進捗通知について

私は計算時間がかかる処理を作ることがちょくちょくあります。といっても何時間も計算機をぶん回すような計算はあまりなくて、数秒から数十秒程度の計算が殆ど、長くても数分でしょうか。この程度の計算時間でも計算処理の進捗状況が可視化されないとユーザーはイライラを感じますから、プログレスバーで大まかな進捗状況を可視化する必要が出てきます。

しかし当然ながら計算アルゴリズムを ProgressBar のような GUI 部品に依存させる設計は出来ませんから、処理の進捗を通知する部分をGUIから切り離す必要があります。今までは計算処理APIの引数にコールバック(のようなもの)を渡して進捗の通知を行ったりしていました。これを F# ならどう書くのがスマートなのか、思案しています。

重要なポイントとして、ある計算処理の中で別の計算処理を呼び出すことが当然ながらあります。とある計算処理Aは、それを単独で使用されることもあれば、別の計算処理Bの中で呼ばれることもあるということです。Aを単独で使用する場合はAの呼び出し終了時に進捗は100%で良いですが、処理Bから処理Aを呼び出している場合にはAだけで進捗を100%とするわけにはいきません。処理Bにおいて処理Aが占める割合を重み付けして進捗を通知する必要があります。こういう通知処理をスマートに書く方法が欲しいと思っています。

最初は「コンピュテーション式ってやつを勉強して使ってみたい」と思って考えていました。そういう意味では目的と手段が逆になっていたかもしれません。今はだんだん、コンピュテーション式にこだわらなくてもいいかな、という気持ちになってきています。(まだ自分の中で結論が出たわけではありません。とりあえず現状では、ということです。)

問題設定

サンプルとして計算時間がかかる処理を3つ用意します。listener というのは進捗の通知を受け取るコールバックで、float -> unit 型です。

let heavyFuncA listener =
  printfn "heavyFuncA started.."
  for i in 1..10 do Thread.Sleep 10; listener 0.1
  100

let heavyFuncB a listener =
  printfn "heavyFuncB started.."
  for i in 1..10 do Thread.Sleep 10; listener 0.1
  a / 2

let heavyFuncC b listener =
  printfn "heavyFuncC started.."
  for i in 1..10 do Thread.Sleep 10; listener 0.1
  b / 5

これら3つの関数を順番に呼び出す計算処理をスマートに書く方法を検討していきます。

ナイーブな書き方

let heavyFuncNaive listener =
  let a = heavyFuncA (fun step -> listener (0.2 * step))
  let b = heavyFuncB a (fun step -> listener (0.5 * step))
  let c = heavyFuncC b (fun step -> listener (0.3 * step))
  c

これでもいいのかもしれませんが、ちょっとダサい感じがします。
(0.2, 0.5, 0.3 といった数値が各関数の重み付けです。合計が 1.0 となるように重みを配分する必要があります。)

関数の合成を使う

let heavyFuncByComposition listener =
  let a = heavyFuncA ((*) 0.2 >> listener)
  let b = heavyFuncB a ((*) 0.5 >> listener)
  let c = heavyFuncC b ((*) 0.3 >> listener)
  c

だいぶいい感じになりました。こうやって合成して書けることにしばらく気づきませんでした。まだ修行が足りませんね…。もうこれでいいんじゃないの、という気分にもなってきますが…。

演算子を定義してみる

こういう演算子を定義してみます。

let ( *>>) weight listener = (*) weight >> listener

するとこう書けます。

let heavyFuncByOperator listener =
  let a = heavyFuncA (0.2 *>> listener)
  let b = heavyFuncB a (0.5 *>> listener)
  let c = heavyFuncC b (0.3 *>> listener)
  c

さっきよりちょっとだけ短くなりました。うーん、これだけのために演算子を定義するのは evil かもしれません…。どうなんでしょう?

weightListener 関数

こういう関数を定義します。前述の *>> 演算子を利用しています。というか、*>> 演算子と引数の順序が逆になっただけです。

let weightListener listener weight = weight *>> listener

するとこう書けます。

let heavyFuncByWeightListener listener =
  let w = weightListener listener
  let a = w 0.2 |> heavyFuncA
  let b = w 0.5 |> heavyFuncB a
  let c = w 0.3 |> heavyFuncC b
  c

w の定義行が一行増えますが、なかなかよい感じです。

コンピュテーション式

分かりやすさのため、次のような関数型に別名を付けておきます。

type ProgressListener = float -> unit
type Progressive<'a> = ProgressListener -> 'a

次のようなビルダーを定義してみました。

type ProgressiveBuilder (listener : ProgressListener) =
  member this.Bind((weight, calculation : Progressive<'a>), remaining : 'a -> Progressive<'b>) : Progressive<'b> = 
    fun listener -> calculation (weight *>> listener) |> remaining <| listener

  member this.Return x : Progressive<_> = fun _ -> x 
  member this.Zero ()  : Progressive<_> = fun _ -> Unchecked.defaultof<_>
  member this.Delay x = x
  member this.Run f = f() <| listener
  member this.Combine (_, f) = f ()

let progressive listener = ProgressiveBuilder listener

すると次のように書けます。

let heavyFuncByCexpr listener =
  progressive listener {
    let! a = 0.2, heavyFuncA
    let! b = 0.5, heavyFuncB a
    let! c = 0.3, heavyFuncC b
    return c
  }

とてもシンプルで読みやすいですね!
これは @bleis さんからアドバイスを頂いたり、@nagat01 さんが作成したコード https://gist.github.com/nagat01/a5b5977286f56a353b77 を参考にしつつ、自分の好みで少し書き換えたものです。@bleis さん、@nagat01 さん、ありがとうございました。
しかし現状、while や for が使えません。これらを使えるようにするにはビルダーに While() や For() を定義する必要があるようですが、それは私の能力を超えているようです。@bleis さんの記事 詳説コンピュテーション式 - ぐるぐる~ とだいぶ睨めっこしたのですが、、、撃沈しました><