LoginSignup
0

More than 5 years have passed since last update.

The Lazy.Force Awakens #FsAdvent

Posted at

この記事は F# Advent Calendar 2015 の19日目の記事です。 18日目は @CallMeKohei さんの 「コンソールにマリオを描いてみた!」でした。

The Lazy.Force Awakens

最近なんか「Forceの覚醒」とか「Forceを使え」という言葉を聞きます。そういえばF#にもForceがありますので、今日はF#のForceについて調べてみます。

Lazy.Force()

F#は正格評価の言語ですが、たまには遅延評価を使いたい時もあります。そんな時は評価を遅延させたい式をlazy (式)という風にlazyでくるみます。

lazyでくるまれた中の式が'a型を返す場合、lazy全体の式が返す型はLazy<'a>型になります。この時点ではlazyにくるまれた中の式はまだ評価されません。

そしてLazy<'a>型の値から元の'a型の値を取り出す際に初めてlazyにくるまれた中の式が評価されます。この時、元の値を取り出すためのメソッドがForce()です。

なお、一度既に評価された式の値はキャッシュされ、2回目以降のForce()の呼び出しには単にキャッシュされた値を返すだけになります。

let lazy1 = lazy (printfn "hoge"; 1)
// val lazy1 : Lazy<int> = 値は作成されていません。

// The Force Awakens
lazy1.Force()
// hoge
// val it : int = 1

lazy1.Force()
// val it : int = 1

1回目のlazy1.Force()の呼び出し時に初めてlazyでくるまれた式が評価され、副作用のprintfn "hoge"が実行された後に式の結果の1を返します。

2回目のlazy1.Force()の呼び出し時は単にキャッシュされた式の結果1を返しているだけなので、副作用のprintfn "hoge"は実行されていません。

なお、lazyは関数ではなく構文なので、以下のようなパイプラインで渡すような書き方はできません。

let lazy1 = (printfn "hoge"; 1) |> lazy

式の関数化による遅延実行との違い

わざわざlazyを使わなくても、似たような遅延実行を行うことは実は可能です。単に遅延実行させたい式を関数化してしまえばいいだけです。

ただlazyを使った場合と異なるのは、式の評価結果が自動的にキャッシュされるわけではないため毎回式が評価されることになります。

let func1 = fun () -> printfn "fuga"; 2
// val func1 : unit -> int

func1()
// fuga
// val it : int = 2

func1()
// fuga
// val it : int = 2

他の例として、mutableな値を参照する式について、両者の違いを見てみます。

let mutable s = "hoge"
let lazy2 = lazy (s)
let func2 = fun () -> s

lazy2.Force()
// val it : string = "hoge"
func2()
// val it : string = "hoge"

s <- "fuga"
lazy2.Force()
// val it : string = "hoge"
func2()
// val it : string = "fuga"

副作用がない式は毎回評価しても必ず同じ値を返すので、2回目以降は計算せずともキャッシュを返せば事足りるはずです。従って副作用がない式の評価を遅延させたい場合はlazyを使えばよいでしょう。逆に副作用がある式の評価を遅延させたい場合は式の関数かを使う必要があります。

Lazy<'a> と unit -> 'a の相互変換

unit -> 'aの関数をLazy<'a>に変換したり、その逆変換を行うことは可能でしょうか?

まずunit -> 'aLazy<'a>の変換ですが、それを可能とする関数Lazy.Createが用意されています。

// unit -> 'a を Lazy<'a> に変換する関数
let lazy3 = func1 |> Lazy.Create
// val lazy3 : Lazy<int> = 値は作成されていません。

lazy3.Force()
// fuga
// val it : int = 2

lazy3.Force()
// val it : int = 2

実はLazy.Createと同じことがLazy型のコンストラクタ呼び出しで可能です。F# 4以降はコンストラクタが関数と同じ扱いとなり、パイプラインで扱いやすくなったりしたので、もはやLazy.Createを使う意味はないかもしれません。

// System.Lazyクラスのコンストラクタ呼び出し。F#4以降は関数のように扱える
let lazy3' = func1 |> Lazy

逆にLazy<'a>unit -> 'aの変換ですが、シグネチャ上の変換であればLazy.Forceがそれに当たります。

しかし関数のシグネチャに変換された後でもキャッシュされる機能は残ったままなので、結局変換後の関数を何度呼び出しても式の評価は初回しかされません。

// Lazy<'a> を unit -> 'a に(シグネチャだけ)変換する方法
let func3 = lazy1.Force
// val func3 : (unit -> int)

func3()
// hoge
// val it : int = 1

func3()
// val it : int = 1

The dark side of the Force

Forceを使うものは時として「例外」という暗黒面に堕ちる場合があります。

lazy内の式の評価中に例外が発生した場合はどうなるのでしょうか?

結論としては、Force()実行時に普通に例外が投げられます。そしてこの例外もキャッシュされ、2回目以降の呼び出し時は即座にキャッシュされた例外が投げられます。

(ただし例外がキャッシュされるのは後述するLazyThreadSafetyModePublicationOnly以外の場合、かつ、Lazyコンストラクタで評価用の関数を渡した場合のみです。lazy構文を使用した場合は普通にキャッシュされます)

// lazy内で例外が出た場合は例外がキャッシュされて、評価のたびにキャッシュされた例外が投げられる
let mutable x = "Jedi"
let f() = if x = "Jedi" then 0 else failwithf "oops! : %s" x
let lazyDarkSide = f |> Lazy

x <- "Darth Vador"
lazyDarkSide.Force()
(*
System.Exception: oops! : Darth Vador
>    場所 FSI_0233.f@65-11.Invoke(String message) 場所 force.fsx:行 65
   場所 System.Lazy`1.CreateValue()
   場所 System.Lazy`1.LazyInitValue()
   場所 <StartupCode$FSI_0234>.$FSI_0234.main@() 場所 force.fsx:行 69
エラーのため停止しました
*)

x <- "Jedi"
lazyDarkSide.Force()
(*
> System.Exception: oops! : Darth Vador
   場所 FSI_0233.f@65-11.Invoke(String message) 場所 force.fsx:行 65
   場所 System.Lazy`1.CreateValue()
--- 直前に例外がスローされた場所からのスタック トレースの終わり ---
   場所 System.Lazy`1.get_Value()
   場所 <StartupCode$FSI_0236>.$FSI_0236.main@() 場所 force.fsx:行 80
エラーのため停止しました
*)

x <- "Anakin Skywalker"
lazyDarkSide.Force()
(*
System.Exception: oops! : Darth Vador
>    場所 FSI_0233.f@65-11.Invoke(String message) 場所 force.fsx:行 65
   場所 System.Lazy`1.CreateValue()
--- 直前に例外がスローされた場所からのスタック トレースの終わり ---
   場所 System.Lazy`1.get_Value()
   場所 <StartupCode$FSI_0240>.$FSI_0240.main@() 場所 force.fsx:行 92
エラーのため停止しました
*)

The Multi-thread of the Force

lazy内の式の評価時にはロックを取って行われるためスレッドセーフになります(lazy内での式中にて使用される型がすべてスレッドセーフである前提ですが)。

ただしキャッシュされた値を取得するだけの場合でもロックを取ることになるため、その分パフォーマンスに影響を及ぼす可能性があります。また、ロックが発生するということは当然デッドロックの危険性もあります。

lazy構文を使わずにLazy<'a>型のコンストラクタを使用することで、ロックを取るかどうかを指定することができます。

// Lazyのコンストラクタの別バージョン。シングルスレッド用
let lazy4 = Lazy((fun () -> printfn "piyo"; 3), false)
// Lazyのコンストラクタの別バージョン。初期化処理は同時に行われるがキャッシュは1つだけ作成される
let lazy5 = Lazy((fun () -> printfn "foo"; 4), System.Threading.LazyThreadSafetyMode.PublicationOnly)

指定の方法はboolをパラメータに渡す方法とSystem.Threading.LazyThreadSafetyModeを渡す方法の2種類があります。

  • trueまたはLazyThreadSafetyMode.ExecutionAndPublicationを渡した場合、最初に実行するスレッドがロックを取って式の評価と評価結果の値のキャッシュを行います。2回目以降の実行時はキャッシュ読み込みのためにロックを取ります。lazy構文を使用した場合はこれになります。
  • falseまたはLazyThreadSafetyMode.Noneを渡した場合、シングルスレッドで使用される前提となり、ロックを取りません。複数スレッドから使用された場合の動作は未定義です。
  • LazyThreadSafetyMode.PublicationOnlyを渡した場合、同時に式の評価が行われる可能性がありますが、キャッシュされるのは最初に実行されたスレッドでの結果のみで、他のスレッドでの実行結果は捨てられます。

通常はlazy構文を使い、シングルスレッドのみで使われることがわかっていてパフォーマンス重視の場合はfalseを渡すコンストラクタを使用すればよいでしょう。

Lazy型のその他の操作

Lazy<'a>型には今まで紹介した以外にもコンストラクタやプロパティを持っていますので、一気に紹介します。ついでにコンストラクタにLazyThreadSafetyMode.PublicationOnlyを渡した場合に例外がキャシュされないことの確認もしてみました。

// Lazyのコンストラクタの別バージョン。シングルスレッド用
let lazy4 = Lazy((fun () -> printfn "piyo"; 3), false)
// Lazyのコンストラクタの別バージョン。初期化処理は同時に行われるがキャッシュは1つだけ作成される
let lazy5 = Lazy((fun () -> printfn "foo"; 4), System.Threading.LazyThreadSafetyMode.PublicationOnly)
// Lazyのコンストラクタの別バージョン。指定した型のデフォルトコンストラクタを遅延呼び出しする
let lazyDefaultInt = Lazy<int>()

lazy4.IsValueCreated
// val it : bool = false

// Force()を使わなくてもValueプロパティで評価できる
lazy4.Value
// piyo
// val it : int = 3

lazy4.Value
// val it : int = 3

lazy4.IsValueCreated
// val it : bool = true

// Force()を使わなくてもパターンマッチで評価できる
lazy5 |> fun (Lazy x) -> x
// foo
// val it : int = 4

let lazy6 = Lazy(f, System.Threading.LazyThreadSafetyMode.PublicationOnly)

x <- "Darth Vador"
lazy6.Force()
(*
System.Exception: oops! : Darth Vador
>    場所 FSI_0233.f@65-11.Invoke(String message) 場所 force.fsx:行 65
   場所 System.Lazy`1.CreateValue()
   場所 System.Lazy`1.LazyInitValue()
   場所 <StartupCode$FSI_0243>.$FSI_0243.main@() 場所 force.fsx:行 126
エラーのため停止しました
*)

x <- "Jedi"
lazy6.Force()
// val it : int = 0

実はForce()使わなくてもValueプロパティでよかったんですね。Forceとはいったい……うごごご!!

この記事で使用したソースコード

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