Help us understand the problem. What is going on with this article?

Go製のToolをHaskellで実装する

初めに

最近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を学びたいけど題材がないという方はぜひチャレンジしてください。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした