Yesodのmodelsに独自定義した型を使いたいときには

Yesod Advent Calendar 2016|18日目です。

Yesodは型安全を追求した実にHaskellらしいWebアプリケーションフレームワークですが、DBアクセスにも型安全が徹底されています。

DBに格納するモデルを定義するのが ``` config/models ``` ファイルです。
stackのテンプレートを使って作った models ファイルは次のようになっています。

```
User
    ident Text
    password Text Maybe
    UniqueUser ident
    deriving Typeable
Email
    email Text
    userId UserId Maybe
    verkey Text Maybe
    UniqueEmail email
Comment json -- Adding "json" causes ToJSON and FromJSON instances to be derived.
    message Text
    userId UserId Maybe
    deriving Eq
    deriving Show

 -- By default this file is used in Model.hs (which is imported by Foundation.hs)
```

nullになり得るフィールドには ```Maybe```と書かれて、実にHaskellらしい定義ファイルだと思います。
フィールドの型としては、上記で使われている```Text```の他に```Int```や```ByteString```、```Textarea```などが使えます。

さてフィールドの型には独自に定義した型が使えるので、方法を紹介します。

ここではサンプルとしてRDBのインスタンスを管理するモデルを作ってみます。
modelsファイルに以下のような定義を追加するとしましょう。

```
DbInstance
    type DbType
```

``` DbType ```というのが独自の型です。


# 型を定義する
## 定義する場所
独自の型を ``` Model.hs ``` に書いてはいけません。TemplateHaskellの制限に引っかかり、コンパイルに失敗します。
ですので別ファイルを作らなければいけません。
ここでは、``` ModelDef.hs ```というファイルを作ることにします。

## ModelDef.hs

```ModelDef.hs
module ModelDef where

import GHC.Read
import GHC.Show

import Database.Persist.TH (derivePersistField)

data DbType = MySQL | PostgreSQL
  deriving (Show, Read)
derivePersistField "DbType"
```

ポイントは ``` derivePersistField ``` です。
modelsファイルで使う型は ``` PersistField ```という型クラスのインスタンスでなければいけません。しかしこれは面倒である程度定型的なプログラムになります。

そこで``` derivePersistField ```関数を使うと、**いい感じに**処理してくれます。かなり助かります。


# Model.hsに一工夫

 ``` ModelDef.hs ``` に定義した型を扱いやすくするために、``` Model.hs ``` に一工夫を加えましょう。
具体的には、``` ModelDef ``` モジュールを ``` Model ```モジュールとして公開します。

```Model.hs
module Model where
```

という箇所を以下のように書き換えます。

```Model.hs
module Model (
  module Model
  , module ModelDef
) where

import ModelDef
```

これで、ModelDefモジュールで定義された型をModelモジュールの型として扱えるようになります。

ここに紹介したサンプルは本当に基本的な物で、例えば独自に定義した型をJSONで扱えるようにする時は、もう少し記述が必要になります。

それはまたの機会にご紹介します。

# cabalファイルにModelDefを追記

(2016.12.26追記）

これを忘れるととても怖いエラーになるのでした。

```
/vagrant/model-example/.stack-work/dist/x86_64-linux/Cabal-1.22.5.0/build/libHSmodel-example-0.0.0-IYcBfABB7oy20gCSpTfhLB.a(Model.o):(.text+0xa02a): undefined reference to `modelzuIYcBfABB7oy20gCSpTfhLB_ModelDef_zdfPersistFieldDbType6_closure'
/vagrant/model-example/.stack-work/dist/x86_64-linux/Cabal-1.22.5.0/build/libHSmodel-example-0.0.0-IYcBfABB7oy20gCSpTfhLB.a(Model.o):(.text+0xa035): undefined reference to `modelzuIYcBfABB7oy20gCSpTfhLB_ModelDef_zdfPersistFieldDbType1_closure'
/vagrant/model-example/.stack-work/dist/x86_64-linux/Cabal-1.22.5.0/build/libHSmodel-example-0.0.0-IYcBfABB7oy20gCSpTfhLB.a(Model.o):(.text+0x1697f): undefined reference to `modelzuIYcBfABB7oy20gCSpTfhLB_ModelDef_zdfPersistFieldDbType_closure'
/vagrant/model-example/.stack-work/dist/x86_64-linux/Cabal-1.22.5.0/build/libHSmodel-example-0.0.0-IYcBfABB7oy20gCSpTfhLB.a(Model.o):(.text+0x15982): undefined reference to `modelzuIYcBfABB7oy20gCSpTfhLB_ModelDef_zdfPersistFieldDbTypezuzdcfromPersistValue_info'
/vagrant/model-example/.stack-work/dist/x86_64-linux/Cabal-1.22.5.0/build/libHSmodel-example-0.0.0-IYcBfABB7oy20gCSpTfhLB.a(Model.o):(.data+0x3c78): undefined reference to `modelzuIYcBfABB7oy20gCSpTfhLB_ModelDef_zdfPersistFieldDbType1_closure'
/vagrant/model-example/.stack-work/dist/x86_64-linux/Cabal-1.22.5.0/build/libHSmodel-example-0.0.0-IYcBfABB7oy20gCSpTfhLB.a(Model.o):(.data+0x3c80): undefined reference to `modelzuIYcBfABB7oy20gCSpTfhLB_ModelDef_zdfPersistFieldDbType6_closure'
/vagrant/model-example/.stack-work/dist/x86_64-linux/Cabal-1.22.5.0/build/libHSmodel-example-0.0.0-IYcBfABB7oy20gCSpTfhLB.a(Model.o):(.data+0x3e58): undefined reference to `modelzuIYcBfABB7oy20gCSpTfhLB_ModelDef_zdfPersistFieldDbTypezuzdcfromPersistValue_closure'
/vagrant/model-example/.stack-work/dist/x86_64-linux/Cabal-1.22.5.0/build/libHSmodel-example-0.0.0-IYcBfABB7oy20gCSpTfhLB.a(Model.o):(.data+0x3e98): undefined reference to `modelzuIYcBfABB7oy20gCSpTfhLB_ModelDef_zdfPersistFieldDbType_closure'
collect2: error: ld returned 1 exit status
Progress: 0/2
--  While building package model-example-0.0.0 using:
      /home/vagrant/.stack/setup-exe-cache/x86_64-linux/setup-Simple-Cabal-1.22.5.0-ghc-7.10.3 --builddir=.stack-work/dist/x86_64-linux/Cabal-1.22.5.0 build lib:model-example exe:model-example test:test --ghc-options " -ddump-hi -ddump-to-file"
    Process exited with code: ExitFailure 1
```

なんじゃこりゃ。

このエラーはcabalファイルに ``` ModelDef ``` モジュールを追記し忘れていると起こります。

(修正前)

```
library
    hs-source-dirs: ., app
    exposed-modules: Application
                     Foundation
                     Import
                     Import.NoFoundation
                     Model
                     Settings
                     Settings.StaticFiles
                     Handler.Common
                     Handler.Home
                     Handler.Comment
                     Handler.Profile
```

(修正後)

```
library
    hs-source-dirs: ., app
    exposed-modules: Application
                     Foundation
                     Import
                     Import.NoFoundation
                     Model
                     ModelDef  <<<< ここを追記
                     Settings
                     Settings.StaticFiles
                     Handler.Common
                     Handler.Home
                     Handler.Comment
                     Handler.Profile
```