LoginSignup
5
1

More than 3 years have passed since last update.

Go製のToolをHaskellで実装する

Last updated at Posted at 2020-05-25

初めに

最近Haskellの学習としてGo製のToolをHaskellで実装するようにしています。

Haskellでなにかしら作りたいと考えていて、そういえばGo製のToolはよく作られているから面白そうなものありそうだなと思ったのがきっかけです。

mockサーバであるhttplabをHaskellで作ることにしました。

まだ100%の実装ができていないのですが、ある程度形ができたので記事を書くことにしました。

screeencast.gif

ソースコードはこちらにあります。

Haskellでbrickというライブラリを使っていますが、使い方等は以前書いた記事を参考してください

GoもHaskellも至らぬところがあるので間違った箇所はアドバイスや助言をいただけると嬉しいです。

HaskellとGoもさほど違いがない箇所があったので、比較していきます。

引数のパース

Goではpflagを使ってパースしています。


pflag.StringVarP(&args.status, "status", "s", "200", "Specifies the initial response status.")

Haskellではoptparse-applivativeを使ってパースしてます。


parseStatus :: Parser Status
parseStatus = option (maybeReader $ fmap toEnum . readMaybe)
  $ long "status"
 <> short 's'
 <> value status200
 <> help "Specifies the initial response status."
 <> metavar "StatusCode"

こうやってみると使い方はさほど違いがないように感じます。

ただHaskellの方は関数の引数ではなくmonoidを使ってプログラム引数の構成が自由にできることと、
IntやBool, String以外のデータ型に対してもパースできる強みがあります。

ただその分記述量が多くなっています。

mock server

main.go

    srv := &http.Server{
        Addr:    fmt.Sprintf(":%d", args.port),
        Handler: http.Handler(middleware(NewHandler(ui, g))),
    }

    go func() {
        // Make sure gocui has started
        g.Execute(func(g *gocui.Gui) error { return nil })

        if err := srv.ListenAndServe(); err != nil {
            errCh <- err
        } else {
            ui.Info(g, "Listening on :%d", args.port)
        }
    }()

serverの情報を定義してgo rountineで起動させています。

Haskellはwaiを使って実装しています。

App.hs

  race_ (WAI.run (argPort cmd) $ middleware $ respHandler state chan)
            $ customMain vty buildVty (Just chan) app state


middlewareはまだちゃんと実装していないのでmiddleware=idでやっています。

こちらもサーバを非同期で起動しています。

handlerについては

main.go

// NewHandler returns a new http.Handler
func NewHandler(ui *ui.UI, g *gocui.Gui) http.Handler {
    fn := func(w http.ResponseWriter, req *http.Request) {
        if err := ui.AddRequest(g, req); err != nil {
            ui.Info(g, "%v", err)
        }

        resp := ui.Response()
        time.Sleep(resp.Delay)
        resp.Write(w)

    }
    return http.HandlerFunc(fn)
}

uiをpointerで渡すことでスレッド間の値を共有しています。

App.hs

respHandler :: TVar ResponseData -> BChan Requested -> Application
respHandler tvar bchan req send = do
  dt <- readTVarIO $ tvar
  threadDelay $ dt ^. resDelay
  send $ responseBuilder
    (dt ^. resStatusCode)
    (M.foldrWithKey' (\k a -> (toResHeader k a :)) [] $ dt ^. resHeaders)
    $ B.byteString $ T.encodeUtf8 $ dt ^. resBody
  <* writeBChan bchan (toRequested req)
  where
    toResHeader a b = (CI.mk $ T.encodeUtf8 $ CI.foldedCase a, T.encodeUtf8 b)

スレッド間の値を共有する為にTVar を使用しています

大きな違いとして、Requestについては、brickはuiの情報は純粋なデータしか受け付けていない為、アプリケーション定義イベントで渡すようにしていますがgocuiも同じことができると思います。

おまけ:Zipper

httplabはRequest情報を履歴で持つ機能が備わっています。それを実現するために内部データは次のように保持しています。

ui.go
type UI struct {
    ... 
    reqLock        sync.Mutex
    requests       [][]byte
    currentRequest int
}

requestsがrequestのあった履歴、currentRequestがrequestを表示している場所になっています。
ただこれだと毎回リストを走査しないといけなく効率が悪いです。
やりたいことはrequest履歴の追加、履歴の表示、1つ前後の履歴の移動になります。Haskellでよくある手法として Zipper を使うことにしました。

Zipperについてはこちらがよくまとめられています。

今回はこのようにしました。

Types.hs

data RequestedZipper a = RZ
  { _reqzAbove :: [a]
  , _reqzBelow :: [a]
  , _reqzCurrent :: Maybe a
  , _reqzCurPos :: Int
  , _reqzLength :: Int
  }
reqzLength :: RequestedZipper a -> Int
reqzCurPos :: RequestedZipper a -> Int
reqzCurrent :: RequestedZipper a -> Maybe a
initReqZipper :: RequestedZipper Requested
moveAboveReqz :: RequestedZipper a -> RequestedZipper a
moveBelowReqz :: RequestedZipper a -> RequestedZipper a

最後に

UIについては割愛します。見比べたところIOに関してはHaskellもGoも対して変わらず、実装しやすかったです。
純粋なところは他の言語とかけ離れてしまいますが、実装できたときは嬉しく見比べたときはHaskellのが分かりやすく感じます。

Goは面白いToolがたくさんあって色々参考になりますので
Haskellを学びたいけど題材がないという方はぜひチャレンジしてください。

5
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
5
1