F# 里沒有三元運算符。雖說 if else 可以用作三元表達式。但是有時候為了流暢的 |> 下去,被迫寫出這樣的代碼:

... |> fun x -> if x then a.A else a.B

網上也有一些試圖用函數來代勞的,譬如自定義一個運算符:

let inline (|?) condition (consequence, alternative) =
if condition then
consequence
else
alternative

使用時候是這樣:

let foo (x: int) (a: Bar) =
x > 0 |? (a.A, a.B)

是很簡潔,但是,細心的同學可能會發現這裡有問題,為了更清晰反映這個問題,我們看看C#對應代碼:

public static int foo(int x, Bar a)
{
bool flag = x > 0;
int a@ = a.A@;
int b@ = a.B@;
if (flag)
{
return a@;
}
return b@;
}

簡單的說,a.A 和 a.B都被求值了。

有沒有辦法避免提前求值呢?

我們試試用計算表達式:

type TernaryBuilder() =

member inline __.Yield _ = ignore

[<CustomOperation("eith")>]
member inline __.Either(_, a, b) =
fun x ->
if x then
a
else
b
let tri = TernaryBuilder()

所用測試代碼如下:

let foo (x: int) (a: Bar) =
x > 2 |> tri { eith a.B a.C }

看看對應C#代碼是什麼:

public static int foo(int x, Bar a)
{
bool flag = x > 2;
int b@ = a.B@;
int c@ = a.C@;
if (flag)
{
return b@;
}
return c@;
}

結果與上面自定義符號「 |? 」 沒有區別。這是預料中的事。因為參數是以數值方式傳入的嘛。

有什麼解決的方法呢?下面就是今天要介紹的主角:ProjectionParameter

我們在參數a 和 b 前面分別加上 ProjectionParameter 特性,測試代碼不變:

type TernaryBuilder() =

member inline __.Yield _ = ignore

[<CustomOperation("eith")>]
member inline __.Either(_, [<ProjectionParameter>]a, [<ProjectionParameter>]b) =
fun x ->
if x then
a
else
b

編譯沒有出錯,但是測試函數的返回簽名從 int 變成了 unit ->int。那意味著什麼呢?

Either方法傳入的參數 a 和 b ,從原來的int 變成了 unit -> int,也就是說,他們傳入之前,變成了 Thunk 函數。好,現在我們把返回結果時的a、b改成函數調用:

type TernaryBuilder2() =

member inline __.Yield _ = ignore

[<CustomOperation("eith")>]
member inline __.Either(_, a, b) =
fun x ->
if x then
a()
else
b()

從 ILSply 看看 foo 函數變什麼樣子了?

public static int foo(int x, Bar a)
{
if (x > 2)
{
return a.B@;
}
return a.C@;
}

看來是成功了。如果還有些同學沒看明白,下面我再做一個測試:直接調用 tri.Either 方法:

tri.Either ((), (fun() -> a.A), (fun() -> a.B)) (x > 2)

編譯後結果跟上面一致,也就是說,加了ProjectionParameter特性之後,傳入參數a.A 自動變成了 fun() -> a.A

通過前面三元表達式的小演示,我大致介紹了 ProjectionParameter 的特性。相對於C#,F#的短路符號只有 || 和 &&, 而 ??、?. 等符號都缺乏。 計算表達式卻給我們開了一扇惰性求值的門,除了標準的yield 之外,自定義指令也可以通過 ProjectionParameter 特性打開封印,獲得自由而強大的力量。


推薦閱讀:
相关文章