JavaScriptにおいて、Promiseのthenを使った書き方を、async/awaitを使って書き換えられるわけですけども
その書き換えって、Haskellのモナドでbind(>>=)を使った書き方を、Do記法に書き換えとそっくりじゃないですか?
ということで、どんな感じで対応しているか検証してみます。
コードの全文は最後に載せますね。合わせて60行もないです。
Haskell側のモナドとして、Eitherモナドを使うことにします。
Haskellのコードはimport Data.Either
しておきます。
#記事を通して使う関数
まずは、Promiseやモナドを返す関数oh
とyeah
を用意します。
oh = () => Promise.resolve('OH!!');
yeah = x => Promise.resolve(x + 'YEAH!!');
oh :: Either String String
oh = return "OH!!"
yeah:: String -> Either String String
yeah x = return $ x ++ "YEAH!!"
oh
は何も受け取らず、Promiseやモナドに入った文字列"OH!!"を返します。
yeah
は文字列を受け取り、後ろに"YEAH!!"を付け、Promiseやモナドに入れて返します。
Haskell側のreturn
はRight
と書いても同じです。
###出力系はこんな感じ
output = foo().then(x => x + 'Completed!!');
main = () => {
output.then(console.log);
};
main();
output = foo >>= (\x -> return $ x++ "Completed!!")
main = putStrLn $ fromRight "" output
foo
はまだ作ってませんが、このあと作るメイン処理のことです。
output
はfoo
の出力の中身に"Completed!!"を付け足します。
main
はそれを画面に出力しています。
#メイン処理foo
foo = () =>
oh()
.then(x => yeah(x))
.then(y => {return y + 'やったね!!';});
foo :: Either String String
foo = oh
>>= (\x -> yeah x)
>>= (\y -> return $ y ++ "やったね!!")
thenとbind(>>=)が対応していると考えれば、そっくりです。
でもちょっと違う所もありますね。
JavaScriptの方は、「引数を取らない関数」を表現するために()=>
という部分があります。
Haskellの方は「引数を取らない関数」と「定数」に区別がないので余計な記述がありません。
###出力結果(どちらも同じ)
OH!!YEAH!!やったね!!Completed!!
async/awaitを使ってfooを書き換えたbarを作成
ではいよいよ書き換えてみましょう。
bar = async () => {
x = await oh();
y = await yeah(x);
return y + 'やったね!!';
};
bar :: Either String String
bar = do
x <- oh
y <- yeah x
return $ y ++ "やったね!!"
いよいよそっくりですね!
出力系のfoo
をbar
に書き換えれば、さっきと同じ結果が出ます。
やはり同じように書けました。やったね!!
#結論
ということで、
「thenを使った書き方をasync/awaitで書き換える」
「bindを使った書き方をDo記法で書き換える」
この2つは少々の違いを無視すれば同じ。
あとは、何の違いを無視するか次第ですね。
上で述べたように、そっくりですが、違うところもありますので。
合同な三角形は「位置」や「向き」の違いを無視して「同じ」と言いますし、
「大きさ」の違いも無視すれば相似な三角形も「同じ」です。
今回の結論もそんなような話です!多分ね!でも同じと言っていいと思う!
#throwとcatchは何に対応する?
さてさて。
さらに対応関係がないか探ってみます。
Promiseにはエラーハンドリングの機能がありますから、これと同じことをHaskellで書くとどうなるか見てみます。
実はこのためにMaybe
モナドではなくEither
モナドを使っています。
##Haskell側にcatch
とthrow
を用意
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
が来た場合はそのままスルーして流します。
throw
はLeft
と同じです。
###bind
もthenn
に書き換えちゃえ
これは別に必要ではありませんが、さらに見た目をそっくりにするため、>>=
もthenn
に書き換えてしまいます。
then
は予約語なので残念ながら少しスペル違いのthenn
となっています。かっこ悪いですが妥協。
thenn :: Either a b -> (b -> Either a c) -> Either a c
thenn = (>>=)
##return
とthrow
、then
とcatch
の対称性
次のコードを見てください。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
return
とthrow
、then
とcatch
はそれぞれ右と左が入れ替わっているだけだということが読み取れます。
JavaScriptの方でもこのような対称性を意識して使ってみると理解が深まると思います。
要は2種類の「送り手と受け手のペア」がいるだけなんですね。慣例として片方をエラーの伝播につかっているだけで。
##output
にエラーハンドリング機能をつける
先程の出力系のoutput
を少しいじって、エラーハンドリングできるようにします。
output = bar()
.then(x => {return x + 'Completed!!';})
.catch(e => {return 'えらーだよ: ' + e;});
output = bar
`thenn` (\x -> return $ x ++ "Completed!!")
`catch` (\e -> return $ "えらーだよ: " ++ e)
作った関数thenn
とcatch
のおかげで、よく似ています。
Haskellのコードで、バッククォートで囲んである関数は中置記法を表しています。
これで実行すると、先程と同じ出力がでます。特にcatchするエラーがないからですね。
そこで、bar
の最後をreturn
ではなくthrow
に書き換えてみます。
bar = async () => {
x = await oh();
y = await yeah(x);
throw y + 'やったね!!';
};
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のエラーハンドリング機能については対応関係が見れたと思うけど、「解決を待つ機能」については未検証です。
#ソースコード
以上です。最後にソースコード全体を載せておきます。
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();
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