yesodで全てのモデルにcreatedAt,updatedAtを作りたかった話

背景

User
    email     Text
    name      Text
    createdAt UTCTime
    updatedAt UTCTime

のように,それぞれのモデルに生成時間と更新時間を付けたい.

  • 役に立つことがあるかもしれない
  • 全てのモデルにつけることで一貫性を保ちたい

問題

単純にFormを

userForm :: Form User
userForm = renderBootstrap3 BootstrapBasicForm $ User <$>
    areq emailField "email" Nothing <*>
    areq textField "name" Nothing <*>
    lift (liftIO getCurrentTime) <*>
    lift (liftIO getCurrentTime) <*
    bootstrapSubmit ("submit" :: BootstrapSubmit Text)

のように作ってしまうと,当然getCurrentTimeアクションが2回実行されるので,生成時間と更新時間がズレてしまいます.

誤った解決法

formの型をForm Userを,Form (UTCTime -> UTCTime -> User)とします.

これはformの型の長さが際限なく増えていき,FileInfoなどが絡むと可読性が極めて悪くなるので,やめるべきであると結論づけました.

簡単な解決法

単純なコンストラクタであるUserを使うのではなく,ラムダ式を書いて1つのUTCTimeを2つのフィールドに格納してUserを合成します.

しかし,これも場当たり的な対処であり,モデルが増えてフォームが増えていくと書くのが段々面倒くさくなっていくという問題があります.

統一的な解決法

現在私はこのような関数を記述して使用することにしています.

-- | Formで`getCurrentTime`を2回実行しないで済むためのコンストラクタ生成装置
withCurrentTime :: (Applicative (t m), MonadTrans t, MonadIO m) =>
    t m (UTCTime -> UTCTime -> a) -> t m a
withCurrentTime ctor = (\c t -> c t t) <$> ctor <*> lift (liftIO getCurrentTime)

この関数を使えば,単純なコンストラクタからcreatedAt, updatedAtを必要としないコンストラクタが解決できます.

真の問題

そもそも,全てのモデルにcreatedAt, updatedAtは必要でしょうか?生成日は必要なことが多いでしょうが,更新日時は必要性が疑問なモデルも多いです.追記型であるpostgresqlとの相性も悪い.

railsやdjangoのように,勝手にフィールドを補完してくれるオプションがあるならば,プログラムが一貫性を保ちますが,yesodはそういったことは行いません.

それには,haskellの型がrubyやpythonの型とは大きく性質が異なることも影響していると考えています.デフォルトでnullではないことや,要素を動的に増やすことが出来ないことですね.

いくら一貫性を保ちたいからと言って,合理性無くフィールドを増やすのは,haskellプログラミングにおいては避けるべきであると考えるようになりました.haskellでプログラミングを行うならば,データ型の要素に関しては「それは本当に必要なの?」と考えるべきです.

更新時間が必要なモデルのほうが特殊なのであり,この件に関しては一貫性よりも合理性を重視するべきでした.

私のデータベース設計は稚拙であり,きちんとRDBと型について学習し直すべきでした.

早くこのツイートを読みたかった.