Monadic Rust: Part I
(Haskell主義亡我Rust之心不死啊!)這篇文章也許可以幫助你理解Monad。
作者在前文: Rust中Functor的可行性中分析了Rust中實現Functor的可行性。
起因是Rust官方團隊的withoutboats在實現非同步的過程中,發了一系列的推文說了Rust中並不能像Haskell的do-notation來實現非同步,而是用async/await來代替的原因。
該文作者寫文章分析了Rust中實現functor的可行性,闡述了一個重要的觀點:
Rust將效能作為最高優先順序,所以就不可能像Haskell那樣過多地傾向於型別的抽象,而是更傾向於使用trait作為抽象。所以在以效能優先的考慮下,就沒有提供haskell那樣的HKT,所以不能使用monad來實現Future。
原文:Read More
理論上,future/promise也可以看作是monad是一個例項:
—— Monad is just a fancy name for the Wrapper interface above
class
Monad
m
where
return
::
a
->
m
a
—— wrap
(
>>=
)
::
m
a
->
(
a
->
m
b
)
->
m
b
—— chain
—— Promises are monads, so we can make an instance for it
instance
Monad
Promise
where
return
value
=
Promise
。
resolve
value
value
>>=
callback
=
Promise
。
then
value
callback
—— Then the do-syntax becomes equivalent to async/await
getBalances
::
Promise
(
Map
String
String
)
getBalances
=
do
accounts
<-
getAccounts
balances
<-
getBalance
accounts
return
(
Map
。
fromList
(
zip
accounts
balances
))
上面是使用monad實現promise的一個示例(來源)。
所以,作者在前文中提供了一個基於trait的Functor和Monad實現(高階trait),純粹是作為學術上的探索。
在今天這篇文章中,作者進一步探索了do-notation在Rust中的實現。
do-notation是Haskell中的語法,用於操作Monad,而不再需要很長的巢狀函式。但目前在Rust中實現do-notation的問題在於:Rust的閉包並不遵循TCP(Tennent一致性原則,Tennent’s Correspondence Principle)。
// This function returns `5`。
fn
early_return
()
->
u8
{
return
5
;
0
}
該函式會返回5,但是在換成閉包,就得到另外的結果:
// This function returns `0`。
fn
abstracted_early_return
()
->
u8
{
(
||
return
5
)();
0
}
這裡
(|| return 5)();
返回的是
()
。
return
在這裡沒有作用。這就是withoutboat在推文中所說的:
Rust’s imperative control flow statements like
return
and
break
inside of do notation also doesn’t make sense, because we do not have TCP preserving closures。
所以,作者嘗試在給出一種解決方案,實現一個宏:
do
!
{
expr1
;
let
!
a
=
expr2
;
expr3
;
let
!
b
=
expr4
;
expr5
;
expr6
(
a
,
b
);
return
!
expr7
;
}
等價於:
surface
!
{
expr1
;
bubble
!
expr2
。
bind
(
|
a
|
{
expr3
;
bubble
!
expr4
。
bind
(
|
b
|
{
expr5
;
expr6
(
a
,
b
);
Monad
::
unit
(
expr7
)
})
})
}
其中bubble!等價於:
match
expr
{
ControlFlow
::
Return
(
_
)
=>
return
expr
,
ControlFlow
::
Break
(
Some
(
_
))
=>
break
expr
,
ControlFlow
::
Break
(
None
)
=>
break
,
ControlFlow
::
Continue
=>
continue
,
ControlFlow
::
Value
(
_
)
=>
expr
,
// We‘d also want `Yield` here eventually,
// but that comes with its own problems,
// which is a story for another time。
}
surface! block
等價於:
enum
ControlFlow
<
T
>
{
Return
(
T
),
Break
(
Option
<
T
>
),
Continue
,
// `Value` simply means we’re forwarding
// the value without effecting any control
// flow。
Value
(
T
),
}
match
(
||
block
)()
{
ControlFlow
::
Return
(
t
)
=>
return
t
,
ControlFlow
::
Break
(
Some
(
t
))
=>
break
t
,
ControlFlow
::
Break
(
None
)
=>
break
,
ControlFlow
::
Continue
=>
continue
,
ControlFlow
::
Value
(
t
)
=>
t
,
}
透過自定義的ControlFlow,捕獲閉包的控制流,並且在其外對其進行轉發,假裝從閉包控制流中逃脫,這就是一種do-notation。
但這種實現方式效能上還有問題。也有人在論壇裡提出pre-RFC讓Rust實現TCP式閉包,比如:
fn
main
(){
// `return` would be a divergent expression and return from `main`
let
id
:
i32
=
“321”
。
parse
()。
or_else
(
||
return
);
}
允許從閉包中返回。但是官方並不同意這樣做,主要理由有兩點:
首先,這樣肯定會破壞現有的Rust生態
其次,如果允許閉包在堆疊之間跳轉則會放大「異常安全」的問題。
所以,官方應該是不會支援TCP閉包的。
另外,在Redox專案裡實現了一個crate,來達成這種效果:control_flow ,但該包也有說明:除非你真的需要它的時候,你再用它。