ryokatsu.dev

メモ:Haskellで雑にAPIを実装する


ここ最近趣味でHaskellに触れていて、ある程度構文とか、思想は理解できた。しかしプロジェクトを作成する手順や、環境構築などの知見が一切なく、学びたくてChatGPTさんに少し頼りながら雑にAPIを作ってみた。

成果物

※フロントエンドはまだ未対応

Haskellの環境構築

基本的には、mod_poppoさんの以下の記事を最初に読んである程度理解できた。

今回は設定が楽ということもあり、Stackを採用した。Stackは、コマンドが分かりやすくとりあえず打てば、いい感じにbuildしてくれたり、開発環境を立ち上げつつ、エラーとかも丁寧に教えてくれる。(Cabalの方も今後試したい)

Stackにはpackage.ymlがありここにdependenciesを記述できる。今回APIを作る時に、画像の情報をリクエストしたら、いい感じに圧縮してくれるAPIとかを実装したいなと思って、ChatGPTに聴いてみたらJuicyPixelsというライブラリを紹介してくれたので、これを使うことにした。JuicyPixelsは、jpgや、pngなどの画像を読み込んで色々と変更できるライブラリだ。

そもそもAPIどう実装するか(servantの利用)

ググるとservantというAPIのフレームワークのようなものを使うと良さそうというのを発見して採用した。このservant本体と、servant-serverをStackのpackage.yml内のdependenciesに記載してインストールした。ちなみにこのservantは、他にも数々のライブラリを出していて、servant-jsというservantで定義したAPIを、フロントエンドから利用するためのコードとして自動生成してくれるようで型安全だと思うしちょっと気になる。(aspidaみたいなノリ?)あとauth系の認証を扱うサブモジュールっぽいのもあった。

API作成の知見もLaravelでしかないため良く分からなかった。ひとまずディレクトリ構成については、ChatGPTに聴いてみた。

haskell-app/

├── stack.yaml
├── haskell-app.cabal

├── src/
│   ├── API.hs             -- APIの型定義
│   ├── Server.hs          -- エンドポイントの実装
│   ├── Compression.hs     -- 画像圧縮関連の関数 (オプション)
│   └── ... (その他のモジュール)

├── app/
│   └── Main.hs            -- サーバーの起動部分

└── test/
    └── Spec.hs            -- テスト関連 (必要に応じて)

こんな構成を教えてもらいつつ公式ドキュメントとか参考のリポジトリなどをいくつかみるとまあ大きく間違っていなさそうなので、この構成で作った。(Spec.tsは作ってない)

ひとまずhelloとimageという2つのエンドポイントを定義するのは、以下のようにStack独自の:<|> という構文でつなぐということらしい

{-# LANGUAGE DataKinds #-}
{-# LANGUAGE TypeOperators #-}

module API where

import Servant
import Data.ByteString (ByteString)

-- なんかわからんがこういう書き方をしないとAPIを複数定義できないらしい :<|> でつなぐ
type MyAPI = "hello" :> Get '[PlainText] String
       :<|> "image" :> ReqBody '[OctetStream] ByteString
                    :> Post '[OctetStream] ByteString

myAPI :: Proxy MyAPI
myAPI = Proxy

圧縮しようと思ったけど

JuicyPixelsのモジュールを見ていくとCodec.Picture的なもので、圧縮できそうだなと思い色々試したが、型が合わないとかエンコードがうまく行かないみたいなエラーが頻繁してちょっと諦めた。代わりにとりあえず画像を送ってそのまま返す実装をした。

とりあえず以下のcurlで疎通確認。

curl -X POST -H "Content-Type: application/octet-stream" --data-binary "@/xxx/xxx/xxx/xxx.jpg" http://localhost:8080/image > response.jpg

とりあえず返すだけなので当然成功する。このあと画像全体をグレースケールに変換してから返すように関数を作った。

-- 画像を受け取ってグレースケールに変換して返すAPI
imageHandler :: ByteString -> Handler ByteString

imageHandler imgData =
    case decodeImage imgData of
        Left err -> throwError err500 { errorBody = fromStrict . B.pack $ "Failed to decode image: " ++  error }
        Right dynamicImg -> return . toStrict . encodePng . imageToGrayScale $ dynamicImg

-- 画像をグレースケールに変換する処理
imageToGrayScale :: DynamicImage -> Image Pixel8
imageToGrayScale img = pixelMap computeLuma (convertRGB8 img)
  where
    computeLuma (PixelRGB8 r g b) = round (fromIntegral r * 0.3 + fromIntegral g * 0.59 + fromIntegral b * 0.11)

case式でパターンマッチングをして成功と失敗を絞り込んで記述した。case式は本当に便利で、JavaScriptでも書きたい。。(そういえばTC39のどっかのStageにあったような・・・)imageToGrayScale関数は8ビットのグレースケール画像を返すような関数です。RGBの取得の計算だけChatGPTを使いました。こういう時は本当に便利!!!

これでstack runを実行した状態で再度curlを叩くと画像全体がグレースケールされた状態で返ってきた。

感想

色々学べて楽しかった!でもまだまだわかんないことだらけだからもう少し弄ってみようと思う。そもそもHaskellの構文は分かるけど、概念的なことでちょっと理解が及ばない箇所が目立つのでそのあたりは何かしらでお勉強したい。