scotty使ってみた

scottyはHaskellの軽量なWebフレームワーク。

yesodちょっとゴツそうな感じがして若干ためらいがあって、個人的にはサーバサイドはAPI呼び出しだけぐらいの使い方が理想なので、もっとお手軽なかんじののがよいなーというのでscottyというのを使ってみた。

scottyのリポジトリ
https://github.com/scotty-web/scotty

とりあえず上記のリポジトリwikiにリンクされてたscottyのチュートリアルがあったのでこれをやってみた。
http://adit.io/posts/2013-04-15-making-a-website-with-haskell.html

試してみた手元の環境はMacOSX10.10+ghc7.8.3+cabal1.20.0.3で、scottyのバージョンは0.9.0を使いました。

実際に動かして試してみたコードは以下に置いておいた。
https://github.com/minamijoyo/scotty-todo

まずはプロジェクトのディレクトリ作る。

$ mkdir scotty-todo
$ cd scotty-todo
$ cabal sandbox init

次にビルドの定義。scotty-todo.cabalを作って以下のようなかんじにした。

name:                scotty-todo
version:             0.0.1
synopsis:            todo app using scotty
homepage:            https://github.com/minamijoyo/scotty-todo
license:             MIT
author:              minamijoyo
maintainer:          minamijoyo@gmail.com
category:            Web
build-type:          Simple
cabal-version:       >=1.8

executable scotty-todo
  main-is:             Main.hs
  -- other-modules:
  build-depends:       base ==4.7.*
                     , wai
                     , warp
                     , http-types
                     , resourcet
                     , scotty
                     , text
                     , bytestring
                     , blaze-html
                     , persistent ==1.3.*
                     , persistent-template ==1.3.*
                     , persistent-sqlite ==1.3.*
                     , persistent-postgresql ==1.3.*
                     , monad-logger ==0.3.*
                     , heroku
                     , transformers
                     , wai-middleware-static
                     , wai-extra
                     , time

ghcのバージョンとかの依存関係の都合でチュートリアルのサンプルからbaseとmonad-loggerのバージョンを若干修正してます。

Main.hsを以下のようなかんじで作ってまずはHello Worldする。

{-# LANGUAGE OverloadedStrings #-}
import Web.Scotty

main = scotty 3000 $ do
  get "/" $ do
    html "Hello World!"

ビルドしてみる。

$ cabal update
$ cabal install

ちなみにpostgresqlが入ってないと以下のようなエラーが出た。

cabal: Error: some packages failed to install:
persistent-postgresql-1.3.1.3 depends on postgresql-libpq-0.9.0.1 which failed
to install.
postgresql-libpq-0.9.0.1 failed during the configure step. The exception was:
ExitFailure 1
postgresql-simple-0.4.8.0 depends on postgresql-libpq-0.9.0.1 which failed to
install.
scotty-todo-0.0.1 depends on postgresql-libpq-0.9.0.1 which failed to install.

なんでsqlitepostgresql両方使ってんのとか思ったら、なんかチュートリアルの後半でHerokuにデプロイしてるところでpostgresqlを使ってるっぽい。なので手元の環境にもpostgresqlをインストールしておく。既に入ってる人は気にしないで下さい。

$ brew install postgresql
$ cabal install

で、ビルドできたら、以下のコマンドでWebサーバを起動する。

$ .cabal-sandbox/bin/scotty-todo

ブラウザでhttp://localhost:3000にアクセスしてHello World!が見えればOK。
とりあえずここまででscotty動かすまでできた。

scotty自体の使い方はチュートリアルの説明読んでもらった方が早いのだけど、チュートリアルの説明で若干捕捉。scottyのバージョンの違いからかヘッダーの取得でreqHeaderという関数がなかった。

get "/agent" $ do
  agent <- reqHeader "User-Agent"
  text agent

調べたところheaderという関数があったのでheaderを使えばよいっぽい。型はこんなかんじ。

header :: Text -> ActionM (Maybe Text)

微妙にMaybeになってるので

{-# LANGUAGE OverloadedStrings #-}
import Web.Scotty

main :: IO ()
main = scotty 3000 $ do
  get "/agent" $ do
    agent <- header "User-Agent"
    case agent of
      Just ua -> text ua
      Nothing -> text "no User-Agent header"

としないと動かない。

また、ヘッダーの設定で、headerという関数じゃなくて

get "/adit" $ do
  status status302
  header "Location" "http://www.adit.io"

setHeaderという関数という関数になってるので、こんなかんじにしないと動かない。

{-# LANGUAGE OverloadedStrings #-}
import Web.Scotty
import Network.HTTP.Types

main :: IO ()
main = scotty 3000 $ do
  get "/adit" $ do
    status status302
    setHeader "Location" "http://www.adit.io"

その他Persist周りで
Database.Persist.GenericSqlがないのでDatabase.Persist.Sqlを使う。

import Database.Persist.Sql

persistがないのでpersistLowerCaseを使う

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
Post
  title String
  content Text
  createdAt UTCTime
  deriving Show
|]

で、runDbのところ

runDb :: SqlPersist (ResourceT IO) a -> IO a
runDb query = runResourceT . withSqliteConn "dev.sqlite3" . runSqlConn $ query

readPosts :: IO [Entity Post]
readPosts = (runDb $ selectList [] [LimitTo 10])

こんなかんじで書いてると、Monad.Loggerが0.3.0からMonadLogger IOのインスタンスがなくなってるっぽくてこんなエラーが出る。

Main.hs:43:22:
    No instance for (MonadLogger IO) arising from a use of ‘selectList’
    In the second argument of ‘($)’, namely
      ‘selectList [] [LimitTo 10]’
    In the expression: (runDb $ selectList [] [LimitTo 10])
    In an equation for ‘readPosts’:
        readPosts = (runDb $ selectList [] [LimitTo 10])
cabal: Error: some packages failed to install:
scotty-todo-0.0.1 failed during the building phase. The exception was:
ExitFailure 1

ぐぐってみると、NoLoggingTで包むと型が合うよとの情報があり
database - No instance for Control.Monad.Logger.MonadLogger when attempting to use persistent - Stack Overflow

こんなかんじで試して見たら型があった。

runDb :: SqlPersist (ResourceT (NoLoggingT IO)) a -> IO a
runDb query = runNoLoggingT . runResourceT . withSqliteConn "dev.sqlite3" . runSqlConn $ query

readPosts :: IO [Entity Post]
readPosts = (runDb $ selectList [] [LimitTo 10])

なんかscottyの使い方の勉強のはずなのに半分ぐらいPersistentの使い方の勉強になってきたような。
Herokuへのデプロイは力尽きて諦めた。。。