ラベル yesod の投稿を表示しています。 すべての投稿を表示
ラベル yesod の投稿を表示しています。 すべての投稿を表示

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年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にコンパイルしてくれるとこのこと。

参考:

2014年6月16日月曜日

[haskell][yesod] YesodでFacebookのOAuth認証とつなぐ手順

Yesodチュートリアルのサンプル(Yosog)には予め認証(auth)のコードが含まれており、/auth/loginでGoogleEmailとBrowserIdによる認証が可能になっています。今回、そのチュートリアルサンプルをFacebookのOAuth認証とつなげることができたのでその手順とコードを紹介しておきます。

Facebookの設定

FacebookのOAuth認証を利用するには、認証を利用するアプリケーションをFacebookのdevサイトに予め登録しておく必要があります。手順は以下の通りです。
  1. アプリを登録してApp IDとSecretを得る
    1. facebookの開発者サイトにログイン
    2. 「アプリ」-「 新しいアプリを作成する」で必要項目を入力
  2. ログイン後のページ遷移ができるようにサイトURLを設定しておく
    1. 新たに登録したアプリの設定ページで「基本データ」タブを開く
    2. [Add Platform]を選択し一覧から「ウェブサイト」を選択
    3. 表示された設定項目中の「サイトURL」に起動するweb appのURLを登録
      • ローカルで起動したサーバーを用いて動作確認する際いは以下のURLを設定しておく
      • http://localhost:3000/

チュートリアルのコードの変更

まずここを参考にチュートリアルサンプルが動作する環境を整えてください。http://localhost:3000/で"Hello"画面が出ていればOKです。http://localhost:3000/auth/loginにアクセスすると、GoogleEmail/BrowseIdによる認証画面が表示されます。この状態で以下の変更を加えるとFacebook認証を利用することができます。
  1. Foundation.hsの変更
    1. importに以下のモジュールを追加
    2. 以下の3行を追加します。
      
      import Yesod.Facebook (YesodFacebook(..))
      import Yesod.Auth.Facebook.ServerSide
      import Facebook (Credentials(..))
      
      
    3. authPluginsをFacebookに書き換える
    4. 既存のBrowserId, GoogleEmailの宣言をFacebookのものに書き換えます。
      
          authPlugins _ = [authBrowserId def, authGoogleEmail]
          authPlugins _ = [authFacebook ["email"]]
      
      
    5. Facebook.Credentialの追加
    6. 以下のコードを追加します。前述のFacebookの設定によって得られるApp IDとApp Secretをそれぞれ、Facebook.Credentialの第2、第3引数に指定します。
      
      instance YesodFacebook App where
          fbCredentials _ = Facebook.Credentials "Yesod FB Auth Sample" "012345678901234" "aaaaaaaabbbbbbbbccccccccdddddddd"
          fbHttpManager = httpManager
      
      
  2. Yosog.cabalに依存ライブラリを追加する
  3. Yosog.cabalをエディタで開き、build-dependsに以下の3つのライブラリを追加します。
    
                     , yesod-fb
                     , yesod-auth-fb
                     , fb
    
    
  4. HomeRの変更(省略可能)
  5. Handler/Home.hsのgetHomeRのコードを以下のように編集することで、http://localhost:3000/の挙動をAuthentication and AuthorizationAuthenticate Meサンプルと同じ挙動にすることができます。
    
    import           Yesod.Auth
    
    getHomeR :: Handler Html
    getHomeR = do
    
        maid <- lookupSession "_ID"
        defaultLayout
            [whamlet|
                <p>Your current auth ID: #{show maid}
                $maybe _ <- maid
                    <p>
                        <a href=@{AuthR LogoutR}>Logout
                $nothing
                    <p>
                        <a href=@{AuthR LoginR}>Go to the login page
            |]
    
    
これでFacebookのOAuth認証が利用可能になります。ブラウザを起動してhttp://localhost:3000/auth/loginにアクセスしてください。ブラウザ上に"Login with Facebook"というリンクが表示されるはずです。そのリンクをクリックするとFacebookのサイトにジャンプしてアプリケーションの認証が促されます。そこでOKを選択すると、ログイン状態に遷移します。
動作確認に用いたコードはgithubにアップしました。

参考にしたサイト:

2014年3月13日木曜日

[heroku][yesod] Heroku上でSQLiteを使ってはいけない

HerokuでSQLiteを使ってはいけない理由

なぜHerokuでSQLiteを使ってはいけないか。理由は以下の2つです。
  • HerokuのCedar Stackはephemeral filesystemを持っており、一見ファイルの読み書きができるように見えるが、一日に一度すべてクリアされる。このためSQLiteで保存したデータも毎日失われてしまう。
  • もしHerokuのファイルシステムが永続的にデータを保持したとしても別の問題がある。SQLiteはサービス機能を持っていないため各dynoの間でデータを共有することができず、それぞれ固有のDBを保持しているように振る舞ってしまう。
HerokuサイトのLearning - Application Architectureで公開されているSQLite on Herokuに詳細が説明されています。SQLiteの代わりにアドオンで提供されているPostgreSQL、MySQL、MongoDBなどを使うことになります。
今回YesodチュートリアルのYosogアプリで利用するデータベースをSQLiteからPostgreSQLに移行しました。その手順を紹介しておきます。

ローカル(mac)環境でのPostgres対応

まずローカル環境で動作しないことにはサーバ上で動作するはずがない、ということで、mac上のDevelopment環境でPostgreSQLを利用する手順を記載しておきます。
Yesodチュートリアルのサンプル(Yosogアプリ)をローカル環境でPostgreSQL上で動作させるには以下の修正が必要でした。yesod initコマンド実行時に利用DBとしてsqliteでなくpostgresqlを選択すれば、ここで述べるのSQLiteからPostgreSQLへの変更手順は不要になります。
  1. SQLite環境をPostgreSQL環境に変更
    1. Application.hsのdbconfをSQLiteからPostgreSQLに
    2. 
      -     dbconf <- withYamlEnvironment "config/sqlite.yml" (appEnv conf)
      +     dbconf <- withYamlEnvironment "config/postgresql.yml" (appEnv conf)
      
      
    3. Settings.hsのPersistentConfをSQLiteからPostgreSQLに
    4. (その1)
      
      -import Database.Persist.Sqlite (SqliteConf)
      +import Database.Persist.Postgresql (PostgresConf)
      
      
      (その2)
      
      -type PersistConf = SqliteConf
      +type PersistConf = PostgresConf
      
      
    5. Yosog.cabalのbuild-dependsをpersistent-sqliteからpersistent-postgresに
    6. (その1)
      
      -                 , persistent-sqlite             >= 1.3        && < 1.4
      +                 , persistent-postgresql         >= 1.3        && < 1.4
      
      
      (その2)
      
      -                 , persistent-sqlite
      +                 , persistent-postgresql
      
      
    7. config/postgresql.ymlの準備
    8. 
      Default: &defaults
        user: Yosog
        password: Yosog
        host: localhost
        port: 5432
        database: Yosog
        poolsize: 10
      
      Development:
        <<: *defaults
      
      Testing:
        database: Yosog_test
        <<: *defaults
      
      Staging:
        database: Yosog_staging
        poolsize: 100
        <<: *defaults
      
      Production:
        database: Yosog_production
        poolsize: 100
        <<: *defaults
      
      
  2. MacPortsでpostgresql93をインストールする
    1. postgresql93パッケージをインストール
    2. 
      % sudo port install postgresql93
      % sudo port install postgresql93-server
      % sudo port select postgresql postgresql93
      
      
    3. DBインスタンスの生成(serverのinstall時の指示に従う)
    4. 
      % sudo mkdir -p /opt/local/var/db/postgresql93/defaultdb
      % sudo chown postgres:postgres /opt/local/var/db/postgresql93/defaultdb
      % sudo su postgres -c '/opt/local/lib/postgresql93/bin/initdb -D /opt/local/var/db/postgresql93/defaultdb' 
      
      
    5. サーバーの起動
    6. 
      % sudo launchctl load -w /Library/LaunchDaemons/org.macports.postgresql93-server.plist
      % sudo launchctl start org.macports.postgresql93-server
      
      

  3. Yosogアプリが利用するDBを生成する
    1. ユーザーとデータベースの生成
    2. 自動生成されるconfig/postgres.ymlを確認するとYosogユーザーでYosogデータベースにアクセスする設定になっています。このため以下のコマンドでユーザーとデータベースを作成しておきます。
      
      % createuser -U postgres -P Yosog
      % createdb -U postgres Yosog
      
      
      ユーザーとデータベースが作成されていないと、Yosogアプリ起動時に以下のエラーになります。
      
      Yosog: SqlError {sqlState = "", sqlExecStatus = FatalError, sqlErrorMsg = "FATAL:  role \"Yosog\" does not exist\n", sqlErrorDetail = "", sqlErrorHint = ""}
      
      
    3. PostgreSQL対応したYosogアプリの起動
    4. 
      % cabal run -- Development
      
      

HerokuにPostgreSQL対応版Haskell/Yesodアプリをdeployする

PostgreSQLを利用する手順はRuby on Rails上であればHerokuのドキュメントで紹介されています。ここではHaskellのYesodフレームワーク上でPostgreSQLを利用するサーバーをHerokuにデプロイするための手順を示しておきます。
  1. postgresqlアドオンの登録&接続情報の確認
    1. postgresqlアドオン登録
    2. 
      %  heroku addons:add heroku-postgresql:dev
      
      
    3. DB情報の確認
    4. 
      % heroku pg:info
      === HEROKU_POSTGRESQL_IVORY_URL
      Plan:        Dev
      Status:      available
      Connections: 0
      PG Version:  9.3.3
      Created:     2014-03-12 15:42 UTC
      Data Size:   6.4 MB
      Tables:      0
      Rows:        0/10000 (In compliance)
      Fork/Follow: Unsupported
      Rollback:    Unsupported
      
      
    5. 接続情報の確認
    6. 
      % heroku pg:credentials HEROKU_POSTGRESQL_IVORY_URL
      Connection info string:
         "dbname=ddddd host=aaa-11-222-33-44.compute-1.amazonaws.com port=5432 user=uuuuu password=XXXXX sslmode=require"
      Connection URL:
          postgres://uuuuu:XXXXX@aaa-11-222-33-44.compute-1.amazonaws.com:5432/ddddd
      
      
  2. 接続情報の反映と接続確認
    1. config/postgresql.ymlの編集
    2. 接続情報で確認した、dbname, host, user, passwordをpostgresql.ymlのProduction:の設定に反映します。
      
      --- a/config/postgresql.yml
      +++ b/config/postgresql.yml
      @@ -19,6 +19,10 @@ Staging:
         <<: *defaults
       
       Production:
      -  database: Yosog_production
      +  user: uuuuu
      +  password: XXXXX
      +  host: aaa-11-222-33-44.compute-1.amazonaws.com
      +  port: 5432
      +  database: ddddd
         poolsize: 100
         << *defaults
      
      
    3. postgresql対応版サーバーのdeploy
    4. 
      % git commit -m "- use postgersql instead of sqlite." .
      % git push heroku master
      % heroku open
      
      
    5. psqlを用いた接続確認(おまけ)
    6. 以下のコマンドでPCからadonしたPostgreSQLのDBに接続することができます。postgresql.ymlのDevelopment:欄に同様の設定をすればアプリ本体はPC上、DBはサーバー上の組み合わせで動作確認することもできます。
      
      % psql -h aaa-11-222-33-44.compute-1.amazonaws.com -p 5432 -U uuuuu DDDDD
      ddddd> \d
                        List of relations
       Schema |      Name      |   Type   |     Owner      
      --------+----------------+----------+----------------
       public | article        | table    | uuuuu
       public | article_id_seq | sequence | uuuuu
       public | email          | table    | uuuuu
       public | email_id_seq   | sequence | uuuuu
       public | user           | table    | uuuuu
       public | user_id_seq    | sequence | uuuuu
      (6 rows)
      
      

ここまでの修正はGitHubにコミット済みです。
https://github.com/kurokawh/Yosog/tree/Yosog_PostgreSQL
メモ:ローカルリポジトリの変更をリモートのmaster以外の別ブランチにプッシュするには、以下のコマンドを実行する。

% git push origin master:Yosog_PostgreSQL
Counting objects: 24, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (18/18), done.
Writing objects: 100% (18/18), 2.02 KiB | 0 bytes/s, done.
Total 18 (delta 12), reused 0 (delta 0)
To https://kurokawh@github.com/kurokawh/Yosog.git
   be68e29..8a25b29  master -> Yosog_PostgreSQL
% git branch -r
  origin/Yosog_PostgreSQL
  origin/master


参考にした情報:

2014年3月3日月曜日

[heroku][yesod] YesodアプリをHeroku上にデプロイする手順

YesodアプリをHeroku上にデプロイできました!以前にローカル環境で確認したYesodチュートリアルのwebアプリケーションをHeroku上で動作させることができたので、その手順をまとめておきます。

前提環境:

  • Yesodチュートリアルのアプリケーション(Yesog)が動作している
  • gitがインストールされている
  • heroku toolbeltがインストールされている
  • 動作確認した環境の詳細
    • % ghc --version
      • The Glorious Glasgow Haskell Compilation System, version 7.6.3
    • % cabal --version
      • cabal-install version 1.18.0.2
      • using version 1.18.1.2 of the Cabal library 
    • % yesod version
      • yesod-bin version: 1.2.6
    • % heroku version
      • heroku-toolbelt/3.4.1 (x86_64-darwin10.8.0) ruby/1.9.3

デプロイ手順:

以下の手順を実行することでYosogアプリケーションをHeroku上にデプロイし、クライアントからアクセスできるようになります。
  1. Yosog.cabalをエディタで開き、以下の取り消し線部分の記述を削除する
  2. 
    
    executable         Yosog
    
        if flag(library-only)
            Buildable: False
    
        main-is:           main.hs
        hs-source-dirs:    app
        build-depends:     base
                         , Yosog
                         , yesod
    
    
    この記述があると、cabal runで以下のようなエラーが発生してしまいます。原因不明…。
    
    cabal: Cannot build the executable 'Yosog' because the component is marked as
    
    disabled in the .cabal file.
    
    
  3. YosogルートディレクトリにProfileを用意する
  4. 
    % echo "web: cabal run -- production -p $PORT" > Profile
    
    
    このファイルはHerokuがサーバーを起動する手順になります。ローカル環境でも同様のコマンドでサーバーが起動することを確認しておきましょう。
  5. gitリポジトリにファイルを登録
  6. 
    % git init .
    % git add *
    % git commit -m "Initial commit." .
    
    
  7. heroku上にアプリケーションを登録
  8. 
    % heroku create --stack=cedar --buildpack https://github.com/begriffs/heroku-buildpack-ghc.git
    
    
  9. heroku上にアプリケーションをデプロイ
  10. 
    % git push heroku master
    
    
    10分くらい時間がかかります。状況によっては15分のビルド時間制限をオーバーしてしまうこともあるようです。そのようなときには、以下の手順で別サーバーでビルドを実行することができる模様。
    
    % heroku plugins:install https://github.com/ddollar/heroku-anvil
    % heroku build -r -b https://github.com/begriffs/heroku-buildpack-ghc.git
    
以上です。
heroku open
上記のコマンドでパブリックなサーバーとして起動しているYosogアプリケーションにアクセスすることができます。GitHubでこの環境をを公開しておきます。

  • https://github.com/kurokawh/Yosog.git
  • 参考情報:

    以下、参考にした情報についてまとめておきます。複数サイトの情報を参照しつつ作業したのですが、自分の環境ではうまくいかなかった手順もありました。最終的に最も成功に近かった手順は以下のサイトの情報になります。
    以下のサイトの情報も試しましたが、いずれもクライアントからアクセスできる状態にたどりつけませんでした。
    • Haskell Buildpack Demo
      • サンプルのデプロイ、動作確認は成功。だが、自環境のYosogアプリについては、deployまで成功したが、heroku openを実行してもサーバーとは繋がらず…。
    • MacでHerokuにYesodを(Herokuに公開編)
      • この手順はバイナリをgitコミットして、コンパイルなしてサーバー上で起動する手順だと思われます。mac上でビルドしたバイナリはdeployまではできるものの、heroku openで繋がりませんでした。

    その他、新たに学習した情報のメモ

    いろいろはまったことで、自分にとっての新しい知識が少しだけ増えました。Herokuへのデプロイと直接は関係しませんが、それらもまとめておきます。
    • yesodアプリをリリースビルドする手順
      • scafoldsiteではyesod develでビルド&テストサーバーが起動されますが、リリース用のバイナリを生成するには
        • cabal install
      • を実行することになります。このコマンドを実行すると
        • dist/dist-sandbox-5ba8a016/build/Yosog/Yosog
      • が生成されます。production -p XXXという引数とともにこのバイナリを起動すると、プロダクション版のサーバーを起動できます。
        • dist/dist-sandbox-5ba8a016/build/Yosog/Yosog production -p 3000
    おわり。

    2014年1月13日月曜日

    [haskell][yesod] Hashkell and Yesod(Basics章)学習メモ

    Yesod学習のためにオンラインで公開されているHaskell and Yesodを読んでいます。本エントリにはBasicsの章について自分の理解をまとめておきます。


    Basics
    • Hello World
      • サンプルコード(helloworld.hs)を起動するにはコマンドラインから単に以下のコマンドを実行すればよい。
        • runhaskell helloworld.hs
        • {-# LANGUAGE QuasiQuotes           #-}
          {-# LANGUAGE TemplateHaskell       #-}
          {-# LANGUAGE TypeFamilies          #-}
          import           Yesod
          
          data HelloWorld = HelloWorld
          
          mkYesod "HelloWorld" [parseRoutes|
          / HomeR GET
          |]
          
          instance Yesod HelloWorld
          
          getHomeR :: Handler Html
          getHomeR = defaultLayout [whamlet|Hello World!|]
          
          main :: IO ()
          main = warp 3000 HelloWorld
      • コンパイル後ウェブサーバーが起動される。起動されたウェブサーバーにアクセスするには、ブラウザに以下のURLを入力する。
    • Routing
      • 以下のようなコードでrouteを定義する。
      • mkYesod "HelloWorld" [parseRoutes|
        / HomeR GET
        |]
      • 上記のコードは、次のことを意味している。
        • "HelloWorld"アプリケーションはrouteを1つ持つ
        • このrouteを"HomeR"と呼ぶ。
          • "R"はresourceを表すものに付与する接尾辞。従うべきconvention。
        • このruoteは"/"(アプリケーションのroot)に対する、GETリクエストに応答する
        • mkYesodはTemplate Haskell関数、parseRoutesはquasi-quote)
    • Handler Function
      • 上記のHomeRというrouteに対するGETリクエストにレスポンスを返すにはgetHomeRというHandler Functionを用意する。
        • "get"はrouteの"GET"に対応
        • HomeRはrouteの名前
      • helloworldサンプルではdoc type, tagなどを自動的に付与してレスポンスデータを生成してくれるdefaultLayout関数に[whamlet|Hello World!|]という引数を渡している。
        • whamletはhamlet syntaxをwidgetに変換するquqsi-quote
        • hamletはYesodのでフォルトHTMLテンプレートエンジン
    • The Foundation
      • 全てのYesodアプリケーションはfoundationデータ型を持つ。 このデータ型はYesod型クラスのインスタンスでなければならない。以下のような項目を制御する。
        • DBのコネクションプール
        • コンフィグファイルから読み込んだ設定値
        • HTTPコネクションマネージャ
        • rumdom number generator
      • Yesodはヘブライ語で"foundation"という意味らしい。
    • Running
      • YesodはWeb Application Interface(WAI)上でビルドされており、以下の環境で動作する
        • FastCGI
        • SCGI
        • Warp
        • Webkitライブラリを利用するデスクトップアプリケーション
    • Resources and type-safe URLs
      • 複数のページ(HomeR, Page1R, Page2R)を遷移するサンプル:
      • {-# LANGUAGE QuasiQuotes           #-}
        {-# LANGUAGE TemplateHaskell       #-}
        {-# LANGUAGE TypeFamilies          #-}
        import           Yesod
        
        data HelloWorld = HelloWorld
        
        mkYesod "HelloWorld" [parseRoutes|
        / HomeR GET
        |]
        
        instance Yesod HelloWorld
        
        getHomeR :: Handler Html
        getHomeR = defaultLayout [whamlet|Hello World!|]
        
        main :: IO ()
        main = warp 3000 HelloWorldI
      • 上記のサンプルではwhamletでtype-safe URLを用いている。"@{...}"で囲まれた部分はYesodが自動的に正しいURL文字列に変換した上でクライアントにレスポンスデータとして返される。
      • type-safe URLは非常に価値があるもの:
        • 開発時に何度URLを変更しても決してリンク切れになることはない 
        • コンパイル時にパラメタ込みで不整合が検知される

    • The scaffolded site
      • 本書ではscaffold site関連のツールは意図的に用いていないが、実際の開発ではscaffold siteを利用することを強く推奨する。
      • yesod init
        • いくつかの質問を入力すると、デフォルトのscaffold siteが格納されたディレクトリが生成される
      • cabal install --only-dependencies
        • yesod initで生成されたディレクトリの中で実行
        • DBなどの付加的な依存ライブラリをビルドする
      • yesod devel
        • サーバーを起動する
    • Development Server
      • yesod develを利用すると、
        • コードの変更に対して自動的にリビルド・リロードがかかり、インタープリタ言語の強みであるrapid prototypingに相当する恩恵がえられる。
        • production site用には最適化されたバイナリを生成でき、fast productionも実現できる



    2014年1月12日日曜日

    [haskell][yesod] Hashkell and Yesod(Introduction & Haskell章)学習メモ

    Yesodのチュートリアルを一通りなぞったものの、いざ自分でweb appを作ろうとするといろいろとわからないことが多いため、Haskell and Yesodを最初から読むことにしました。
    本エントリで主に自分が重要だと感じたポイントをメモにして残しておきます。

    • Introduction
      • YesodおよびHaskellを用いるメリットは以下の通り。
      • Type Safety
        • 入力として期待している型を記述できる
        • データのマーシャリング機能で境界値問題を抑制できる
      • Concise
        • formsライブラリでコーディング量を削減できる
        • routesで型安全性を損なうことなく簡単な宣言ができる
        • DBへのデータの登録・取り出しコードを自動生成してくれる 
      • Performance
        • コンパイル時にHTML, CSS, JavaScriptを解析することで、サーバー実行時のdisk I/Oを削減してくれる
        • Haskell製の最速のウェブサーバーであるWarpを利用しているので高速
    • Haskell
      • Haskell全てに精通していなくてもYesodでアプリケーションは開発できるが、以下の項目は押さえておく必要がある
      • Terminology
        • Data type
          • type GearCount = Int
          • newtype Make = Make Text
          • data Vehicle = Bicycle GearCount | Car Make Model
        • Data constructor
          • PersonMakeBicycleCar
        • Type constructor
          • PersonMakeVehicle
        • Type variables
          • data Maybe a = Just a | Nothing
          • data Person = Person Text Int
      • Language Pragmas
        • Yesodでは強力なtype class, syntax changesなどの言語拡張機能を利用しているがその宣言はソースコードで行うことを強く推奨する
          • {-# LANGUAGE MyLanguageExtension #-}
        • GHCの-XMyLanguageExtensionオプション、cabalのextensionブロックの指定は環境依存になってしまうので使わない方がよい
      • Overloaded Strings
        • HaskellStringには以下の制限があるため、HaskellはOverloadedStringsという仕組みを用意している。
          • メモリアロケーションが各consに必要、要素のcharacter毎にmachine wordを消費するなどのパフォーマンスの問題
          • ByteStringsやHTMLなど、文字列っぽいデータを扱いたいことがある型
        • IsString型のインスタンスにはTextStringより効率がよい)、StringおよびHtmlなどがある。
        • ただし"hello"のように宣言されたデータの型が一意に決まらなくなるため、明示的な型指定が必要になることがある点に注意が必要。
      • Type Families
        • 複数の異なる型の関連を記述する。
      • Template Hashkell(TH)
        • THはコード生成の一つの手順で、Abstract Syntax Tree(AST)を構築する。GHCは、以下のような"$("で始まる関数をTemplate Haskell(TH)関数と認識する。
          • $(hamletFile "myfile.hamlet")
      • QuasiQuotes
        • QuasiQuotes(QQ)はTemplate Haskell(TH)のちょっとした拡張。以下のように"["と最初の"|"の間にquasi-quoteの名前を記述し、contentsを"|"の間に記述する。
        • {-# LANGUAGE QuasiQuotes #-}
          [hamlet|This is quasi-quoted Hamlet.|]
        • Haskell and Yesodのサンプルではコピペ(動作確認)を容易にするためにQQを多様しているが、実際の開発においてはhamletFileなどを用いて外部ファイルを参照することを奨める。

    Basicsの章以降は別エントリで。

    2013年12月21日土曜日

    [haskell][yesod] Yesodのインストールとチュートリアルの実行手順のまとめ

    HaskellのウェブアプリケーションフレームワークであるYesodに触れてみました。Yesodのチュートリアルを参考にして、自分のmac上にYesodをインストールし、サンプルサーバーをデプロイしたときの手順と、あとで調べたいと思った疑問点のメモを残しておきます。haskell platformがインストールされている環境を前提にしています。

    Yesodのセットアップ手順:

    1. cabalでyesod関連のパッケージをインストール
      • コマンドラインから以下のコマンドを実行。20分くらい?かかります。
        • cabal install yesod-platform yesod-bin cabal-dev
    2. プロジェクトの生成
      • 以下のコマンドを実行
        • yesod init
          • プロジェクト名に"Yosog"を入力
          • DBは's' (sqlite) を選択
          
          % yesod init
          Welcome to the Yesod scaffolder.
          I'm going to be creating a skeleton Yesod project for you.
          
          What do you want to call your project? We'll use this for the cabal name.
          
          Project name: Yosog
          Yesod uses Persistent for its (you guessed it) persistence layer.
          This tool will build in either SQLite or PostgreSQL or MongoDB support for you.
          We recommend starting with SQLite: it has no dependencies.
          
              s      = sqlite
              p      = postgresql
              pf     = postgresql + Fay (experimental)
              mongo  = mongodb
              mysql  = MySQL
              simple = no database, no auth
              url    = Let me specify URL containing a site (advanced)
          
          So, what'll it be? s
          That's it! I'm creating your files now...
          
          ---------------------------------------
          
                               ___
                                      {-)   |\
                                 [m,].-"-.   /
                [][__][__]         \(/\__/\)/
                [__][__][__][__]~~~~  |  |
                [][__][__][__][__][] /   |
                [__][__][__][__][__]| /| |
                [][__][__][__][__][]| || |  ~~~~
            ejm [__][__][__][__][__]__,__,  \__/
          
          
          ---------------------------------------
          
          The foundation for your web application has been built.
          
          
          There are a lot of resources to help you use Yesod.
          Start with the book: http://www.yesodweb.com/book
          Take part in the community: http://yesodweb.com/page/community
          
          
          Start your project:
          
             cd Yosog && cabal install && yesod devel
          
          or if you use cabal-dev:
          
             cd Yosog && cabal-dev install && yesod --dev devel
          
          
          
        • 生成されたYesogディレクトリに移動
          • cd Yosog
      • サーバーのテンプレートをセットアップ、ビルドして起動?こちらは30分以上かかりました。
        • cabal-dev install
        • yesod --dev devel
    3. サーバーへの接続確認
      • ブラウザで以下のURLにアクセス
        • http://localhost:3000/

    これでサンプルサーバーが起動され、ブラウザからのアクセスに対して上記のようなレスポンスを返してくれる状態になりました。さらに、自前のハンドラを登録してエコーバックするweb serviceを動作させるには以下の作業を行います。

    エコーバックハンドラの登録:

    1. ハンドラの追加
      • 以下のコマンドで空のハンドラが生成されます。
        • yesod add-handler
        
        % 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
        
        
        • Name of routeに"Echo"を指定
        • route patternに"/echo/#String"を指定
        • list of methodsに"GET"を指定
    2. echoハンドラの実装
      • add-handlerで生成されたHandler/Echo.hsを以下のように編集
        
        
        module Handler.Echo where
        
        import Import
        
        getEchoR :: String -> Handler Html
        getEchoR theText = defaultLayout [whamlet|<h1>#{theText}|]
        
        
    3. サーバーを起動
      • 以下のコマンドを実行する。このコマンドでビルドもされます。
        • yesod --dev devel
    4. ブラウザからechoサーバーのuriをたたく。
      • 以下のURLを指定してブラウザを開きます。
        • http://localhost:3000/echo/Hello%20Echo%20Server

    これで上記のような画面が表示されます。

    チュートリアルを一通りなぞった後の感想

    最新のサーバーフレームワーク事情に精通している訳ではないのですが、ちょっと触った感覚としてはYesodが想像以上に多機能で本格的なフレームワークであることがわかりました。rubyのWEBrick的なプリミティブなものをイメージしていたのですが比較になりません。ざっとみただけでも以下のような機能、仕組みが用意されており「恐れ入りました」という感じです。
    • ビルド→サーバー起動がyesodコマンド一発
      • サーバー起動中もコードの変更を検知して自動的にビルド&デプロイ
    • ログ出力
    • Handlerの登録がadd-handler一発
      • 空のハンドラコード
      • ハンドラの登録
    • 各種DB対応
      • SQLite, PostgreSQL, MySQL, MongoDB
      • config/modelsにORマップ定義ができる
    • 認証・認可の仕組みも備わっている模様
      • OAuthのためのライブラリも存在
    • viewをtemplate(.hamlet)として分離できる
      • hamletの中でhaskellコードを書くことも可能
      • ループを書ける!
      • コードから参照しているhamletが存在しないと、ビルド時にエラー!
    • 自動生成されるhtmlをみていると各種ブラウザの振る舞いの違いも吸収してくれそう?
    最後に(haskell超初心者の)自分にとって、よく理解できなかった手順の意味、チュートリアルの説明と調査結果をまとめておきます。
    • cabal-devってなんのためのパッケージ?
      • どうもcabalの代わりにcabal-devを利用することで、cabalでインストール済みのパッケージ群から独立して、個別のパッケージ群をインストールすることができるようです。これにより特定の環境で利用するパッケージのバージョンを固定し、cabalによるパッケージ更新の影響を受けなくできます(ここここのブログエントリ参考にしました)。
    • cabal-dev installは具体的には何をやっているの?
      • 前述の通りcabal-devを用いると既存のパッケージを参照しないように、必要なパッケージを全て0からダウンロード&インストールすることになります。これによりyesodでは30分という時間がかかるようです。
    • チュートリアル中に"It is a good practice to use Data.Text instead of String."とあったが、その心は?
      • hackageのData.Textのページを見ると、「時間的・空間的に効率の良いユニコードテキストの実装」「正規化、正規表現、非標準的なエンコーディング、ロケール等の便利な機能がそろっている」ということらしい。

    github上のリポジトリでチュートリアルで利用されたコード類が公開されているようです。
    終わり。