Edited at

async/awaitがモナドのDo記法と同じっぽいので検証。throwとcatchもモナドに対応させる

JavaScriptにおいて、Promiseのthenを使った書き方を、async/awaitを使って書き換えられるわけですけども

その書き換えって、Haskellのモナドでbind(>>=)を使った書き方を、Do記法に書き換えとそっくりじゃないですか?

ということで、どんな感じで対応しているか検証してみます。

コードの全文は最後に載せますね。合わせて60行もないです。

Haskell側のモナドとして、Eitherモナドを使うことにします。

Haskellのコードはimport Data.Eitherしておきます。


記事を通して使う関数

まずは、Promiseやモナドを返す関数ohyeahを用意します。


promise.js

oh = () => Promise.resolve('OH!!');

yeah = x => Promise.resolve(x + 'YEAH!!');


monad.hs

oh :: Either String String

oh = return "OH!!"

yeah:: String -> Either String String
yeah x = return $ x ++ "YEAH!!"


ohは何も受け取らず、Promiseやモナドに入った文字列"OH!!"を返します。

yeahは文字列を受け取り、後ろに"YEAH!!"を付け、Promiseやモナドに入れて返します。

Haskell側のreturnRightと書いても同じです。


出力系はこんな感じ


promise.js

output = foo().then(x => x + 'Completed!!');

main = () => {
output.then(console.log);
};

main();



monad.hs

output = foo >>= (\x -> return $ x++ "Completed!!") 

main = putStrLn $ fromRight "" output


fooはまだ作ってませんが、このあと作るメイン処理のことです。

outputfooの出力の中身に"Completed!!"を付け足します。

mainはそれを画面に出力しています。


メイン処理foo


promise.js

foo = () =>

oh()
.then(x => yeah(x))
.then(y => {return y + 'やったね!!';});


monad.hs

foo :: Either String String

foo = oh
>>= (\x -> yeah x)
>>= (\y -> return $ y ++ "やったね!!")

thenとbind(>>=)が対応していると考えれば、そっくりです。

でもちょっと違う所もありますね。

JavaScriptの方は、「引数を取らない関数」を表現するために()=>という部分があります。

Haskellの方は「引数を取らない関数」と「定数」に区別がないので余計な記述がありません。


出力結果(どちらも同じ)

OH!!YEAH!!やったね!!Completed!!


async/awaitを使ってfooを書き換えたbarを作成

ではいよいよ書き換えてみましょう。


promise.js

bar = async () => {

x = await oh();
y = await yeah(x);
return y + 'やったね!!';
};


monad.hs

bar :: Either String String

bar = do
x <- oh
y <- yeah x
return $ y ++ "やったね!!"

いよいよそっくりですね!

出力系のfoobarに書き換えれば、さっきと同じ結果が出ます。

やはり同じように書けました。やったね!!


結論

ということで、

「thenを使った書き方をasync/awaitで書き換える」

「bindを使った書き方をDo記法で書き換える」

この2つは少々の違いを無視すれば同じ

あとは、何の違いを無視するか次第ですね。

上で述べたように、そっくりですが、違うところもありますので。

合同な三角形は「位置」や「向き」の違いを無視して「同じ」と言いますし、

「大きさ」の違いも無視すれば相似な三角形も「同じ」です。

今回の結論もそんなような話です!多分ね!でも同じと言っていいと思う!


throwとcatchは何に対応する?

さてさて。

さらに対応関係がないか探ってみます。

Promiseにはエラーハンドリングの機能がありますから、これと同じことをHaskellで書くとどうなるか見てみます。

実はこのためにMaybeモナドではなくEitherモナドを使っています。


Haskell側にcatchthrowを用意


monad.hs

catch :: Either a b -> (a -> Either c b) -> Either c b

catch (Left x) f = f x
catch (Right x) f = Right x

throw :: a -> Either a b
throw x = Left x


catchは、Left xを受け取ってfに適用します。Right xが来た場合はそのままスルーして流します。

throwLeftと同じです。


bindthennに書き換えちゃえ

これは別に必要ではありませんが、さらに見た目をそっくりにするため、>>=thennに書き換えてしまいます。

thenは予約語なので残念ながら少しスペル違いのthennとなっています。かっこ悪いですが妥協。


monad.hs

thenn :: Either a b -> (b -> Either a c) -> Either a c

thenn = (>>=)


returnthrowthencatchの対称性

次のコードを見てください。return>>=の中身も書き下して、4つの関数を並べてみます。

return :: a -> Either b a

return x = Right x

throw :: a -> Either a b
throw x = Left x

thenn :: Either a b -> (b -> Either a c) -> Either a c
thenn (Right x) f = f x
thenn (Left x) f = Left x

catch :: Either a b -> (a -> Either c b) -> Either c b
catch (Left x) f = f x
catch (Right x) f = Right x

returnthrowthencatchはそれぞれ右と左が入れ替わっているだけだということが読み取れます。

JavaScriptの方でもこのような対称性を意識して使ってみると理解が深まると思います。

要は2種類の「送り手と受け手のペア」がいるだけなんですね。慣例として片方をエラーの伝播につかっているだけで。


outputにエラーハンドリング機能をつける

先程の出力系のoutputを少しいじって、エラーハンドリングできるようにします。


promise.js

output = bar()

.then(x => {return x + 'Completed!!';})
.catch(e => {return 'えらーだよ: ' + e;});


monad.hs

output = bar

`thenn` (\x -> return $ x ++ "Completed!!")
`catch` (\e -> return $ "えらーだよ: " ++ e)

作った関数thenncatchのおかげで、よく似ています。

Haskellのコードで、バッククォートで囲んである関数は中置記法を表しています。

これで実行すると、先程と同じ出力がでます。特にcatchするエラーがないからですね。

そこで、barの最後をreturnではなくthrowに書き換えてみます。


promise.js

bar = async () => {

x = await oh();
y = await yeah(x);
throw y + 'やったね!!';
};


monad.hs

bar :: Either String String

bar = do
x <- oh
y <- yeah x
throw $ y ++ "やったね!!"


出力(どちらも同じ)

えらーだよ: OH!!YEAH!!やったね!!

barの最後をthrowに変えたら、見事にcatchされたね!


対応関係

ということで、こんな対応関係があると言えそうですね。

JavaScript
Haskell

return
Right

throw
Left

then
>>=

catch
(上記の自作関数)


その他考察


finallyはないの?

JavaScriptのfinallyに対応するものは、Eitherモナド側にはないのか、ということです。

これは恐らく、ない、と思います。

finallyに与えるaction関数は引数を受け取りませんが、引数受け取らない関数ということは、ただの定数です。

定数が、自身が読み取られる以外に何か働くなら副作用を起こすしかないです。

つまり、action関数の外側にある何かにアクセスする必要があるということですね。

Haskellでそれは基本的にできないので、結論、ないと思います。


で、Promiseはモナドなの?

入れ子にできないので違います。

今回は入れ子にする場面がなかったのでモナドと同じように振る舞ってますけども。

ちなみにRxに出てくるObservableなら入れ子にできるのでモナドだと思います。

私の記事ですが参考までに。

Observableってモナドらしいですよ


課題


  • Promiseのエラーハンドリング機能については対応関係が見れたと思うけど、「解決を待つ機能」については未検証です。


ソースコード

以上です。最後にソースコード全体を載せておきます。


promise.js

oh = () => Promise.resolve('OH!!');

yeah = x => Promise.resolve(x + 'YEAH!!');

foo = () =>
oh()
.then(x => yeah(x))
.then(y => {return y + 'やったね!!';});

bar = async () => {
x = await oh();
y = await yeah(x);
throw y + 'やったね!!';
};

output = bar()
.then(x => {return x + 'Completed!!';})
.catch(e => {return 'えらーだよ: ' + e;});

main = () => {
output.then(console.log);
};

main();



monad.hs

import Data.Either

oh :: Either String String
oh = return "OH!!"

yeah:: String -> Either String String
yeah x = return $ x ++ "YEAH!!"

foo :: Either String String
foo = oh
>>= (\x -> yeah x)
>>= (\y -> return $ y ++ "やったね!!")

bar :: Either String String
bar = do
x <- oh
y <- yeah x
throw $ y ++ "やったね!!"

thenn :: Either a b -> (b -> Either a c) -> Either a c
thenn = (>>=)

catch :: Either a b -> (a -> Either c b) -> Either c b
catch (Left x) f = f x
catch (Right x) f = Right x

throw :: a -> Either a b
throw x = Left x

output = bar
`thenn` (\x -> return $ x ++ "Completed!!")
`catch` (\e -> return $ "えらーだよ: " ++ e)

main = putStrLn $ fromRight "" output