Edited at

Freer Effectsが、だいたいわかった: 11-9 LambdaCase拡張


Freer Effectsが、だいたいわかった: 11-9 LambdaCase拡張


はじめに

LambdaCase拡張は、ちょっとした構文上の拡張なのだけど、これを使うことでコードがきれいになる。「どのようにきれいになるか」を「Erlangのプロセスを使ったコードをHaskellで書きなおす例」で説明する。


目次

(0). 導入



  1. Freeモナドの概要


    • Freeモナドとは

    • FreeモナドでReaderモナド、Writerモナドを構成する



  2. 存在型(ExistentialQuantification拡張)の解説

  3. 型シノニム族(TypeFamilies拡張)の解説

  4. データ族(TypeFamilies拡張)の解説

  5. 一般化代数データ型(GADTs拡張)の解説

  6. ランクN多相(RankNTypes拡張)の解説


  7. FreeモナドとCoyoneda


    • Coyonedaを使ってみる

    • FreeモナドとCoyonedaを組み合わせる


      • いろいろなモナドを構成する






  8. Freerモナド(Operationalモナド)でいろいろなモナドを構成する


    • FreeモナドとCoyonedaをまとめて、Freerモナドとする

    • Readerモナド

    • Writerモナド

    • 状態モナド

    • エラーモナド




  9. モナドを混ぜ合わせる(閉じた型で)


    • Freerモナドで、状態モナドとエラーモナドを混ぜ合わせる


      • 両方のモナドを一度に処理する

      • それぞれのモナドを、それぞれに処理する





  10. 存在型による拡張可能なデータ構造(Open Union)

  11. 追加の言語拡張


    1. ScopedTypeVariables拡張

    2. TypeOperators拡張

    3. KindSignatures拡張

    4. DataKinds拡張

    5. MultiParamTypeClasses拡張

    6. FlexibleInstances拡張

    7. OVERLAPSプラグマ

    8. FlexibleContexts拡張

    9. LambdaCase拡張



  12. Open Unionを型によって安全にする

  13. モナドを混ぜ合わせる(開いた型で)


    • FreeモナドとOpen Unionを組み合わせる

    • 状態モナドにエラーモナドを追加する



  14. Freer Effectsで、IOモナドなどの、既存のモナドを使用する

  15. 関数を保管しておくデータ構造による効率化

  16. いろいろなEffect


    • 関数handleRelayなどを作成する

    • NonDetについてなど




コード例

GitHubにコード例を置いておきます。

GitHub: YoshikuniJujo/test_haskell/tribial/qiita/try-lambda-case


Erlangの軽量プロセスの例

Erlangについての説明は、WikipediaやMatz氏の記事を参照してください。

Wikipedia: Erlang

Rubyistのための他言語訪問

上記の記事中の「計量プロセスを使ったコード例」を紹介する。


pingpong.erl

-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で実装してみよう。ソースコードを示す。


src/PingPong.hs

{-# 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を置くことで、スマートに記述することができる。


最終的なソースコード

最終的なソースコードを示す。


src/PingPongLambdaCase.hs

{-# 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拡張を使うことで、おなじような、きれいな書きかたができる。