2016年12月6日火曜日

[haskell][yesod] TypedContentを利用してクライアントが要求するフォーマットでレスポンスを返す

Yesod Advent Calendar 2016の6日目の記事です。

RESTfulなAPIを提供する場合、クライアントの都合にあわせて、フォーマットを変えてレスポンスを返したいケースがあります。サーバー上で管理しているDBから、表現だけをHTML, JSON, XML, CSVなどに変更して返すイメージです。例えば、人物情報(名前、年齢、性別など)の一覧を返す際には以下のようなデータが返されることになります。
  • HTML
  • <table border>
      <tr>
        <th>name</th>    <th>sex</th>    <th>age</th>
      </tr>
      <tr>
        <td>Taro Yamada</td>    <td>Male</td>    <td>18</td>
      </tr>
      <tr>
        <td>Hanako Yamada</td>    <td>Female</td>    <td>25</td>
      </tr>
      <tr>
        <td>Ichiro Suzuki</td>    <td>Male</td>    <td>43</td>
      </tr>
    </table>
    
  • JSON
  •  [
      {"name":"Taro Yamada", "sex":"Male", "age":18},
      {"name":"Hanako Yamada", "sex":"Female", "age":25},
      {"name":"Ichiro Suzuki", "sex":"Male", "age":43}
     ]
    
  • CSV
  •  Taro Yamada,Male,18
     Hanako Yamada,Female,25
     Ichiro Suzuki,Male,43
    

通常はフォーマット毎に別のURLを割り当てますが、Yesodでは1つのURLで複数フォーマットのレスポンスを返す実装が簡単にできます。
このエントリでは、http://localhost:3000/に対応する一つのハンドラの中でTypedContentを利用して複数のフォーマットを扱う方法を紹介します。クライアントが送信するリクエストに記載されるAcceptヘッダによって、サーバーの挙動を変えます。


準備:

まず、素のYesodのscaffolding siteを起動できる環境を作ります。stackが利用できる環境を前提にしています。
  1. yesod-simpleを指定して空の"typedcontent"プロジェクトを生成
  2. % stack new typedcontent yesod-simple
    
    yesod-simpleは最も単純なyesodテンプレートでDB関連ライブラリへの依存がありません。
  3. 生成したプロジェクトディレクトリに移動し、依存ツールをビルド
  4. % cd typedcontent
    % stack build yesod-bin cabal-install
  5. 生成したtypedcontentプロジェクトをビルド
  6. % stack build
  7. develサーバーを起動
  8. % stack exec -- yesod devel
    ブラウザからhttp://localhost:3000/を開いてdevelサーバーにアクセスできることを確認してください。


基礎1:Home.hsを修正してHTMLを返すシンプルなハンドラを作ってみる

  1. Handler/Home.hsに人物情報データ定義を追加
  2. yesod-simpleテンプレートではDBを利用しないプロジェクトが生成されます。プロジェクト内のHandlerディレクトリにHome.hsとCommon.hsが存在しているので、Home.hsをエディタで開いて、以下のコードを追加してください。
    -- サーバー上で管理するデータ定義。
    data Sex = Male | Female
        deriving (Show)
    data Person = Person
        { name :: Text
        , age  :: Int
        , sex  :: Sex
        }
        deriving (Show)
    
    -- サーバー上のサンプルデータ。3人分の情報を保持。
    samplePersonList :: [Person]
    samplePersonList = [ (Person "Taro Yamada" 18 Male)
                       , (Person "Hanako Yamada" 25 Female)
                       , (Person "Ichiro Suzuki" 41 Male) ]
    
    
  3. Handler/Home.hsのgetHomeRの定義を更新
  4. 既存のgetHomeRの実装を削除し、toTableHtmlで置き換えて下さい。
    -- HTML tableフォーマットでレスポンスを生成。
    toTableHtml :: Handler Html
    toTableHtml = withUrlRenderer [hamlet|
                 <table border>
                     <tr>                                                         
                       <th>name                                                   
                       <th>age                                                    
                       <th>sex                                                    
                   $forall person <- samplePersonList                             
                     <tr>                                                         
                       <td>#{name person}                                         
                       <td>#{age person}                                          
                       <td>#{show $ sex person}                            
                 |]
    
    getHomeR :: Handler Html
    getHomeR = toTableHtml
    
    
  5. 動作確認
  6. ブラウザでhttp://localhost:3000/にアクセスし、以下のようなテーブルが表示されていればOKです。

    curlコマンドを実行すると実際にサーバーから返されているHTMLフォーマットデータを確認することができます。
    % curl http://localhost:3000/
    


基礎2:クライアント要求に応じてHTMLとplain textのどちらかを返す

いよいよ、クライアントからの要求に応じて異なるフォーマットを返すよう、getHomeRを改変します。ここでは"text/html"が要求されている場合にはHTMLを、"text/plain"が要求されている場合にはshow関数の実行結果を返すようにします。
  1. getHomeRの型をHandler HtmlからHandler TypedContentに変更し、"text/html"と"text/plain"に対応する
  2. getHomeRの実装をselectRep/provideRepを用いて以下のように変更します。
    getHomeR :: Handler TypedContent
    getHomeR = selectRep $ do
        provideRep $ toTableHtml
        provideRep $ return $ repPlain $ show samplePersonList
    
    selectRepはdoブロックの中でprovidRepによって提供される複数のフォーマットから、クライアントの要求に適合するものを選択します。provideRepの引数で渡されているHtml, RepPlainはHasContentTypeのインスタンスであり、hasContentTypeが実装されています。この関数によりmime typeが比較され適切なContentが選択されます。マッチするものがない場合にはクライアントには406 Not Acceptableが返されます。
  3. 動作確認
  4. curlコマンドで-HでAcceptヘッダを指定することで、HTMLとplain textの2種類の結果が得られることが確認できます。
    % curl -H "Accept: text/plain" http://localhost:3000/
    [Person {name = "Taro Yamada", age = 18, sex = Male},Person {name = "Hanako Yamada", age = 25, sex = Female},Person {name = "Ichiro Suzuki", age = 41, sex = Male}]
    % curl -H "Accept: text/html" http://localhost:3000/
    <table border><tr><th>name</th>
    <th>age</th>
    <th>sex</th>
    ...
    
    Yesodにはquery string parameterからAcceptヘッダを自動生成する便利機能が実装されています。 この仕組みを利用することで、Acceptヘッダを入力できないブラウザ上でも(URL入力だけで)動作を確認できます。
    % curl http://localhost:3000/?_accept=text/plain
    [Person {name = "Taro Yamada", age = 18, sex = Male},Person {name = "Hanako Yamada", age = 25, sex = Female},Person {name = "Ichiro Suzuki", age = 41, sex = Male}]
    
  5. provideRepをprovideRepTypeに置き換えてみる
  6. "text/plain"をprovideRepTypeを用いて実装すると以下のようになります。
    getHomeR :: Handler TypedContent
    getHomeR = selectRep $ do
        provideRep $ toTableHtml
    --    provideRep $ return $ repPlain $ show samplePersonList
        provideRepType "text/plain" (return $ show samplePersonList)
    
    provideRepTypeを利用することで、HasContentTypeのインスタンスを持っていないフォーマットを返すことができます。


応用:JSON, CSVフォーマットをサポートする

ここまでの手順を応用して、CSV及びJSONフォーマットを返すようにgetHomeRを拡張します。
  1. provideRepTypeを利用してCSVフォーマットを返す実装を追加
  2. -- CSVフォーマットのレスポンスを生成する。
    class ToCSV a where
      toCsv :: a -> Text
    instance ToCSV Person where
      toCsv p = (name p) 
               ++ ("," :: Text) 
               ++ (pack $ show $ age p) 
               ++ ("," :: Text) 
               ++ (pack $ show $ sex p) 
               ++ ("\n" :: Text)
    instance (ToCSV a) => ToCSV [a] where
      toCsv [] = ""
      toCsv (x:xs) = (toCsv x) ++ (toCsv xs)
    
    getHomeR :: Handler TypedContent
    getHomeR = selectRep $ do
        provideRep $ toTableHtml
        provideRep $ return $ repPlain $ show samplePersonList
        provideRepType "text/csv" (return $ toCsv samplePersonList) -- 追加!
    
    
  3. 動作確認
  4. % curl -H "Accept: text/csv" http://localhost:3000/ 
    Taro Yamada,18,Male
    Hanako Yamada,25,Female
    Ichiro Suzuki,41,Male
    
    
  5. ついでにJSONもサポート
  6. {-# LANGUAGE DeriveGeneric #-}
    
    ...
    -- toJSONを自動導出。DeriveGeneric言語拡張が必要。
    instance ToJSON Sex 
    instance ToJSON Person 
    
    getHomeR :: Handler TypedContent
    getHomeR = selectRep $ do
        provideRep $ toTableHtml
        provideRep $ return $ repPlain $ show samplePersonList
        provideRepType "text/csv" (return $ toCsv samplePersonList) 
        provideJson $ samplePersonList -- 追加!
    
    
  7. 動作確認
  8. % curl -H "Accept: application/json" http://localhost:3000/ 
    [{"age":18,"name":"Taro Yamada","sex":"Male"},{"age":25,"name":"Hanako Yamada","sex":"Female"},{"age":41,"name":"Ichiro Suzuki","sex":"Male"}]
    


まとめ:

TypedContentを利用して一つのURLからHTML, PlainText, CSV, JSONフォーマットのデータを返す方法を説明しました。ハンドラを実装する上で、selectRep/provideRep/provideRepTypeをどう使えばよいか、YesodフレームワークがContentTypeの判定にHasContentTypeを用いている、といったキモになる情報をまとめています。
このエントリで利用したコードは以下のgithubリポジトリにコミットしてあるので、参考にしてください。
https://github.com/kurokawh/work_haskell/tree/master/yesodweb/typedcontent


参考:

2016年11月30日水曜日

[emacs] emacs上のgrep関連機能、複数ファイルの一括置換手順のまとめ

emacs上でgrep関連の機能を利用する際のTIPSをまとめておきます(よく忘れて調べ直すので…)。

grepコマンドのオプション

  • 指定ディレクトリ以下のファイルを再帰的に検索
    • -R DIR    シンボリックリンクを辿る
    • -r  DIR    シンボリックリンクは辿らない
  • 検索対象をファイル名でフィルタする
    • --include=GLOB    GLOBにファイル名を指定する。"*.cpp"のようにワイルドカード(*,?,[...])を指定可能
  • 検索対象から指定ファイル・ディレクトリを除外する
    • --exclude=GLOB   GLOBに除外するファイルのファイル名を指定する(ワイルドカード指定可能)。
    • --exclude-dir=DIR DIRに指定されたディレクトリをスキップ

  • 使用例:
    • docディレクトリ以下の.txtを拡張子に持つ全てのファイルを対象に"Test"という文字列を検索
    • % grep Test -R doc --include="*.txt"
    • includeディレクトリとsrcディレクトリ以下で拡張子が.hもしくは.cppのファイルを対象に"typedef"という文字列を検索(同じオプションを複数回指定することができる)
    • % grep typedef -R src -R include --include="*.h" --include="*.cpp"

*grep*バッファのキーバインディング

  • M-n / TAB
    • (compilation-next-error)次のマッチ箇所へ移動する。
  • n
    • (next-error-no-select)次のマッチ箇所へ移動する。かつ、別バッファに該当ファイルのマッチ箇所が表示される
  • } / M-}
    • (compilation-next-file)同じファイルのマッチ箇所を飛ばして、次のファイルのマッチ箇所へ移動する。
  • M-p / S-TAB
    • (compilation-previous-error)手前のマッチ箇所へ移動する。
  • p
    • (previous-error-no-select)手前のマッチ箇所へ移動する。かつ、別バッファに該当ファイルのマッチ箇所が表示される
  • { / M-{
    • (compilation-previous-file)同じファイルのマッチ箇所を飛ばして、手前のファイルのマッチ箇所へ移動する。
  • g
    • (recompile)再検索
  • q
    • (quit-window)*grep*バッファを閉じる
  • <
    • (beginning-of-buffer)先頭へ移動
  • >
    • (end-of-buffer)末尾へ移動
  • h / ?
    • (describe-mode)ヘルプ。キーバインド一覧が表示される。

wgrepによる一括置換の手順

  1. 置換元文字列を含むファイルをM-x grepで検索
  2. *grep*バッファでC-c C-p(wgrep-change-to-wgrep-mode)を実行
  3. *grep*バッファ内で必要な編集を加える(replace-stringを実行するなど)
  4. C-x C-s(wgrep-finish-edit)で編集を終了
    • この操作を行っても編集した内容はファイルには書き込まれていない(emacs上で開かれた各ファイルのバッファに未保存の状態で変更が反映されるだけ)。
    • ※編集をキャンセルする場合はC-c C-k (wgrep-abort-changes)
  5. M-x wgrep-save-all-buffersを実行し、更新したファイルバッファをディスク上に保存

参考

2016年9月24日土曜日

[cygwin] cygwin64セットアップメモ

自分向け備忘録。cygwin64環境をセットアップする際の手順をまとめておきます。

設定


環境変数

  • HOME
    • ホームディレクトリを設定
  • SHELL
    • "/bin/tcsh"とすることでデフォルトのシェルが切り替わる
      • ※/etc/passwdの編集では切り替わらないので注意

インストールするパッケージ

  • Archive
    • bzip2
    • p7zip
    • unzip
    • zip
  • Database
    • mysql
    • postgresql-client
    • sqlite3
    • sqliteman
  • Devel
    • autoconf
    • automake
    • bashdb
    • binutils
    • ctags
    • dmalloc
    • doxygen
    • gcc
    • gdb
    • git
    • gitk
    • jlint
    • make
    • mercurial
    • ocaml
    • patch
    • patchutils
    • subversion
  • Doc
    • cygwin-doc (libc関連man page)
  • Editors
    • emacs
    • emacs-X11
    • vim
  • Libs
    • libiconv
    • libncurses-devel
  • Net
    • curl
    • inetutils  (telnet)
    • nc
    • nc6
    • openssh
    • openssl
    • openssl-devel
  • Web
    • ping
    • wget
    • wput
  • Ruby
    • ruby
    • ruby-sqlite3
  • Shells
    • fish
    • rxvt-unicode
    • tcsh
    • xterm
  • Text
    • enscript
    • jq(JSON processor)
  • Utils
  • X11
    • xhost
    • xmodmap
    • xinit
    • xorg-server

参考

2016年8月28日日曜日

[haskell][yesod] YesodにおけるRESTfulなJSON API実装チュートリアル

HaskellのwebフレームワークであるYesodにおいて、RESTful APIを実装する手順を紹介します。Haskell上のデータ構造をJSONテキストに変換する、逆に、JSONテキストをパースしてHaskell上のデータ構造を生成する、といった処理が非常に簡単に実現できます。加えて、コードを書かなくてもバックエンドのDBとのORマッピングが可能になっており、効率的に開発することができます。
ここで紹介しているコードはgithubにコミットしています。

準備:

  • json-sampleというプロジェクト名でYesodのscaffolding siteをセットアップする
    • 空のプロジェクト生成
      • % stack new json-sample yesod-sqlite --system-ghc
        
        "--system-ghc"は省略可能。インストール済みのghcを使うことを指示しています。
    • 依存ツールをビルド
      • % stack build yesod-bin cabal-install --no-install-ghc
        
        "--no-install-ghc"は省略可。ghcのインストールを抑制するオプションです。
    • scaffolding siteをビルド
      • % stack build
        
    • scaffolding siteの動作確認
      • % stack exec -- yesod devel
        
        ブラウザで http://localhost:3000/ にアクセスできることを確認

このチュートリアルで作るもの:シンプルな掲示板

  • 以下のREST APIを提供するシンプルな掲示板を作ってみます。
    • POST http://localhost:3000/posts
      • 記事を1件ポストする。
    • GET http://localhost:3000/posts
      • 投稿済みの記事一覧を返す。シンプルなGET。
    • GET http://localhost:3000/posts/sender
      • "sender"によって投稿された記事一覧を検索して返す。
  • バックエンドDBにはsqliteを利用し、postされた記事は以下のスキーマで生成された"post"テーブルに格納します。
    • CREATE TABLE "post"(
        "id" INTEGER PRIMARY KEY,
        "title" VARCHAR NOT NULL,
        "content" VARCHAR NOT NULL,
        "sender" VARCHAR NOT NULL);
      

POSTの実装:

  • DBのmodelの定義
    • mode/configファイルを開き、以下の記述を追加します。"Post"の直後に記載されている"json"がキモです。この記述によりToJson/FromJson関数が自動生成されます。
    • Post json
          title Text
          content Text
          sender Text
      
  • Handerの追加
    • 以下の通りyesod add-handlerコマンドを実行しHandlerを追加します。
    • % stack exec -- yesod add-handler
      Name of route (without trailing R): Posts
      Enter route pattern (ex: /entry/#EntryId): /posts
      Enter space-separated list of methods (ex: GET POST): GET POST
      
      後で実装するGETもあわせて追加しておきます。
  • handler/Post.hsのpostPostRを実装する
    • handler/Post.hsのpostPostR関数を以下のように修正します。関数の型がHandler HtmlからHandler ()に変更されている点に注意してください。
    • {--
      postPostsR :: Handler Html
      postPostsR = error "Not yet implemented: postPostsR"
      --}
      
      postPostsR :: Handler ()
      postPostsR = do
          post <- requireJsonBody :: Handler Post
          _    <- runDB $ insert post
          sendResponseStatus status201 ("CREATED" :: Text)
      
    • add-handlerのバグで発生するjson-sample.cabalファイルの不備を修正します。
    • library
          hs-source-dirs: ., app
          exposed-modules: Application
                           Foundation
                           Import
                           Import.NoFoundation
                           Model
                           Settings
                           Settings.StaticFiles
                           Handler.Common
                           Handler.Home
                           Handler.Comment
                           Handler.Posts
      
      上記の通り、json-sample.cabalファイルの"exposed-modules"にHandler.Postsを手動で追記します。
  • 動作確認
    • curlコマンドで記事をポストしてみましょう。
    • % curl -v -H "Accept: application/json" -H "Content-type: application/json" -X POST -d '{"title" : "this is a title.", "content" : "this is a content.", "sender" : "kuro"}' --noproxy "*" http://localhost:3000/posts
      *   Trying 127.0.0.1...
      * Connected to localhost (127.0.0.1) port 3000 (#0)
      > POST /posts HTTP/1.1
      > Host: localhost:3000
      > User-Agent: curl/7.47.0
      > Accept: application/json
      > Content-type: application/json
      > Content-Length: 83
      > 
      * upload completely sent off: 83 out of 83 bytes
      < HTTP/1.1 201 Created
      < Transfer-Encoding: chunked
      < Date: Sun, 28 Aug 2016 07:28:15 GMT
      < Server: Warp/3.2.8
      < Content-Type: text/plain; charset=utf-8
      < Set-Cookie: _SESSION=O2DB2N/gbqZCdpbwyHihgoyK0Zfcj77lkv7J619gaHi8YZliO58oqpvWHIXKeGYZxZcDZpiVF1MJxWzoSaza0+pB5OrEMoG59xLuayySrnI2gUNMrGn+zRfeLkIUDEcCy7DjTNLaaYY=; Path=/; Expires=Sun, 28-Aug-2016 09:28:07 GMT; HttpOnly
      < Vary: Accept, Accept-Language
      < 
      * Connection #0 to host localhost left intact
      
      POSTに成功しHTTP/1.1 201 Createdが返されていればOKです。GETで記事を取得できるかどうかは次のフェーズで確認します。

シンプルなGETの実装:

  • getPostR関数に実装を与える。こちらもgetPostRの型をHandler HtmlからHandler Valueに変更しています。
    • {--
      getPostsR :: Handler Html
      getPostsR = error "Not yet implemented: getPostsR"
      --}
      
      getPostsR :: Handler Value
      getPostsR = do
          posts <- runDB $ selectList [] [] :: Handler [Entity Post]
          return $ object ["posts" .= posts]
      
      
      "selectList [] []"により、persistentの機能を利用して、postテーブル内のすべての行を取得しています。
  • 動作確認
    • 以下のcurlコマンドでGETの動作を確認してみます
    • % curl -v -H "Accept: application/json" --noproxy "*" http://localhost:3000/posts
      *   Trying 127.0.0.1...
      * Connected to localhost (127.0.0.1) port 3000 (#0)
      > GET /posts HTTP/1.1
      > Host: localhost:3000
      > User-Agent: curl/7.47.0
      > Accept: application/json
      > 
      < HTTP/1.1 200 OK
      < Transfer-Encoding: chunked
      < Date: Sun, 28 Aug 2016 07:37:02 GMT
      < Server: Warp/3.2.8
      < Content-Type: application/json; charset=utf-8
      < Set-Cookie: _SESSION=8cq2RzyFQ4GmsHEgbAltpOFpOgys9zSm+xaE1LWFfv1WvgGpyKhmAkfRNRjqQf/clKN1y5BDgI36KedcIJWlIBFSz2teM8QSqMUon1BeLjzz8SOAT1Kdgi0JS5hfdlgu0TMMtHYXwIk=; Path=/; Expires=Sun, 28-Aug-2016 09:37:02 GMT; HttpOnly
      < Vary: Accept, Accept-Language
      < 
      * Connection #0 to host localhost left intact
      {"posts":[{"sender":"kuro","content":"this is a content.","id":1,"title":"this is a title."}]}
      
    • レスポンスボディのJSONテキストを整形すると、以下のようになっています。
    • % curl -H "Accept: application/json" --noproxy "*" http://localhost:3000/posts | python -mjson.tool
      {
         "posts" : [
            {
               "sender" : "kuro",
               "id" : 1,
               "content" : "this is a content.",
               "title" : "this is a title."
            }
         ]
      }
      

フィルタ処理を伴うGET:

  • Filterハンドラを新たに追加
    • add-handlerで新たにFilter.hsを生成します。
    • % stack exec -- yesod add-handler
      Name of route (without trailing R): Filter
      Enter route pattern (ex: /entry/#EntryId): /posts/#Text
      Enter space-separated list of methods (ex: GET POST): GET
      
  • Filter.hsにgetFilterRを実装
    • getFilterRを以下のように変更します。先ほどのgetPostRとほぼ同じですが、selectListで検索条件としてsenderを指定している点だけが異なります。
    • {--
      getFilterR :: Text -> Handler Html
      getFilterR sender = error "Not yet implemented: getFilterR"
      --}
      
      getFilterR :: Text -> Handler Value
      getFilterR sender = do
          posts <- runDB $ selectList [PostSender ==. sender] [] :: Handler [Entity Post]
          return $ object ["posts" .= posts]
      
      persistentのクエリ機能の詳細については以前にまとめたブログエントリを参照を参照していただければ。
  • 動作確認
    • curlコマンドでsenderを指定して、一覧を取得してみましょう。正しく動いているようです。
    • % curl --noproxy "*" http://localhost:3000/posts/kuro
      {"posts":[{"sender":"kuro","content":"this is a content.","id":1,"title":"this is a title."},{"sender":"kuro","content":"this is a content.","id":2,"title":"this is a title."}]}
      
      % curl --noproxy "*" http://localhost:3000/posts/hoge
      {"posts":[{"sender":"hoge","content":"this is a content.","id":3,"title":"this is a title."}]}
      
      % curl --noproxy "*" http://localhost:3000/posts/xxx
      {"posts":[]}
      

参考:

2016年7月17日日曜日

[haskell][yesod] stack対応版Yesod tutorial

HaskellのwebサービスフレームワークにYesodというフレームワークがあります。Yesodに触れたことのない開発者向けに書かれたチュートリアルの一つにYesod tutorialがあり、手順に沿っていくだけで簡単なwebサービスを動作させることができ、Yesodで何ができるかを簡単に理解できるようになっています。
ただ残念なことに、このYesod tutorialの記載は内容が古く、stackを利用した現行の手順とマッチしなくなっています。stackに対応している最新環境(Yesod 1.4.x)における順があると役に立つと思い、書き起こしてみました。

  1. Before the real start(はじめに)
    1. Install(インストール手順)
    2. stackをインストールする。以下のサイトが参考になります。
    3. Initialize(初期化)
    4. オリジナルのチュートリアルではyesod initの実行する手順が記載されていますが、最新版のYesodでは以下のようにstack newを利用するよう指示されます。
      // original
      % yesod init
      yesod: The init command has been removed. Please use 'stack new' instead
      
      stack対応版では、以下のようにテンプレートを指定してプロジェクトを生成します。これでオリジナル版でプロジェクト名にyosogを、利用DBにsqliteを指定したのと同じ状態になっています。
      // stack support
      % stack new my-project yesod-sqlite
      
      次にyosogプロジェクトをビルドします。オリジナルの手順は以下のようになっています。
      // original
      % cd yosog
      % cabal sandbox init
      % cabal install --enable-tests . yesod-platform yesod-bin --max-backjumps=-1 --reorder-goals
      % yesod devel
      
      これに対応する、stack版での手順は以下になります。
      // stack support
      % cd my-project
      % stack build yesod-bin cabal-install --install-ghc
      % stack build
      % stack exec -- yesod devel
      
      あとはブラウザを起動して http://localhost:3000/ にアクセスすればscaffolding siteの画面が表示されます。ただ、環境によっては"getAddrInfo: does not exist"というエラーが表示され、scaffolding siteに繋がらない現象があるので、そのときにはこちらの情報を参考に対処してください。
    5. Configure git
    6. 「この作業は必須ではありませんが、gitを使うことはよい習慣です」だそうです
      % git init .
      % git add .
      % git commit -a -m "Initial yesod commit"
      
    7. A few words before we start
    8. my-project以下のディレクトリ構成の概要:
      config/routesURLとコードのマッピング設定ファイル
      Handler/URLにマッピングされたコード(ハンドラ)を格納
      templates/HTMLファイル、js, cssテンプレートファイルを格納
      config/modelsデータモデル(DBスキーマ)設定ファイル
  2. Echo
  3. さてEchoサーバーの実装です。最初の手順としてオリジナルサイトには以下のコマンド実行が記載されています。
    // original
    $ yesod add-handler
    
    stack対応版ではyesodコマンドを直に実行することはできません。かならず"stack exec"を介する必要があります。具体的には以下のコマンドを実行すればOKです。
    // stack support
    % stack exec -- yesod add-handler
    Name of route (without trailing R): Echo
    Enter route pattern (ex: /entry/#EntryId): /echo/#String
    Enter space-separated list of methods (ex: GET POST): GET
    
    上記手順でEchoハンドラを登録することができます。そして本来ならばこの状態で何も編集を加えなくてもビルドができるはずなのですが、以下のエラーが発生してしまいました・・・。
    % stack exec -- yesod devel
    Yesod devel server. Type 'quit' to quit
    Application can be accessed at:
    
    http://localhost:3000
    https://localhost:3443
    If you wish to test https capabilities, you should set the following variable:
      export APPROOT=https://localhost:3443
    
    Warning: The package list for 'hackage.haskell.org' is 146.2 days old.
    Run 'cabal update' to get the latest list of available packages.
    Resolving dependencies...
    Configuring my-project-0.0.0...
    ghc: unable to load package `my-project-0.0.0'
    ghc: C:\work_haskell\yesodweb\scaffolding\my-project\dist\build\HSmy-project-0.0.0-5AgQdK8FuSe8tlY0YoDpHN.o: unknown symbol `myprozu5AgQdK8FuSe8tlY0YoDpHN_HandlerziEcho_getEchoR_closure'
    
    確認したところ、add-hanlderコマンドによってmy-project.cabalに追加された、Handler.Echoの追加場所が正しくなく、リンクエラーになっている模様。
    --- a/yesodweb/scaffolding/my-project/my-project.cabal
    +++ b/yesodweb/scaffolding/my-project/my-project.cabal
    @@ -101,6 +101,7 @@ test-suite test
         other-modules:     Handler.CommentSpec
                            Handler.CommonSpec
                            Handler.HomeSpec
    +                       Handler.Echo
                            TestImport
         hs-source-dirs:    test
         ghc-options:       -Wall
    
    自動追加された上記の状態ではダメで、以下の場所に移動する必要がある。add-handlerの不具合により、.cabalファイル内の"library"欄に追加する項目が"test suite"欄に追加されてしまうのが原因です。
    --- a/yesodweb/scaffolding/my-project/my-project.cabal
    +++ b/yesodweb/scaffolding/my-project/my-project.cabal
    @@ -23,6 +23,7 @@ library
                          Handler.Common
                          Handler.Home
                          Handler.Comment
    +                     Handler.Echo
     
         if flag(dev) || flag(library-only)
             cpp-options:   -DDEVELOPMENT
    
    これでコンパイルが無事に通り、scafolding siteを起動できる状態になります。tutorialに沿ってブラウザから以下のURLにアクセスしてみます。
    現時点ではまだハンドラの実装が空のままなので、以下のエラーが返されるのが期待値になります。
    いよいよ、Echoハンドラの実装です。 エディタでHandler/Echo.hsを開いてみてください。以下のようになっているはずです(前述のNot yet implementedエラーはこのコードによるものです)。
    module Handler.Echo2 where
    
    import Import
    
    getEcho2R :: String -> Handler Html
    getEcho2R string = error "Not yet implemented: getEcho2R"
    
    Handler/Echo.hsを以下の実装に変えることでEchoが動作する状態になります。
    module Handler.Echo where
    
    import Import
    
    getEchoR :: String -> Handler Html
    getEchoR theText = defaultLayout [whamlet|<h1>#{theText}|]
    
    試しにブラウザから以下のURLにアクセスしてみましょう!
    以下の通り"foo"がエコーバックされれば成功です。

オリジナルのチュートリアルではEchoサーバーの実装後、以下の手順が案内されています。yesodコマンド実行時にstack execを適用することで問題なく進められると思います。
    2. Echo
    2.1. Bulletproof?
    2.2. Cleaning up
    2.2.1. Data.Text
    2.2.2. Use templates
    3. Mirror
    4. A Blog
一通りの手順を実装したコードをgithubの以下のサイトにコミットしています。必要に応じてこちらも参照してみてください。

環境:

  • The Glorious Glasgow Haskell Compilation System, version 7.10.3
  • yesod-bin version: 1.4.17.1

参考:


[haskell][yesod] stack exec -- yesod devel で devel.hs: getAddrInfo: does not existというエラーになる問題の対処方法

Widnwos環境での現象:

windows上でyesodのscafolding siteをセットアップし、さあ起動!ブラウザから接続確認してOKとなるはずが、なぜか「The application isn't built」という表示が出てしまいました。

このときターミナルには以下のようなログが出力されていました。
% stack exec -- yesod devel
Yesod devel server. Type 'quit' to quit
Application can be accessed at:

http://localhost:3000
https://localhost:3443
If you wish to test https capabilities, you should set the following variable:
  export APPROOT=https://localhost:3443

Warning: The package list for 'hackage.haskell.org' is 146.0 days old.
Run 'cabal update' to get the latest list of available packages.
Resolving dependencies...
Configuring my-project-0.0.0...
Rebuilding application... (using cabal)
Starting development server...
Starting devel application
Devel application launched: http://localhost:3000
devel.hs: getAddrInfo: does not exist (error 11001)
receiveloop: failed (No error)

ブラウザには「ビルドができてない!」と表示されていますが、ビルド自体は成功しています。以下のディレクトリにバイナリが生成されています。
% find . -name "*.exe"
./.stack-work/dist/2672c1f3/build/my-project/my-project.exe
./.stack-work/dist/2672c1f3/build/test/test.exe

PCによってはうまく動くこともあり原因を切り分けて調査したところ、HOST環境変数の有無で挙動が変わることがわかりました。ここから先は推測ですが、HOST環境変数に設定されている名前でIPアドレス解決を試みてエラーとなっているような気がします。
以下の手順のいずれかでHOST環境変数を空にして、scaffolding siteを再起動すると正常にアクセスできるようになります。
  • cmd.exe
  • set HOST=
    
  • tcsh
  • unsetenv HOST
    
  • bash
  • export HOST=
    

mac環境の現象:

macでも同様の問題が発生します。mac上のログは以下のようになります。やはりHOST環境変数を無効にすることでブラウザからアクセスできるようになりました。
% stack exec -- yesod devel
Yesod devel server. Type 'quit' to quit
Application can be accessed at:

http://localhost:3000
https://localhost:3443
If you wish to test https capabilities, you should set the following variable:
  export APPROOT=https://localhost:3443

Warning: The package list for 'hackage.haskell.org' is 42.1 days old.
Run 'cabal update' to get the latest list of available packages.
Resolving dependencies...
Configuring my-project-0.0.0...
Rebuilding application... (using cabal)
Starting development server...
Starting devel application
Devel application launched: http://localhost:3000
devel.hs: getAddrInfo: does not exist (nodename nor servname provided, or not known)

参考:

2016年7月12日火曜日

[haskell][yesod] stackのnewコマンドで指定できるyesod関連templateの説明

現状、stackで指定できるyesod関連のtemplatesには以下のものがあります。どのtemplateに何が用意されているのか、知りたかったのですがどこにも説明されていないようなので、調べてまとめてみました。
% stack templates | grep yesod
yesod-hello-world (←現時点では削除されています)
yesod-minimal
yesod-mongo
yesod-mysql
yesod-postgres
yesod-postgres-fay
yesod-simple
yesod-sqlite

以下、各テンプレートの説明です。後に出てくるテンプレートほど内容が複雑になっています。テンプレートを指定して新しいプロジェクトを生成する場合は以下のコマンドを実行します。
% stack new プロジェクト名 yesod-???

yesod-hello-world

  • 最もシンプルなテンプレート
  • app.hs内のコードで"/"に対応するHomeハンドラだけが登録されている。
  • configurationファイルなどは一切なし。
  • Home
    • "Hello World"を表示するだけ
(2016/12/03 追記:githubの情報によるとこのテンプレートは削除されたようです。)


yesod-minimal

  • 次にシンプルなテンプレート。
  • 以下のroutesファイルによって、Home, Addの2つのハンドラが登録されている。
    • routes
    • /              HomeR GET
      /add/#Int/#Int AddR  GET
      
  • Home, Addハンドラの実装は以下の通り。
  • Home
    • 5+7の通常(HTML形式)リンクと、JSON形式のリンクを表示
  • Add
    • 5+7の結果を出力(通常はHTML形式でレスポンスを返す)
      • accept=application/jsonの場合のみJSON形式でレスポンスを返す

yesod-simple

  • 各DB用のテンプレートのベースになるテンプレート。
  • HTML, javascript, CSSの動的生成、リンク切れ検知など、DB接続と認証機能を除いて一通りの機能を確認できる。
  • 以下のフォルダ構成が生成される
    • app
      • 通常起動、devel起動用のエントリ関数
      • 通常起動は、引数で設定ファイル(yaml)を指定可能。
        % yesod-simple config/settings.yml
        
        以下のコマンドでdevel起動。
        % stack exec -- yesod devel
        
    • config
      • routesファイル(ハンドラリスト)
      • /static StaticR Static appStatic
        
        /favicon.ico FaviconR GET
        /robots.txt RobotsR GET
        
        / HomeR GET POST
        
        /comments CommentR POST
        
        
      • 設定ファイル
    • static
      • 静的ファイル置き場。デフォルトではcss, fontが配置される。
    • templates
      • テンプレートファイル置き場。Haskellコードを埋め込むことができる。
      • *.hamlet: HTML
      • *.julius: javascript
      • *.lucius: CSS
    • test
      • テストコード置き場。
      • 以下のコマンドでテスト実行。
        % stack test
        
  • 以下はコードが格納されるディレクトリ
    • Handler
      • 以下のHome, Common, Commentハンドラが生成されている。
      • Home.hs
        • ホーム画面定義。
      • Common.hs
        • FaviconR(favicon.ico)、RobotsR(robots.txt)への参照を定義。
        • これらはFoundation.hsから参照される。
      • Comment.hs
        • DB接続がないため単にエラーを表示するだけ。
    • Import
      • Import宣言まとめ。
    • Settings
      • staticディレクトリ内のファイル参照を記載しておき、コンパイル時にリンク切れのチェックを行う。

yesod-mongo/mysql/postgres/sqlite

  • 各DB用のconnectionコードと認証機能が追加されたテンプレート。
    • それぞれ、MongoDB, MySQL, PostgreSQL, SQLiteと接続するためのコードが自動生成されます。
  • yesod-simpleとの違いは下記の通り
    • config
      • route
      • 下記の通り"/auth"が追加されています。
        /static StaticR Static appStatic
        /auth   AuthR   Auth   getAuth
        
        /favicon.ico FaviconR GET
        /robots.txt RobotsR GET
        
        / HomeR GET POST
        
        /comments CommentR POST
        
      • setting.yml, test-setting.yml
        • DB接続のためのパラメタ追加。
    • Handler
      • Comment.hs
        • クライアントから送信されたコメントをDBに格納・参照する処理が定義されている。
        • 認証済みの場合はユーザー情報も合わせて格納。

yesod-postgres-fay

  • PostgreSQL+Fayを利用するためのテンプレート環境。
  • 調査が追いついていないのですが、FayではHaskellの仕様のサブセットがサポートされていて、Haskellのコードをjavascriptにコンパイルしてくれるとこのこと。

参考:

2016年1月15日金曜日

[haskell][gcc][win] Windows版Haskell Platform付属のgccでC++11のコードをコンパイルする方法

Windows版のHaskell Platformにはmingwが同梱されておりgccが含まれています。現在自分のPCにはHaskell Platform 2014.2.0.0をインストールしているのですが、これに付属されているgccでC++11のコードをコンパイルしようとすると、以下のようなエラーになってしまいました。
% gcc -std=c++11 cpp11.cpp
cc1plus.exe: error: unrecognized command line option '-std=c++11'

-stdオプションで"c++11"を指定しても、認識してくれません。
本家のサイトによると、'-std=c++11'オプションはgcc 4.7でサポートされたようです。これに対し、Haskell Platform 2014.2.0.0に付属されているgccのバージョンを確認したところ、4.6であることがわかりました。
% gcc --version
gcc.exe (rubenvb-4.6.3) 4.6.3
Copyright (C) 2011 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

gccは4.6で、'-std=c++11'オプションは認識できないバージョンであるため、冒頭のエラーとなってしまいます。
ただ、gcc4.6でもC++11(相当)コードをコンパイルする方法はあります。gcc 4.3~4.6では'-std=c++0x'と指定すれば。以下のように実行すればコンパイルすることができます。
% gcc -std=c++0x cpp11.cpp


参考:

2016年1月6日水曜日

[sqlite] SQLiteのロック・トランザクション関連仕様の整理

SQLiteは共有ロック・排他ロックの仕組みを備えており、プロセス内の複数スレッド、だけでなく、プロセスをまたぐ状態でSQLが同時に発行されても適切に処理されます。
トランザクションに複数のモードが存在し、指定するモードに依って取得されるロックが変わります。さらにSQLの内容に依存してロック状態が遷移するため、複数プロセスがアクセスしたときの挙動を正確に把握できるよう、仕様を整理してみました。
(文中の「プロセス」は、正確には「プロセス、もしくはスレッド」を意味します。)

DBのロック状態の種類:

  • UNLOCKED
    • ロックされていない状態。誰も読み書きしていない。DBの初期状態。
  • SHARED
    • read可、write不可な状態。複数プロセスが同時にSHAREDロックを取得可能。複数プロセスが同時にreadできることを意味する。DBがこの状態にあるとき、他のプロセスからのwriteはもちろん不可。
  • RESERVED
    • RESERVEDロックは、プロセスが将来writeを行う予定であるが、現時点ではreadだけを行っている状態である。SHAREDロックは複数プロセスが同時に取得することができるが、RESERVEDロックを取得できるのは1プロセスに限定される。
    • PENDINGロックとの違いは、新たにSHAREDロックを取得できる点(RESERVEDロック中であれば、別プロセスが新たにSHAREDロックを取得できるが、PENDINGロック中は新たにSHAREDロックを取得することはできない)。
    • SQL実行におけるwrite処理がキャッシュ上で完結する場合は、このRESERVEDロック状態で実行される。
  • PENDING
    • PENDINGロックは、既存のSHAREDロックの解放を待ち、EXCLUSIVEロックを取得してwriteを実行しようとしている状態である。PENDINGロック状態では新たなSHAREDロックの取得は許可されない。ただし、既存のSHAREDロックはそれが解放されるまでは有効。
  • EXCLUSIVE
    • EXCLUSIVEロックはDBファイルへのwrite実行時に必要なロックである。あるDBファイルの中では、ただ一つのEXCLUSIVEロックだけが有効になることができる(このとき、その他の全てのロックは解放された状態になる)。
    • SQLiteは並列処理性能を最大化する目的で、EXCLUSIVEロックの取得時間を最小にするよう動作する。

参考: File Locking And Concurrency In SQLite Version 3


トランザクションの開始:

明示的にトランザクションを開始する、しないに関わらず、DBの更新ではかならずトランザクションが実行されます。
  • BEGIN TRANSACTIONなしでDBを更新するSQLを発行した場合には、内部で自動的にトランザクションが開始され、SQL実行後にコミットされる
  • BEGIN TRANSACTIONを明示的に実行した場合には、COMMITもしくはROLLBACKを実行するまでトランザクション状態が継続される

トランザクションのモード:

  • DEFERRED
    • デフォルトはこのモード。トランザクションを開始した後でも、実際にDBに対するアクセスが発生するまではどのロックも取得しない。最初のread処理でSHAREDロックを取得し、最初のwrite処理でRESERVEDロックを取得する。
    • あるプロセスがトランザクションを開始した後に、別のプロセスが新たにトランザクションを開始できる(BEGIN TRANSACTION実行後に、別のプロセスからのBEGIN TRANSACTIONが成功してしまう点には注意が必要)。
  • IMMEDIATE
    • BEGIN TRANSACTION実行時にRESERVEDロックを取得する。あるdatabase connectionにおいてIMMEDIATEトランザクションを開始した後は、他のどのdatabase connectionも、DBへのwrite、IMMEDIATE/EXCLUSIVEトランザクションの開始はできなくなる。ただし、readは可能。
  • EXCLUSIVE
    • BEGIN TRANSACTION実行時にEXCLUSIVEロックを取得する。EXCLUSIVEトランザクション開始後は、”read uncomitted” connectionを除く全てのdatabase connectionでreadが禁止される。また、EXCLUSIVEトランザクション、例外なく全てのdatabase connectionにおいてwriteが禁止される。



ロック状態とトランザクションの関係


各トランザクション開始時にどのロックが取得されて、そのロック状態がどう遷移するのかがわかりにくかったため、絵にまとめました。


以上、SQLiteの動作理解の助けになれば幸いです。