LoginSignup
3
1

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-09-19

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

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1