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

2018年3月5日月曜日

[haskell] http-clientライブラリを利用してHaskellでHTTPクライアント機能を実装する

Haskellでは、http-clientライブラリを用いることで、HTTPクライアント機能を簡単に実装できます。http-client以外にも何種類かライブラリがありますが、今回はhttp-client, http-client-tlsの機能と使い方をまとめておきます。

本エントリで紹介するhttp-client, http-client-tlsライブラリの機能:
  • 単純なHTTP GETリクエスト
    • 主要な型の説明
  • Managerのカスタマイズ 
    • https
    • proxy設定
    • タイムアウト値の設定
  • Requestのカスタマイズ
    • ベーシック認証
    • リクエストヘッダ
  • Responseの操作
    • ストリーミング受信
    • レスポンスヘッダの参照
  • エラーハンドリング


単純なHTTP GETリクエスト

{-# LANGUAGE OverloadedStrings #-}
import Network.HTTP.Client
import Network.HTTP.Types.Status (statusCode)

main :: IO ()
main = do
  manager <- newManager defaultManagerSettings

  request <- parseRequest "http://httpbin.org/get"
  response <- httpLbs request manager

  putStrLn $ "The status code was: " ++ (show $ statusCode $ responseStatus response)
  print $ responseBody response

デフォルトManagerと、"http://httpbin/org/get"へのRequestを生成し、それらを引数にhttpLbsを呼び出して、サーバーからのResponseを得てその内容を表示するサンプルです。
文字列リテラルをRequest, ByteStringに変換する目的でOverloadedStrings言語拡張を利用しています。

主要な型の説明:

  • Manager
    • Managerはサーバーとの間に生成するコネクションの管理するもの。
    • 複数サーバーのコネクションを管理する前提で実装されており、クライアントの中では1つのインスタンスを共有することが推奨されている。 
    • Managerに対しては以下のような設定ができる。デフォルト設定を併記。
      • プロキシ設定:環境変数(http_proxy)の値を利用
      • TLS(https):サポートなし
      • 1サーバーあたりのkeep-aliveコネクション維持数: 10
      • 最大同時オープンコネクション数: 512
      • 受信タイムアウト: 30秒
  • Request
    • 特定サーバーに対して送信する1つのリクエストを表す型。
    • parseRequest関数などでHTTP METHOD, URIを指定してインスタンスを生成する。
    • Request単位の細かい設定ができる
      • ベーシック認証情報(アカウント&パスワード) 
      • プロキシ設定、プロキシ認証情報
      • クエリストリング、bodyなどの送信データ
    • HTTPメソッド(GET, POST, DELETEなど)、ヘッダ は生成したRequestインスタンスに対して、record構文で設定する
      • メソッドのデフォルトはGET
      • Content-Length, Transfer-Encoding, Accept-Encodingが自動で設定される
  • Response 
    • サーバーに送信したRequestに対応するサーバーからのレスポンスを表す型。HTTPステータスコード、レスポンスヘッダ、レスポンスボディなどを取り出すことができます。


Managerのカスタマイズ

defaultManagerSettingsの代わりにtlsManagerSettingsを用いてManagerを生成することでhttps通信が可能になります。 以下のサンプルではhttps対応に加えて、プロキシサーバーとして"127.0.0.1:8080"をManagerに設定しています。
{-# LANGUAGE OverloadedStrings #-}
import Network.HTTP.Client
import Network.HTTP.Types.Status (statusCode)
import Network.HTTP.Client.TLS (tlsManagerSettings) -- 新たにimport文を追加

main :: IO ()
main = do
  -- manageSetProxyでデフォルトのプロキシ設定を上書く。
  -- tlsManagerSettingsを利用するとこでhttps通信が可能になる。
  manager <- newManager $ managerSetProxy (useProxy $ Proxy "127.0.0.1" 8080) tlsManagerSettings

  request <- parseRequest "https://httpbin.org/get" -- https通信に変更
  response <- httpLbs request manager

  putStrLn $ "The status code was: " ++ (show $ statusCode $ responseStatus response)
  print $ responseBody response

プロキシの設定:

  • デフォルトの挙動。環境変数(http_proxy/https_proxy) を参照する。
    • defaultProxy :: ProxyOverride
  • プロキシサーバーの設定を無視して通信する。
    • noProxy :: ProxyOverride
  • コードで明示的にプロキシサーバーを設定する。
    • useProxy :: Proxy -> ProxyOverride

レコード構文でManagerSettings値を生成する方法:

非公開APIを利用するため、オススメの方法ではありませんが以下のようにレコード構文を用いることで、Managerの細かい設定をカスタマイズすることができます。
import Network.HTTP.Client.Internal
-- tlsManagerSettingsをベースにmanagerResponseTimeoutを30→5秒に、
-- managerConnCountを10→3に変更
mySettings :: ManagerSettings
mySettings = tlsManagerSettings
             { managerResponseTimeout = responseTimeoutMicro 5000000
             , managerConnCount = 3
             }


Requestのカスタマイズ

以下のコードはRequestに対して、リクエストヘッダ、クエリーパラメタ、ベーシック認証情報を設定するサンプルです。Requestに対して設定できる項目の詳細は、Request type and fieldsを参照してください。
{-# LANGUAGE OverloadedStrings #-}
import Network.HTTP.Client
import Network.HTTP.Types.Status (statusCode)

main :: IO ()
main = do
  manager <- newManager defaultManagerSettings

  initialRequest <- parseRequest "http://httpbin.org/anything"
  let request = initialRequest
          { method = "POST"   -- "GET", "PUT", "DELETE"などのメソッドを指定
          , queryString = "foo=bar&xxx=yyy" -- parseRequestのuriに記述してもよい
          , requestHeaders =  -- ヘッダはタプル(名前、値)のリストで指定
              [ ("User-Agent", "New Agent!")
              , ("Content-Type", "text/plain")
              , ("Added-Header", "hoge")
              ]
          , requestBody = "body string."
          }
  let authRequest = applyBasicAuth "user" "pass" request -- ベーシック認証情報付与
  response <- httpLbs authRequest manager

  putStrLn $ "The status code was: " ++ (show $ statusCode $ responseStatus response)
  print $ responseBody response



Responseの操作

httpLbs関数はRequestとManagerを引数にとり、IO (Response ByteString)を返します。Response型クラスの関数で、Responseからステータスコード、レスポンスヘッダなどの情報を取り出すことができます。ResonseのAPIリファレンスはこちら
httpLbsは全データを受信しますが、大きなデータをレスポンスとして受信する場合はhttpLbsの代わりに、withResponseとbrReadを利用して逐次受信することが推奨されています。以下にそのサンプルを記載します。
{-# LANGUAGE OverloadedStrings #-}
import Network.HTTP.Client
import Network.HTTP.Types.Status (statusCode)
import qualified Data.ByteString as B

main :: IO ()
main = do
  manager <- newManager defaultManagerSettings

  request <- parseRequest "http://httpbin.org/get"
  withResponse request manager receiveResponse


receiveResponse :: Response BodyReader -> IO ()
receiveResponse response = do
  putStrLn $ "response version: " ++ (show $ responseVersion response)
  putStrLn $ "status code: " ++ (show $ statusCode $ responseStatus response)
  putStrLn $ "response header: " ++ (show $ responseHeaders response)

  -- receive body data block by block
  let loop = do
        bs <- brRead $ responseBody response
        if B.null bs
          then putStrLn "\nFinished response body"
          else do
            print bs
            loop
  loop



エラーハンドリング

以下は、urlのパース(parseRequest)、及び、サーバーとの通信(httpLbs)のエラーハンドリングを行うサンプルです。これらの関数はエラー時にはHttpExceptionをスローします。サンプルではtry関数を用い、Left eでスローされたHttpExceptionの情報を表示しています。
{-# LANGUAGE OverloadedStrings #-}
import Network.HTTP.Client
import Network.HTTP.Types.Status (statusCode)
import Control.Exception (try)
import System.Environment (getArgs)


createRequest :: [String] -> IO Request
createRequest [] = do
  putStrLn "NOTE: no argument is given. so use not existing url."
  parseRequest "http://unknown-host:80/" -- valid but not exists
createRequest args = do
  let url = head args
  eRequest <- try $ parseRequest url
  case eRequest of
    Left e -> do
      print (e :: HttpException)
      putStrLn $ "given url (1st argument) is invalid: " ++ url
      error "error!"
    Right request -> return $ request


main :: IO ()
main = do
  args <- getArgs
  manager <- newManager defaultManagerSettings
  request <- createRequest args

  eResponse <- try $ httpLbs request manager
  case eResponse of
    Left e -> do
      print (e :: HttpException)
      putStrLn $ "cannot reach server with given url: " ++ (head args)
    Right response -> do
      putStrLn $ "The status code was: " ++ (show $ statusCode $ responseStatus response)
      print $ responseBody response


他のライブラリ:

このエントリではhttp-clientの使い方を紹介していますが、これ以外にも以下のようなライブラリがあるようです。これらのライブラリも利用する機会があれば比較などを含めて記事にしたいと思います。

参考:

2018年2月25日日曜日

[haskell] stack install cryptoniteがno such instruction: `rdrand %r8'エラーで失敗する問題の対処方法

手許の環境(mac)で、cryptoniteライブラリのビルドがエラーになる問題が発生したが、ネットの情報を元に解決できたので、その症状と手順をblogに残しておく。


エラーの症状:

stack install cryptoniteで以下のようなエラーが発生。
% stack install cryptonite
--  While building custom Setup.hs for package cryptonite-0.24 using:
      /Users/xxx/.stack/setup-exe-cache/x86_64-osx/Cabal-simple_mPHDZzAJ_2.0.1.0_ghc-8.2.2 --builddir=.stack-work/dist/x86_64-osx/Cabal-2.0.1.0 build --ghc-options " -ddump-hi -ddump-to-file -fdiagnostics-color=always"
    Process exited with code: ExitFailure 1
    Logs have been written to: /Users/kurokawa/.stack/script/lts-10.4/.stack-work/logs/cryptonite-0.24.log

    Configuring cryptonite-0.24...
    Preprocessing library for cryptonite-0.24..
    Building library for cryptonite-0.24..
    [  1 of 120] Compiling Crypto.Cipher.DES.Primitive ( Crypto/Cipher/DES/Primitive.hs, .stack-work/dist/x86_64-osx/Cabal-2.0.1.0/build/Crypto/Cipher/DES/Primitive.o )

... snip ...

    [120 of 120] Compiling Crypto.Tutorial  ( Crypto/Tutorial.hs, .stack-work/dist/x86_64-osx/Cabal-2.0.1.0/build/Crypto/Tutorial.o )
    
    /private/var/folders/3f/7fg601f92_33t43jhn2p0v0w0000gn/T/stack47334/cryptonite-0.24/cbits/cryptonite_rdrand.c:89:0: error:
        no such instruction: `rdrand %r8'
       |
    89 |         asm volatile ("rdrand %0; setc %1" : "=r" (*buffer), "=qm" (err));
       | ^
    
    /private/var/folders/3f/7fg601f92_33t43jhn2p0v0w0000gn/T/stack47334/cryptonite-0.24/cbits/cryptonite_rdrand.c:89:0: error:
        no such instruction: `rdrand %r8'
       |
    89 |         asm volatile ("rdrand %0; setc %1" : "=r" (*buffer), "=qm" (err));
       | ^
    
    /private/var/folders/3f/7fg601f92_33t43jhn2p0v0w0000gn/T/stack47334/cryptonite-0.24/cbits/cryptonite_rdrand.c:89:0: error:
        no such instruction: `rdrand %r8'
       |
    89 |         asm volatile ("rdrand %0; setc %1" : "=r" (*buffer), "=qm" (err));
       | ^
    `gcc' failed in phase `Assembler'. (Exit code: 1)



原因:

ビルド環境のbinutil (or gcc)でrdrandインストラクションがサポートされていないと、このエラーが発生する。


解決方法:

cryptoniteのデフォルトはrdrandインストラクションを利用する設定になっているため、-support_rdrandを指定してrdrand利用コードを無効化する。
% stack install cryptonite --flag cryptonite:-support_rdrand
cryptonite-0.24: configure
cryptonite-0.24: build
cryptonite-0.24: copy/register
--flag cryptonite:-support_rdrandは、crptoniteパッケージのビルド処理において-support_rdrandフラグを適用することを意味する。
cabalをを利用 している環境では、以下の指定で同様の効果が得られるはず(未確認)。
% cabal configure --flag='-support_rdrand'
OR
% cabal install --constraint="cryptonite -support_rdrand"


参考:

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

参考:

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


参考:

2015年11月29日日曜日

[haskell][persistent][sqlite] Persistentパッケージ利用時にテーブルにインデックスを生成する方法

PersistentパッケージにはMigration機能が備わっており、自動的にテーブルを生成してくれます。スキーマ変更を行った際にも、変換が可能な限りテーブル内のレコードを保持したまま新しいスキーマに変換してくれます(Migration機能については過去のエントリでまとめています)。

自分が利用する上で、インデックスやトリガーを生成する手順が紹介されておらず困っていたのですが、rawExecuteという関数を用いることで自由にDDLを発行できることがわかりました。以下その手順とサンプルを紹介しておきます。

サンプルコード:

以下は、personテーブルのnameカラムにインデックスをs生成するサンプルです。runMigration実行直後に、runExecuteを実行することでインデックスを生成しています。このサンプルではインデックスを生成しているだけですが、同じ手順でトリガーの生成(CREATE TRIGGER)も可能です。
{-# LANGUAGE EmptyDataDecls             #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE GADTs                      #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE QuasiQuotes                #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE TypeFamilies               #-}
import Control.Monad.IO.Class  (liftIO)
import Database.Persist         -- persistentパッケージ
import Database.Persist.Sqlite  -- persistent-sqliteパッケージ
import Database.Persist.TH      -- persistent-templateパッケージ

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
Person
    name String
    age Int Maybe
    deriving Show
|]

main :: IO ()
main = runSqlite "test.db" $ do
    -- personテーブル生成
    runMigration migrateAll
    -- nameカラムにindexを生成する。placeholderがないため第2引数は[]でよい。
    -- 2回目以降、すでにインデックスが存在している場合のためにIF NOE EXISTSが必要。
    rawExecute "CREATE INDEX IF NOT EXISTS idx_name_on_person ON person(name);" [] -- here!

    michaelId <- insert $ Person "Michael" $ Just 26
    michael <- get michaelId
    liftIO $ print michael
runExecuteの第一引数には実行するSQLを記載します。既にインデックスが生成済みの場合には何も処理を行わないよう"IF NOT EXISTS"を指定しています。第二引数にはplaceholderの値を指定しますがこの例ではplaceholderは利用していないため"[]"を渡しています。
"IF NOT EXISTS"を指定しないと既に存在しているインデックスを再度生成しようとして、以下のようなエラーが報告されます。
*** Exception: SQLite3 returned ErrorError while attempting to perform prepare "CREATE INDEX idx_name_on_person ON person(name);": index idx_name_on_person already exists

補足:

Yesod BookのPersistentの解説には、rawQueryの利用手順が紹介されています。rawQueryとrawExecuteの型はそれぞれ以下のようになっています。
rawQueryが実行結果を配列で返すのに対し、rawExecuteの結果はunit(空)となっています。このためDDL (Data Definition Language)実行時にはrawExecuteを利用するのがよいと思われます。
さらにrawSqlという関数もあるようですが、こちらの使い分けはよく分からず…。ご存じの方是非ご教授ください。m(_ _)m
(2015/12/16:追記)rawQueryはData.Conduit.Source型で結果を返します。これは結果が巨大であってもストリーム処理できることを意味しています。これに対しrawSqlは単純にリストで結果を返すという違いがあります。

参考:

2015年11月8日日曜日

[haskell][stack] stack exec ghciで”Couldn't match expected type"エラーが発生する問題の対処

先日、haskellのパッケージ管理をcabalからstackに移行して「便利〜!」と感動していたところなのですが、stach exec ghciでソースをロードしようとすると"Couldn't match expected type: xxxxx"とエラーが発生する問題に遭遇しました。
ネットの情報を参考に解決することができたのでその手順をまとめておきます。

問題:

stack buildは成功するにもかかわらず、stack exec ghci xxx.hs(xxx.hsはbuild対象のファイル)がエラーになる。
stack exec ghci実行時のエラーログ:
% stack exec ghci FileToVec.hs
GHCi, version 7.10.2: http://www.haskell.org/ghc/  :? for help
[1 of 1] Compiling FileToVec        ( FileToVec.hs, interpreted )

FileToVec.hs:42:18:
    Couldn't match expected type ‘V.Vector a’
                with actual type ‘vector-0.10.12.3:Data.Vector.Vector a0’
    NB: ‘V.Vector’
          is defined in ‘Data.Vector’ in package ‘vector-0.11.0.0’
        ‘vector-0.10.12.3:Data.Vector.Vector’
          is defined in ‘Data.Vector’ in package ‘vector-0.10.12.3’
    Relevant bindings include
      v :: vector-0.10.12.3:Data.Vector.Vector a0
        (bound at FileToVec.hs:40:15)
      file_to_vec :: FilePath -> IO (V.Vector a)
        (bound at FileToVec.hs:33:1)
    In the first argument of ‘return’, namely ‘v’
    In a stmt of a 'do' block: return v
Failed, modules loaded: none.
Leaving GHCi.
この環境でのstack buildは以下のようなログで成功しています。
% stack build
csv2db-0.1.0.0: build
Preprocessing executable 'csv2db' for csv2db-0.1.0.0...
[2 of 4] Compiling FileToVec        ( FileToVec.hs, .stack-work/dist/x86_64-osx/Cabal-1.22.4.0/build/csv2db/csv2db-tmp/FileToVec.o )
[3 of 4] Compiling DbRecord         ( DbRecord.hs, .stack-work/dist/x86_64-osx/Cabal-1.22.4.0/build/csv2db/csv2db-tmp/DbRecord.o ) [TH]
Linking .stack-work/dist/x86_64-osx/Cabal-1.22.4.0/build/csv2db/csv2db ...
    DbRecord
    FileToVec
    MyArgs
csv2db-0.1.0.0: install
Installing executable(s) in XXXX

解決方法:

以下の2つの方法があります。
  1. execコマンドを用いず、stack ghciで起動する
  2. execコマンドを利用しなければロードすることができます。普通はこちらの手順を実行するのが正しいはずです。が、ファイル指定で起動できないのでプロンプトから:loadコマンドを実行する必要があります。
    % stack ghci
    Using main module: Package `csv2db' component exe:csv2db with main-is file: /Users/kurokawa/git/work_haskell/csv2db/Main.hs
    Configuring GHCi with the following packages: csv2db
    GHCi, version 7.10.2: http://www.haskell.org/ghc/  :? for help
    [1 of 4] Compiling FileToVec        ( FileToVec.hs, interpreted )
    [2 of 4] Compiling MyArgs           ( MyArgs.hs, interpreted )
    [3 of 4] Compiling DbRecord         ( DbRecord.hs, interpreted )
    [4 of 4] Compiling Main             ( XXXX/Main.hs, interpreted )
    Ok, modules loaded: MyArgs, DbRecord, Main, FileToVec.
    *Main> :load FileToVec
    [1 of 1] Compiling FileToVec        ( FileToVec.hs, interpreted )
    Ok, modules loaded: FileToVec.
    *FileToVec>
    
    stack ghci実行時にソースファイルを引数で指定した場合、以下のようなエラーになるのでご注意を。
    % stack ghci FileToVec.hs
    Error parsing targets: Directory not found: FileToVec.hs
    
  3. cabalを直実行して必要パッケージをインストールする
  4. stackを利用しないでcabal installで依存パッケージをインストールし、ローカルのxxx.hsがコンパイルできる状態にし、stackを利用しないでcabal exec ghci xxx.hsを実行する。成功時のログ:
    % cabal install vector
     ... snip...
    % cabal exec ghci FileToVec.hs
    GHCi, version 7.10.2: http://www.haskell.org/ghc/  :? for help
    [1 of 1] Compiling FileToVec        ( FileToVec.hs, interpreted )
    Ok, modules loaded: FileToVec.
    *FileToVec>
    

エラー原因(の推測):

stack build実行時に起動されるcabalが参照するパッケージリストと、stack exec ghciで起動されるghciが参照するパッケージリストが、異なっているのが原因だと思われます。
stack build実行時には、stackが内部で管理している(?)パッケージリストを参照しているようですが、stack exec ghciで起動されたghciはcabalを直接起動したときに参照されるパッケージリストを参照し、バージョンのズレが生じてエラーになります。

参考:

2015年8月29日土曜日

[haskell][persistent][sqlite] Persistentパッケージのmigration機能のまとめ

HaskellでDB操作ができるPersistentパッケージの紹介をしましたが、このエントリではPersistentパッケージが提供しているmigration機能をまとめておきます。
DBを作って運用していると、機能追加や仕様変更に伴いスキーマ変更が必要になるケースが多々あります。このようなケースにおいてPersistentのmigration機能がどれくらい使えるのかを調べた結果です。

基本:

Persistetのmigration機構は(保守的なルールに沿って)スキーマ変更をある程度まで自動で処理してくれます。
ロードしたDB内のテーブル情報と、コードで定義されたEntity Definition(テーブル定義)を比較し、以下のケースにおいてスキーマの変更を行います。
  • カラムの型を変更した場合:
    • ただし、値の変換ができない場合には、DBによって拒否されることになります。
  • カラムを追加した場合:
    • ただし、追加したカラムにNOT NULL制約がある場合は、デフォルト値は設定されず、エラーとなります。
  • カラムのNOT NULL制約を外してNULL許容にした場合:
    • 変換を実行します。
    • 逆にNULL許容カラムにNON NULL制約に変更するケースの結果は、DBの状態に依存します(NULL値が存在しなければ成功、存在していると失敗)。
  • 新規Entity(テーブル)が追加された場合
Persistentは以下のケースには対応していません。
  • フィールド、Entith(テーブル)のリネーム
    • 変更前の名前と変更後の名前の対応関係がわからないので…。
  • フィールドの削除
    • データ喪失につながるので、デフォルトではフィールド削除はエラーとなります。ですが、runMigration()の代わりにrunMigrationUnsafe()を呼ぶことで強制的に削除することは可能です(もちろん非推奨)。

実験:

Migration対象のDBを以下のコードでold.dbという名前で生成します。このコードで生成されたold.dbを、新しいスキーマにmigrateすると、結果がどのようになるかをまとめておきます。
{-# LANGUAGE EmptyDataDecls             #-}
{-# LANGUAGE FlexibleContexts           #-}
{-# LANGUAGE GADTs                      #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses      #-}
{-# LANGUAGE OverloadedStrings          #-}
{-# LANGUAGE QuasiQuotes                #-}
{-# LANGUAGE TemplateHaskell            #-}
{-# LANGUAGE TypeFamilies               #-}
import Database.Persist
import Database.Persist.TH
import Database.Persist.Sqlite
import Control.Monad.IO.Class (liftIO)

share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
Person
    name String
    age Int
    deriving Show
|]

main :: IO ()
main = runSqlite "old.db" $ do
    -- this line added: that's it!
    runMigration migrateAll
    michaelId <- insert $ Person "Michael" 26
    michael <- get michaelId
    liftIO $ print michael


以下、いくつかのスキーマ変更を実際に試してみた結果です。
  • NULL許容カラムの追加:OK
    • コードの変更点
    • Person
          name String
          age Int
          address String Maybe  -- add new NULL-able column
          deriving Show
      |]
      
          michaelId <- insert $ Person "Michael" 26 (Just "Tokyo")
      
    • 実行結果ログ
    • Migrating: CREATE TEMP TABLE "person_backup"("id" INTEGER PRIMARY KEY,"name" VARCHAR NOT NULL,"age" INTEGER NOT NULL,"address" VARCHAR NULL)
      Migrating: INSERT INTO "person_backup"("id","name","age") SELECT "id","name","age" FROM "person"
      Migrating: DROP TABLE "person"
      Migrating: CREATE TABLE "person"("id" INTEGER PRIMARY KEY,"name" VARCHAR NOT NULL,"age" INTEGER NOT NULL,"address" VARCHAR NULL)
      Migrating: INSERT INTO "person" SELECT "id","name","age","address" FROM "person_backup"
      Migrating: DROP TABLE "person_backup"
      Just (Person {personName = "Michael", personAge = 26, personAddress = Just "Tokyo"})
      
  • NON NULL制約のカラム追加:NG
    • コード変更点
    • share [mkPersist sqlSettings, mkMigrate "migrateAll"] [persistLowerCase|
      Person
          name String
          age Int
          address String  -- add new NON NULL column
          deriving Show
      |]
      
          michaelId <- insert $ Person "Michael" 26 "Tokyo"
      
    • 実行結果ログ
    • Migrating: CREATE TEMP TABLE "person_backup"("id" INTEGER PRIMARY KEY,"name" VARCHAR NOT NULL,"age" INTEGER NOT NULL,"address" VARCHAR NOT NULL)
      Migrating: INSERT INTO "person_backup"("id","name","age") SELECT "id","name","age" FROM "person"
      add_column: user error (SQLite3 returned ErrorConstraint while attempting to perform step.)
      
  • カラムの型の変更:OK
    • コード変更点
    • Person
          name String
          age String  -- change type from Int to String
          deriving Show
      |]
      
          michaelId <- insert $ Person "Michael" "26"
      
    • 実行結果ログ
    • Migrating: CREATE TEMP TABLE "person_backup"("id" INTEGER PRIMARY KEY,"name" VARCHAR NOT NULL,"age" VARCHAR NOT NULL)
      Migrating: INSERT INTO "person_backup"("id","name","age") SELECT "id","name","age" FROM "person"
      Migrating: DROP TABLE "person"
      Migrating: CREATE TABLE "person"("id" INTEGER PRIMARY KEY,"name" VARCHAR NOT NULL,"age" VARCHAR NOT NULL)
      Migrating: INSERT INTO "person" SELECT "id","name","age" FROM "person_backup"
      Migrating: DROP TABLE "person_backup"
      Just (Person {personName = "Michael", personAge = "26"})
      
ログを見る限り以下の手順でmigrationを行っています。参照したYesodサイトに記載がありましたがSQLiteではALTER TABLEの機能が足りないためだと思われます。
  1. 古いスキーマでperson_backupテーブルを生成
  2. person_backupテーブルにオリジナルのpersonテーブルの内容をコピー
  3. personテーブルを破棄
  4. 新しいスキーマでpersonテーブルを生成
  5. person_backupの内容をpersonにコピー
  6. person_backupテーブルを破棄

宿題:

自分の用途では、NON NULL制約のカラムを追加し、既存レコードの値にはデフォルト値を埋めた状態にしたかったのですがPersistent備え付けの機能ではできないようです。
migration処理はTemplateHaskellによって生成されたコードで実行されているらしいので、そのあたりを弄ればなんとかなるのかも。
カラム名の変更も新スキーマのカラムと旧スキーマのカラム名の対応をmigration処理にうまく伝えることができれば、なんとかなりそうな気がします。このあたりは今後の課題として、方法がわかったらまたblogにまとめます。

参考:

2015年7月19日日曜日

[cygwin][haskell][emacs] MinGWでcygwinの"/cygdrive"パスにアクセスする裏技(cygwin環境のemacsでflycheckを動作させる方法)

haskell関連のコマンドはMinGW上でビルドされているため、cygwin環境の"/cygdrive"から始まるパスには対応していません。haskell-hlintから呼び出されるhlintも当然この問題の影響を受けておりemacs関連の設定が適切にされていたとしても、以下のようなエラーが表示されてしまいます。

ミニバッファに表示されるエラー詳細:

Suspicious state from syntax checker haskell-hlint: Checker haskell-hlint returned non-zero exit code 1, but no errors from output: hlint.exe: Couldn't find file: /cygdrive/c/Users/Hiroyuki/tmp/flycheck_hello.hs

MinGW関連コマンドで"/cygdrive"から始まるパスを解釈できない問題の回避方法:

"/cygdrive"から始まるパスをMinGWによってビルドされたバイナリ(hlint)からも解釈できるようになれば、この問題は回避できるのでは?と考え、mklinkを用いて以下の手順でシンボリックリンクを作ってみました。
C:\> cd \
C:\> mkdir cygdrive
C:\> cd cygdrive
C:\cygdrive> mklink /d C C:\
これが大成功!"C:\CYGDRIVE\C"に"C:\"へのシンボリックリンクを作っておくことでcygwin上の"/cygdrive/c/xxx/yyy"といったパスがMinGW上では"C:\xxx\yyy"として解釈される状態になります。
この環境ではemacs上でflycheckが正しく動作するようになりました。

この環境はMinGW関連コマンド全般に適用可能:

この裏技はMinGW/MSYSでビルドされた任意のコマンドに適用可能なはずです。cygwin環境とMinGW環境が混在していて、MinGW/MSYS関連コマンドの実行に弊害が出ている方は是非お試しいただければ、と思います。

余談:うまくいかなかった方法(cygwinのマウント機能の利用メモ):

cygwin環境において"C:"ドライブのルートを"/C"としてマウントする方法もありますが、MinGWは"/C/xxx/yyy"というパスは解釈できずNGでした。
% cd /
% mkdir c
% mount c: /c
"C:"を/C:"としてマウントできれば、と思ったのですが残念ながら以下のようなエラーになりました…。
% mkdir c:
% mount c: c:
mount: c:: Invalid argument

参考:

2015年4月25日土曜日

[haskell] cmdargsパッケージで楽々コマンドライン引数パース

コマンドラインツール実装時、オプション指定とか引数の並びとか考え始めると大変です。HaskellではSystem.EnvironmentモジュールからgetArgsという関数が提供されていますが、本エントリで紹介するcmdargsパッケージを利用すると以下のようなことが簡単にできます。

cmdargsパッケージの特徴:

  • データ構造を定義するだけで起動引数・オプションのパースができる
  • パース結果を型付きで参照することができる
  • パース失敗時には、原因がわかるエラーメッセージが表示される
  • --help, --versionオプションで表示される情報を自動で生成してくれる
Haskell版GNU getoptライブラリと比べて以下の2点が優れている、とHPには書かれています
  1. HLintコマンドラインのハンドリングが1/3の短さ
  2. Cabal, darcsなどのmultiple modeプログラムに対応
  3. ※multiple modeは、引数に応じてオプション引数仕様が変わる動作を指している模様

簡単なサンプル:Sample.hs

{-# LANGUAGE DeriveDataTypeable #-}
import System.Console.CmdArgs

data Sample = Sample {
      hello :: String
} deriving (Show, Data, Typeable)

sample = Sample {
           hello = def &= help "World argument" &= opt "world"}
         &= summary "This is a sample of CmdArgs."

main = print =<< cmdArgs sample
パース結果を格納するデータ型(Sample)を定義しています。この型はShow, Data, Typeableのインスタンスである必要があります。defは任意の型のデフォルト値で、この例(String)では""と同じです。&=以降はhelp用のアノテーションになります。

実行結果:Sample.hs

% runghc Sample.hs --help
This is a sample of CmdArgs.

sample [OPTIONS]

Common flags:
  -h --hello[=ITEM]  World argument
  -? --help          Display help message
  -V --version       Print version information

% runghc Sample.hs --hello
Sample {hello = "world"}

より複雑なパース:MyArgs.hs

以下の例は自作コマンドに利用したコードの一部です。複数のCSVファイル、もしくはCSVファイルが格納されたディレクトリを入力として与え、CSVファイル内のデータをdboptで指定したタイプのDB(ファイル名・コネクションは必須の第一引数)に格納するものでした。
{-# LANGUAGE DeriveDataTypeable #-}
import System.Console.CmdArgs

data DbOpt = SQLite | PostgreSQL | MySQL deriving (Data, Typeable, Eq, Show, Read)

data MyArgs = MyArgs {
      dbopt    :: DbOpt
    , schema   :: String
    , recursive :: String
    , targetdb :: String
    , csvfiles :: [String]
    , color :: Bool
    } deriving (Data,Typeable,Show)

help_dbopt = "specify DB type. default DB is 'sqlite'.\n"
             ++ "'postgresql', 'mysql' & etc. may be supported in the future."
help_schema = "specify table type.\n"
              ++ "specify predefined schema index such as 'd12', 'd13', etc.\n"
              ++ "default is 'normal' which stores all values as string.\n"
help_recursive = "specify directory to iterate all files in it recursively."
help_targetdb = "specify DB file/connection name."
help_csvfiles = "specify one ore more csv files.\n"
              ++ "one file must be specified at the minimum."
help_color = "enable colored output."
help_program = "parse CSV files and store all data into DB.\n"
               ++ "TARGET_DB is the mandatory argument."

config = MyArgs {
      dbopt   = SQLite &= typ "TARGET_DB_TYPE" &= help help_dbopt
    , schema  = "normal" &= typ "SCHEMA_INDEX" &= help help_schema
    , recursive = def &= typ "RECURSIVE_DIR" &= help help_recursive
    , targetdb  = def &= typ "TARGET_DB" &= argPos 0 -- &= help help_targetdb
    , csvfiles = def &= typ "CSV_FILES" &= args -- &= help help_csvfiles
    , color = def &= name "c" &= help help_color
} &= verbosity &= program "MyArgs" &= help help_program

main = print =<< cmdArgs config

実行結果:MyArgs.hs

# ヘルプ
% runghc MyArgs.hs --help
The MyArgs program

MyArgs [OPTIONS] TARGET_DB [CSV_FILES]
  parse CSV files and store all data into DB. TARGET_DB is the mandatory
  argument.

Common flags:
  -d --dbopt=TARGET_DB_TYPE     specify DB type. default DB is 'sqlite'.
                                'postgresql', 'mysql' & etc. may be supported
                                in the future.
  -s --schema=SCHEMA_INDEX      specify table type. specify predefined schema
                                index such as 'd12', 'd13', etc. default is
                                'normal' which stores all values as string.
  -r --recursive=RECURSIVE_DIR  specify directory to iterate all files in it
                                recursively.
  -c --color                    enable colored output.
  -? --help                     Display help message
  -V --version                  Print version information
  -v --verbose                  Loud verbosity
  -q --quiet                    Quiet verbosity

# エラーケース(dboptが不正)
% runghc MyArgs.hs -d invalid_dbopt
Could not read, expected one of: sqlite postgresql mysql
# エラーケース(引数不足)
% runghc MyArgs.hs 
Requires at least 1 arguments, got 0

# 正常系(複数csvファイル指定)
% runghc MyArgs.hs a.db 1.csv 2.csv
MyArgs {dbopt = SQLite, schema = "normal", recursive = "", targetdb = "a.db", csvfiles = ["1.csv","2.csv"]}
# 正常系(オプションでdbopt, recursiveを指定)
% runghc MyArgs.hs a.db -r data_dir -d MySQL
MyArgs {dbopt = MySQL, schema = "normal", recursive = "data_dir", targetdb = "a.db", csvfiles = []}

パースの結果得たMyArg型の値から、起動引数・オプションで指定した値を取り出すことができます。必須の第一引数が指定されていない場合、dboptで不正なDBタイプを指定した場合など、起動引数・オプションが不正である場合に、適切なエラーが出力されているのが確認できます。
(2016/02/11: Bool型のオプション--colorを追加。)

参考:

2015年4月5日日曜日

[windows][haskell] Widnwos環境でHaskell Platformを完全削除する方法

Windows上でHaskell Platformを完全削除する方法です。
LinuxやMac環境についてはネット上に多数情報がありますが、Windows環境についてはそれが見当たらなかったため、本エントリにまとめておきます。確認した環境はWindows 8.1+Haslell Platform 2014.2.0.0です。

削除手順:

Windows環境では以下の手順でHaskell Platformを完全に削除できます。
  1. Haskell Platformのアンインストール
    1. [コントロールパネル] - [プログラム] - [プログラムと機能]を開く
    2. "Haslell Platform 2014.2.0.0"を選択して[アンインストール]を実行
  2. ユーザー領域に作成されたパッケージ関連ファイルの削除
    1. 手動(エクスプローラ、rmコマンドなど)で次の2つのディレクトリ以下を完全に削除
      • C:\Users\???\AppData\Roaming\ghc
      • C:\Users\???\AppData\Roaming\cabal
      ※"C:\Users\???\"はユーザーのホームディレクトリです。
    2. 手動で全ての".cabal-sandbox"ディレクトリを削除する
    3. cabal sandbox initを実行したディレクトリには.cabal-sandboxディレクトリが生成され、その中にパッケージ関連ファイルが格納されています。(cabal sandbox delete もしくはエクスプローラーなどで)これらを全て削除します。

参考:

2015年3月25日水曜日

[windows][haskell] bzlibパッケージ利用時にHSbzlib-0.5.0.5.o: unknown symbol `fileno'となる問題の対処

Haskellでは、bzip2で圧縮されたデータを解凍(もちろん逆に圧縮も)できるbzlibパッケージが提供されています。

ところが、自分の環境(Windows 8.1+Haslell Platform 2014.2.0.0)でbzlibパッケージをインポートするサンプルプログラムをビルド・実行しようとすると、以下のようなエラーになってしまいました。
> runghc examples/bunzip2.hs       
bunzipz2.hs: C:\Users\XXX\AppData\Roaming\cabal\x86_64-windows-ghc-7.8.3\bzlib-0.5.0.5\HSbzlib-0.5.0.5.o: unknown symbol `fileno'                      
bunzip2.hs: bunzip2.hs: unable to load package `bzlib-0.5.0.5'            

ネット上の情報を漁ってみたのですが同じ問題に遭遇している人はいなさそう・・・。ということで、自力で調べてみました。以下、分かったことと回避方法をまとめておきます。


問題の原因:

  • 詳細は「調査の内容」に記しましたが、bzlibが内部で呼び出しているfileno()とfdopen()が定義されているmingwのlibmoldname.aがリンクされないのが原因のようです。

問題の回避方法

  • libmoldname.aをリンクする修正が正しいのですが、その方法が分からなかったため、bzlibのコードに手をいれてileno()とfdopen()を呼ばないバージョンをビルドし、それで正規のパッケージを置き換えることで、bzlibが利用できるようになりました。以下、その手順です。
    1. cabalのpackageがダウンロードされているディレクトリに移動する
    2. % cd C:\Users\XXX\AppData\Roaming\cabal\packages\hackage.haskell.org\bzlib\0.5.0.5
      
    3. bzlib-0.5.0.5.tar.gzを展開する
    4. % tar xzf bzlib-0.5.0.5.tar.gz
      
    5. このパッチをDLしてbzlib.patchという名前で保存し、適用する
    6. % patch -p1 < bzlib.patch
      
    7. パッチを含んだアーカイブでオリジナルのbzlib-0.5.0.5.tar.gzを置き換える
    8. % tar czf bzlib-0.5.0.5.tar.gz bzlib-0.5.0.5/
      
    9. bzlibパッケージをreinstallする
    10. % cabal install bzlib --reinstall
      

調査の内容:

  • 自分の環境にcygwinとHaskell付属のmingwの両方が存在しているが原因かと思ったがそうではないらしい。
    • mingwのライブラリにもfilenoがちゃんと用意されている。
    • mingw環境においてここのfilenoのサンプルがビルドでき、動作しているため
      • > gcc fileno.cpp
  • mingw環境の確認
    • filenoを利用するC言語版サンプルをビルドしてmapファイルを出力したところ、filenoはlibmoldname.aというライブラリの定義をリンクしていることが分かった。
      • gcc -Wl,-Map=a.map -g fileno.cpp
      •  .text          0x0000000000402c10        0x8 c:/program files/haskell platform/2014.2.0.0/mingw/bin/../lib/gcc/x86_64-w64-mingw32/4.6.3/../../../../x86_64-w64-mingw32/lib/../lib/libmoldname.a(dnvzs00026.o)
                        0x0000000000402c10                fileno
        
  • bzlib-0.5.0.5をローカル環境でビルドしてその内容を確認
    • filenoがunknown symbolとなっているのは以下の2つのオブジェクトファイル
      • bzlib-0.5.0.5/dist/build/cbits/bzlib.o
      • bzlib-0.5.0.5/dist/build/cbits/HSbzlib-0.5.0.5.o
    • 上記オブジェクトファイルで同じくunknown symbolになっているfopen, fread, fwrite, fcloseといった関数はlibmoldname.aとは別のlibmsvcrt.aにアーカイブされている。
      • a.mapより
      •  .text          0x0000000000402c70        0x8 c:/program files/haskell platform/2014.2.0.0/mingw/bin/../lib/gcc/x86_64-w64-mingw32/4.6.3/../../../../x86_64-w64-mingw32/lib/../lib/libmsvcrt.a(dnhvs00971.o)
                        0x0000000000402c70                fopen
         .text          0x0000000000402c78        0x8 c:/program files/haskell platform/2014.2.0.0/mingw/bin/../lib/gcc/x86_64-w64-mingw32/4.6.3/../../../../x86_64-w64-mingw32/lib/../lib/libmsvcrt.a(dnhvs00979.o)
                        0x0000000000402c78                fread
        
        
  • windows版のみcabal buildでパッケージをビルドする際に、filenoの解決に必要なlibmoldname.aのリンクが漏れているのでは?
    • 試しに、libmoldname.aを削除してcabal buildをビルドしてみたところ、リンクエラーになりました。ということは、パッケージビルド時にはlibmoldname.aをリンクしようとしていることになります。・・・ハズレ。
    • Resolving dependencies...
      Configuring bzlib-0.5.0.5...
      Building bzlib-0.5.0.5...
      Preprocessing library bzlib-0.5.0.5...
      c:/program files/haskell platform/2014.2.0.0/mingw/bin/../lib/gcc/x86_64-w64-mingw32/4.6.3/../../../../x86_64-w64-mingw32/bin/ld.exe: cannot find -lmoldname
      c:/program files/haskell platform/2014.2.0.0/mingw/bin/../lib/gcc/x86_64-w64-mingw32/4.6.3/../../../../x86_64-w64-mingw32/bin/ld.exe: cannot find -lmoldname
      collect2: ld returned 1 exit status
      linking dist\build\Codec\Compression\BZip\Stream_hsc_make.o failed (exit code 1)
      
      command was: C:\Program Files\Haskell Platform\2014.2.0.0\mingw\bin\gcc.exe dist\build\Codec\Compression\BZip\Stream_hsc_make.o dist\build\Codec\Compression\BZip\Stream_hsc_utils.o -o dist\build\Codec\Compression\BZip\Stream_hsc_make.exe -LC:\Users\XXX\AppData\Roaming\cabal\x86_64-windows-ghc-7.8.3\bytestring-0.10.4.1 -LC:\Program Files\Haskell Platform\2014.2.0.0\lib\deepseq-1.3.0.2 -LC:\Program Files\Haskell Platform\2014.2.0.0\lib\array-0.5.0.0 -LC:\Program Files\Haskell Platform\2014.2.0.0\lib\base-4.7.0.1 -lwsock32 -luser32 -lshell32 -LC:\Program Files\Haskell Platform\2014.2.0.0\lib\integer-gmp-0.5.1.0 -LC:\Program Files\Haskell Platform\2014.2.0.0\lib\ghc-prim-0.3.1.0 -LC:\Program Files\Haskell Platform\2014.2.0.0\lib/rts-1.0 -lm -lwsock32 -lgdi32 -lwinmm
      
  • パッケージビルド時ではなく、パッケージを利用するコードをコンパイルして実行ファイルを生成するときにlibmodulename.aがリンクされていないようです。
    • これはどこの設定に書かれているのか・・・?
      
      

備考:

  • 冒頭に書いたとおり、本来ならば適切にlibmoldname.aをリンクするように修正するのが正しい解決方法なのですが、bzlibパッケージの中にそのような見つからず、どこを変えれば良いか分かりませんでした・・・。orz 。どこを修正すればリンクライブラリを追加できるか知っている方、是非ご教示ください。 m(_ _)m 
  • unknown symbolとなるfilenoとfdopenはbzlib-0.5.0.5/cbits/bzlib.cの中で呼び出しています。コードを確認したところどちらも環境によってはundefされており、呼び出さなくても問題はないことを確認しました。
  • なお、Linux, Mac環境上ではこの問題は発生しません。問題なく利用できています。

参考: