Freer Effectsが、だいたいわかった: 11-9 LambdaCase拡張
はじめに
LambdaCase拡張は、ちょっとした構文上の拡張なのだけど、これを使うことでコードがきれいになる。「どのようにきれいになるか」を「Erlangのプロセスを使ったコードをHaskellで書きなおす例」で説明する。
目次
(0). 導入
-
Freeモナドの概要
- Freeモナドとは
- FreeモナドでReaderモナド、Writerモナドを構成する
- 存在型(ExistentialQuantification拡張)の解説
- 型シノニム族(TypeFamilies拡張)の解説
- データ族(TypeFamilies拡張)の解説
- 一般化代数データ型(GADTs拡張)の解説
- ランクN多相(RankNTypes拡張)の解説
-
FreeモナドとCoyoneda
- Coyonedaを使ってみる
- FreeモナドとCoyonedaを組み合わせる
- いろいろなモナドを構成する
-
Freerモナド(Operationalモナド)でいろいろなモナドを構成する
- FreeモナドとCoyonedaをまとめて、Freerモナドとする
- Readerモナド
- Writerモナド
- 状態モナド
- エラーモナド
-
モナドを混ぜ合わせる(閉じた型で)
- Freerモナドで、状態モナドとエラーモナドを混ぜ合わせる
- 両方のモナドを一度に処理する
- それぞれのモナドを、それぞれに処理する
- Freerモナドで、状態モナドとエラーモナドを混ぜ合わせる
- 存在型による拡張可能なデータ構造(Open Union)
- 追加の言語拡張
- ScopedTypeVariables拡張
- TypeOperators拡張
- KindSignatures拡張
- DataKinds拡張
- MultiParamTypeClasses拡張
- [FlexibleInstances拡張] (
https://qiita.com/YoshikuniJujo/items/eb70e7978f333ef3b514 ) - OVERLAPSプラグマ
- FlexibleContexts拡張
- LambdaCase拡張
- Open Unionを型によって安全にする
- モナドを混ぜ合わせる(開いた型で)
- FreeモナドとOpen Unionを組み合わせる
- 状態モナドにエラーモナドを追加する
- Freer Effectsで、IOモナドなどの、既存のモナドを使用する
- 関数を保管しておくデータ構造による効率化
- いろいろなEffect
- 関数handleRelayなどを作成する
- NonDetについてなど
コード例
GitHubにコード例を置いておきます。
GitHub: YoshikuniJujo/test_haskell/tribial/qiita/try-lambda-case
Erlangの軽量プロセスの例
Erlangについての説明は、WikipediaやMatz氏の記事を参照してください。
Wikipedia: Erlang
Rubyistのための他言語訪問
上記の記事中の「計量プロセスを使ったコード例」を紹介する。
-module(pingpong).
-export([start/0, ping/2, pong/0].
ping(0, Pong_PID) ->
Pong_PID ! finished,
io:format("Ping finished\n", []);
ping(N, Pong_PID) ->
Pong_PID ! {ping, self()},
receive
pong -> io:format("Ping received pong\n", [])
end,
ping(N - 1, Pong_PID).
pong() ->
receive
finished ->
io:format("Pong finished\n", []);
{ping, Ping_PID} ->
io:format("Pong received ping\n", []),
Ping_PID ! pong,
pong()
end.
start() ->
Pong_PID = spawn(pingpong, pong, []),
spawn(pingpong, ping, [3, Pong_PID]).
上記のようにソースファイルpingpong.erlを作成して、つぎのように読み込み、実行する。
% erl
1> c(pingpong).
2> pingpong:start().
Pong received ping
Ping received pong
Pong received ping
Ping recieved pong
Pong received ping
Ping received pong
ping finished
Pong finished
関数pingは「回数N」と「pongを受け取りpingを送るプロセスのID」であるPong_PID」を受けとると、N回「pingを送りpongを受けとる」。N回の送受信が終了したらfinishedを送る。関数pongはpingを受けとったら、送信元のプロセスにpongを送りかえしループする。finishedを受けとったら終了する。
Haskellでのpingpongの実装
これとおなじものをHaskellで実装してみよう。ソースコードを示す。
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}
module PingPong where
import Control.Concurrent
import Control.Monad.STM
import Control.Concurrent.STM.TChan
data Ping = Ping (TChan Pong) | Finished
data Pong = Pong
ping :: TChan Pong -> Int -> TChan Ping -> IO ()
ping _self n pon | n < 1 = do
atomically $ writeTChan pon Finished
putStrLn "Ping finished"
ping self n pon = do
atomically $ writeTChan pon (Ping self)
r <- atomically (readTChan self)
case r of
Pong -> putStrLn "Ping received pong"
ping self (n - 1) pon
pong :: TChan Ping -> IO ()
pong self = do
r <- atomically (readTChan self)
case r of
Finished -> putStrLn "Pong finished"
Ping pin -> do
putStrLn "Pong received ping"
atomically $ writeTChan pin Pong
pong self
start :: IO ()
start = do
pin <- newTChanIO
pon <- newTChanIO
_ <- forkIO $ pong pon
ping pin 3 pon
これを対話環境で実行する。
*PingPong> start
Pong received ping
Ping received pong
Pong received ping
Ping received pong
Pong received ping
Ping received pong
piPnogn gf ifniinsihsehde
d
最後のfinishedのメッセージはPingとPongとでまざってしまっているが、これはプロセスが並行して走っていて、Ping側がfinishedのメッセージを表示するのに、Pong側の応答を待たないからだ。
Erlangでは、それぞれのプロセスがはじめから「受信箱」を用意されているがHaskellでは、明示的に用意してあげる必要がある。ここではTChanというSTMを利用するチャンネルを使用した。関数atomicallyについては、ここでは説明しない。
TChanを使うことで、writeTChanで書き込んだものが、readTChanから読み込める。これによってプロセス間でのデータのやりとりを実現している。
Haskellでも(Erlangでいうところの)軽量プロセスによるpingpongができた
すばらしい。TChanを使うことで、Erlangでプロセスがメッセージを送りあうのと同等のことができる。いいね。
ただ、すこし不満がある。Erlangでメッセージを受け取っている部分をみてみよう。
receive
finished ->
io:format("Pong finished\n", []);
{ping, Ping_PID} ->
io:format("Pong received ping\n", []),
Ping_PID ! pong,
pong()
end.
すばらしい。受け取ったメッセージをそのままパターンにマッチさせている。ムダがなくてきれいな文法だ。Haskellでの、おなじ部分をみてみよう。
r <- atomically (readTChan self)
case r of
Finished -> putStrLn "Pong finished"
Ping pin -> do
putStrLn "Pong received ping"
atomically $ writeTChan pin Pong
pong self
readTChanの結果で、いちど変数rを束縛している。それをcase文にわたすことで、パターンマッチしている。ムダだ。こういうムダに変数を使うのは美しくない。すこし気持ち悪い。
LambdaCase拡張を使う
そこで、LambdaCase拡張ですよ。ちょっとした話なのだけど、コードをきれいにする気の効いた拡張だ。LambdaCase拡張を使って書き直してみよう。
pong self = atomically (readTChan self) >>= \case
Finished -> putStrLn "Pong finished"
Ping pin -> do
putStrLn "Pong received ping"
atomically $ writeTChan pin Pong
pong self
ムダな変数rがなくなり、すっきりした。一般的には、つぎのようになる。
\x -> case x of ...
\case ...
変数xをとり、それをcase文を使ってパターンマッチするような関数を、\のあとに予約語caseを置くことで、スマートに記述することができる。
最終的なソースコード
最終的なソースコードを示す。
{-# LANGUAGE LambdaCase #-}
{-# OPTIONS_GHC -Wall -fno-warn-tabs #-}
data Ping = Ping (TChan Pong) | Finished
data Pong = Pong
ping :: TChan Pong -> Int -> TChan Ping -> IO ()
ping _self n pon | n < 1 = do
atomically $ writeTChan pon Finished
putStrLn "Ping finished"
ping self n pon = do
atomically $ writeTChan pon (Ping self)
atomically (readTChan self) >>= \case
Pong -> putStrLn "Ping received pong"
ping self (n - 1) pon
pong :: TChan Ping -> IO ()
pong self = atomically (readTChan self) >>= \case
Finished -> putStrLn "Pong finished"
Ping pin -> do
putStrLn "Pong received ping"
atomically $ writeTChan pin Pong
pong self
start :: IO ()
start = do
pin <- newTChanIO
pon <- newTChanIO
_ <- forkIO $ pong pon
ping pin 3 pon
まとめ
Erlangでプロセスがメッセージを受けとる構文では、メッセージで変数を束縛することなく、直接パターンにマッチさせることができる。ムダのないきれいな文法だ。HaskellではLambdaCase拡張を使うことで、おなじような、きれいな書きかたができる。