MacでHerokuにYesodを(Ubunto on VirtualBox導入編)

HerokuにYesodで作ったWebアプリを公開出来るという情報が!
これはやるしかない!

情報源はこちらです。
Haskell on Heroku - ごったく

大感謝!

ただ開発はMacで支障ないのですがHerokuに公開する実行ファイルはUbuntu 10.04の64bit版でビルドする必要があるようです。
実際Macでビルドしたものは全く動く気配がありませんでした。

そこでMacVirtualBoxをインストールして、その上でUbuntuを動かすことにしました。

VirtualBoxのインストール

VirtualBoxのダウンロードページの「VirtualBox 4.1.8 for OS X hosts x86/amd64」というところからインストールファイルをダウンロードします。

UbuntuのCDイメージをダウンロード

Ubuntuのダウンロードページの「Ubuntu 10.04.1 LTS - 2013年4月までサポート」からCDイメージをダウンロードします。

この↑ページからは32bit版しかダウンロード出来ないです。64bit版はこちらからダウンロードする必要があります!

仮想環境の作成とUbuntuのインストール

こちらのページに手順がきれいにまとまっています。
MacのVirtualBoxにLinuxのUbuntu 10.04をインストールする方法 / Inforati
そのままなぞれば簡単にインストール出来ました。

大感謝!

次はGHCとcabal-installを導入します。

Yesodでファイルアップロード

Yesodにもだいぶ慣れてきました。慣れるまではなかなかコンパイルを通せず苦労しますが、コンパイルに通りさえすれば意図通りに動かないということはまずありません。
YesodはHaskellの素晴らしさを見事に受け継いでいるな、と感じます。

さて今回はファイルアップロードのTipsを。

ポイントはFileInfoという型と、fileAFormReq(Opt)関数です。
アップロードされたファイルはFileInfoで受け取ります。
input type="file"を構成するのは、fileAFormReq(Opt)関数です。

ハンドラの例。

module Handler.File where

import Import
import Data.Text (append)

data FileForm = FileForm {
  file1 :: FileInfo
  , file2 :: Maybe FileInfo
}

fileForm :: Html -> Form Jabara Jabara (FormResult FileForm, Widget)
fileForm = renderDivs $ FileForm
  <$> fileAFormReq ""
  <*> fileAFormOpt ""

getFileR :: Handler RepHtml
getFileR = do
  form <- runFormPost fileForm
  processForm form

postFileR :: Handler RepHtml
postFileR = do
  form <- runFormPost fileForm
  processForm form

processForm :: ((FormResult FileForm, Widget), Enctype) -> Handler RepHtml
processForm ((FormSuccess file, widget), enctype) = do
  message <- return $ (fileContentType $ file1 file) `append` "  " `append` (fileName $ file1 file)
  defaultLayout $ do
    setTitle "File"
    $(widgetFile "file")
processForm ((_, widget), enctype) = do
  message <- return ("no file." :: String)
  defaultLayout $ do
    setTitle "File"
    $(widgetFile "file")


HTMLテンプレートの例。

<form method=post action=@{FileR} enctype=#{enctype}>
  ^{widget}
  <input type=submit>

<strong>#{message}

分かればかんたん。

警告を消す・Warning: A do-notation statement discarded a result of type ...

do記法の中での警告

Yesodを書いているとdo記法をたくさん使うのですが、ときどき以下のような警告に遭遇します。

Handler/Root.hs:36:7:
    Warning: A do-notation statement discarded a result of type <型情報>.
             Suppress this warning by saying "_ <- ($)
                                                     <コード片>,
             or by using the flag -fno-warn-unused-do-bind

「do記法の中の文が結果を戻してるのに変数で受けてませんよ」という意味のようですね。

文の結果がいらない、というのはよくあることだと思うのでこの警告は少々うるさく感じます。

警告を消すには、警告文にある通り

_ <- hogehoge...

という具合に _ で受けてあげればいいのですが、数が多くなってくると面倒ですねぇ。Haskellらしいと言えばらしいのですが。

警告文が示しているもう1つの解決方法は、コンパイルオプションを追加して警告を出さなくする、というものです。
Yesodの場合、これはcabalファイルにて指定します。

ghc-options:   -Wall -threaded -O0

というところに追記して

ghc-options:   -Wall -threaded -O0 -fno-warn-unused-do-bind

とします。

なお上記に該当する行は複数あるのですが、開発中はlibraryというセクション(という呼び名でいいのかな?)の方に追記する必要があります。

どちらの解消方法がいいのか

当面は _ で受ける方法で書くことにします。
この書き方には「この文の結果は使わない!」と強く表明する効果があるからです。
数が多くなってくると面倒ですが、個人で開発している分にはあまり大したことにはならないでしょう。

規模が大きい開発では、警告があまりたくさん出ると真に大事な警告が埋もれてしまう可能性が出てくるので、そんなときはコンパイルオプションを使うといいと思います。
「そもそもたくさん警告が出る状態がどうなんだ?」という議論はありますが、現実としてそれはよくあることですので・・・

Yesodにはまってると思いきやHaskellの文法にはまっていた

よくあるっちゃーよくあるんですが、Yesodを書いていてYesodの作法を理解していなくてはまっているのかと思いきや、実はHaskellの文法にやられていたという。

今回はこんなエラーが。

Handler/Bbs.hs:7:19:
    The first argument of ($) takes one argument,
    but its type `GGWidget master0 m0 ()' has none
    In the second argument of `($)', namely
      `do { setTitle "Bbs Index" } $ (widgetFile "bbs")'
    In the expression:
        defaultLayout $ do { setTitle "Bbs Index" } $ (widgetFile "bbs")
    In the expression:
      do { defaultLayout
         $   do { setTitle "Bbs Index" } $ (widgetFile "bbs") }
Starting development server...

dist/devel.hs:3:1:
    Failed to load interface for `Application':
      it is not a module in the current program, or in any known package.
Exit code: ExitFailure 1

エラーが出ている関数がこちら。

getBbsR :: Handler RepHtml
getBbsR = do
  defaultLayout $ do
    setTitle "Bbs Index"
    $ (widgetFile "bbs")

この関数の修正版がこちら。

getBbsR :: Handler RepHtml
getBbsR = do
  defaultLayout $ do
    setTitle "Bbs Index"
    $(widgetFile "bbs")

ちがいが分かりますか?
最終行の $ の後ろに空白があるかないかのちがいなんです。

なんでこれでエラーになるのか、理解できません。Haskellで、インデントの付け方がまずくてエラーになるのはよくあるのですが、空白の有無で意味が変わるなんてのは初めて。ましてや $ の引数を () で囲んで明示しているのに。

でも理解できなくても先に進むことはできます。当面、理解は置いておいて、前進!

その後・・・

その後、このエントリの一つ前のエントリでリンクした「できる!Template Haskell (完)」にきちんと書いてありました。

合成した構文木を埋め込むには、 $( ) で囲めばよいのです。

http://haskell.g.hatena.ne.jp/mr_konn/20111218/1324220725

そうだったのか!$っていう関数を適用してるのかと思ってたから理解できなかったんだな。なっとく。

ちなみに$()ってのは関数ではなくてTemplateHaskellにおける記法のようです。

Template Haskellとクォートの衝撃

こんなエントリを見付けました。
できる!Template Haskell (完)

最近Yesodを触っていて見慣れない構文にとまどっていたのですが、多くの部分に対する回答を得ることが出来ました。

Template Haskellとクォートの衝撃。

言語の中に言語を埋める。

Haskellの用途を大きく広げる、すごい仕組みだと思う!!

Yesodで入力フォーム

Yesod Web Framework Bookの写経を始めて3日目くらい。
ついに入力フォームに至ったんですがここのサンプルを写経すると私の環境では型エラーが起きました。ここまでは順調だったんですけどね。

ちなみに私の環境のYesodのバージョンは以下の通りです。

$ ghc-pkg list | grep yesod
    yesod-0.9.3.1
    yesod-auth-0.7.5
    yesod-core-0.9.3.3
    yesod-default-0.4.0
    yesod-form-0.3.3
    yesod-json-0.2.1
    yesod-persistent-0.2.1
    yesod-static-0.3.1.2

Yesod Web Framework Bookオリジナルのコードはこちら。

personForm :: Html -> MForm Synopsis Synopsis (FormResult Person, Widget)

MFormがないって怒られてしまうのでFormにします。

personForm :: Html -> Form Synopsis Synopsis (FormResult Person, Widget)

これでいける。

さてFormを作るpersonForm関数の全文は以下の通りです。

personForm :: Html -> Form Synopsis Synopsis (FormResult Person, Widget)
personForm = renderDivs $ Person
    <$> areq textField "Name" Nothing
    <*> areq (jqueryDayField def
        { jdsChangeYear = True -- give a year dropdown
        , jdsYearRange = "1900:-5" -- 1900 till five years ago
        }) "Birthday" Nothing
    <*> aopt textField "Favorite color" Nothing
    <*> areq emailField "Email address" Nothing
    <*> aopt urlField "Website" Nothing

areqは入力必須の項目、aoptは任意項目みたいですね。

jQueryと連携していてなかなかリッチ画面が出来上がります。

<$>とか<*>、それからどうやってPersonと結び付けているのか、とか疑問が山盛りですが写経するときはあまり気にしないようにして先に進むようにしています。

Yesod BookのFormはけっこう大きいトピックなのでしばらくかかりそうですが、次の次に控えるのがPersistence。
ここは楽しみなので頑張ってここを乗り越えよう。

SAStrutsの落とし穴

たまには実践的(笑)なエントリを。

SAStrutsは業務でかなり扱ってるのですが、Actionにははまりどころがあります。それを紹介。

getで始まるメソッドを作らない

これ超大事です。

SAStrutsJSPレンダリングするとき、ActionFormとActionのプロパティとpublicフィールドの値をMapに詰め込みます。こうすることでpublicフィールドをJavaBeansのプロパティのような感覚で扱うことが出来るわけです。

でもこの挙動は「意図せずActionのメソッドが呼び出されてしまう」という事態につながります。

特にActionに対応するActionFormを作って@ActionFormでインジェクトしている場合が要注意です。
開発者がわざわざActionForm作るってことは、「JSPレンダリングする情報はActionではなくてActionFormに全部持たせよう!」と思ってるはずで、まさかActionがJSPでのレンダリング対象になっているとは思わないです。

なのでうっかりActionに「getXxx」なんてメソッドを作ってしまうとはまってしまいます。

この挙動ははっきり言っていやです。
「URLは名詞であるべき」というREST風な考え方からするとgetで始まる名前がそもそもおかしいと言えないこともないですが、SAStrutsの「Actionのメソッドの名前がそのままURLになる」という性質上、Javaの常識に照らせばメソッド名が動詞始まりになるのは自然なことです。なので「get」という動詞が使われるのも、ごく自然。
なのにこのようなメソッドを作ってしまうと意図しない動作を引き起こしてしまいます。

ActionFormがあるときはActionからはプロパティ集めないとか、@Executeが付いたgetterは対象外にするとか、何らかの措置をしてほしかったなぁ、と思います。