HaskellでData.Csv(cassava)を使ってcsvファイルを処理する

# やりたいこと
- 入力はcsvファイル
- そのうちの一部の列の値を使って、別のファイル（例：SQL）を出力したい
- csvファイルはレイアウトの変更が有り得るので多少効率が悪くても変更に柔軟に追随できるようにしておきたい
- 仕事で無理やりHaskellを使いたい

# 方針
- Haskellのcsvライブラリであるcassavaを使う
- cassavaは1行目をヘッダーとして列名で値を取り扱うことができる関数が用意されているので、そちらを使う
- なお、cassavaはデータ型をcsvに変換するエンコードと、csvをデータ型に変換するデコードの両方に対応しているが、今回はデコードに特化した記事とする
- 以降のコードはData.Csvのドキュメントにあるサンプルを元にしている
https://hackage.haskell.org/package/cassava

# Data.Csv
前提知識としてData.Csvの型定義のうちデコードに必要なものを見る。
内部のデータ構造としては主に`Vector`が使われている。`Vector`は定数時間でアクセスできる一次元配列とのこと。
ここが理解できなくとも実例が何となく分かれば問題はないので読み飛ばして良いが、分かっていた方が躓いたときに対応しやすい。

``` Data.Csv.Types.hs
import qualified Data.ByteString as S
import qualified Data.HashMap.Strict as HM
import Data.Vector (Vector)
import qualified Data.Vector as V

-- 単一フィールドはByteString
type Field = S.ByteString
-- レコードはCSVファイルの1行。FieldのVector
type Record = Vector Field
-- CSVデータはByteStringのVectorのVector
type Csv = Vector Record

-- ヘッダーは１つ以上の名前があり、名前はその後ろに続くデータのラベルである
-- 名前はByteString。実体はFieldと等価
type Name = S.ByteString
-- ヘッダーはCSVファイルの1行目。全てのCSVファイルにヘッダーがあるわけではない
-- ヘッダーはNameのVector。実体はRecordと等価
type Header = Vector Name
-- HeaderとRecordをzipしてtoListしてhashMap化する
toNamedRecord :: Header -> Record -> NamedRecord
toNamedRecord hdr v = HM.fromList . V.toList $ V.zip hdr v

-- NamedRecordは列名でindex化されたCSVファイルの1行に対応する
-- NameRecordはByteStringの正格なHashMap
type NamedRecord = HM.HashMap S.ByteString S.ByteString
```
### ヘッダーの情報を使って変換する
ヘッダーを参照して、CSVレコードをユーザー定義のデータ型に変換する。
フィールドの名前はヘッダー情報を使って定義する。ヘッダーとはファイルの最初の行のこと。
ヘッダー情報を使った変換はファイル構造の変更 (列の並べ替えや追加など) に対してより堅牢だが多少時間はかかる。


まず、変換に使用するための値を保持する独自型（この例では`Person`)を定義する。
独自型は`FromNamedRecord`のインスタンスにする必要があるが、ここでは`GHC.Generics`を使って自動導出している（もちろん独自定義も可能。後述の実例では自動導出はしていない）

``` .hs
{-# LANGUAGE DeriveGeneric #-}
import Data.Text    (Text)
import GHC.Generics (Generic)
data Person = Person { name :: !Text 
                     , salary :: !Int }
    deriving (Generic, Show)

instance FromNamedRecord Person
```
### FromNamedRecord型
メソッドは`parseNamedRecord`で、HashMapである`NamedRecord`から独自型のparserを得る


``` .hs
parseNamedRecord :: NamedRecord -> Parser a
```

### decode

``` .hs
decodeByName
  :: FromNamedRecord a
  -> ByteString	-- CSV data
  -> Either String (Header, Vector a)
```
- `ByteString`のCSVレコードを効率的に`Vector`にデコードする
- 入力が不完全または無効なために失敗した場合は`Left msg`が返される
- `decodeByName`、`decodeByNameWith`（後述）はデータの前にヘッダーがある形式が前提


単一のCSVレコードから変換することができるが、変換に失敗する可能性がある。
たとえばRecordのカラム数が間違っている場合などでは、empty、mzero、またはfailを使用して変換を失敗させる。

`Person`型を`FromNamedRecord`のインスタンスにしたことで`decodeByName`を使用してcsvデータをデコードできるようになった。

``` .hs
-- 入力csv
-- name,salary
-- John,27
>>> decodeByName "name,salary\r\nJohn,27\r\n" :: Either String (Header, Vector Person)
Right (["name","salary"]
      ,[Person {name = "John"
              , salary = 27}]
       )
```

# 実例
### 入力ファイル
例として次のデータを使用する。

``` salaries.tsv
Namee　dummy　Salarye
"John ,Doe"　tmp　50000
"Jane ,Doe"　tmp　60000
```
- ファイルはタブ区切りのTSV形式
- 二列目は変換には不要な列である
- 列名は本来使用したいフィールド名(name,salary)と意識的に変えてある

### データ保持のための自作型とインスタンスの定義

``` .hs
{-# LANGUAGE OverloadedStrings #-}
import Data.Csv
data Person = Person
    { name   :: !String
    , salary :: !Int
    }

instance FromNamedRecord Person where
    parseNamedRecord r = Person <$> r .: "Namee" 
                                <*> r .: "Salarye"
-- (.:) :: FromField a => NamedRecord -> ByteString -> Parser a
-- Alias for lookup
-- 指定したレコードのフィールドを名前で取得
```
- `ByteString`値を文字列リテラルとして書くために`OverloadedStrings`言語拡張を使用
- 自作型は入力ファイルの形式と一致している必要がない。後続処理に必要な列のみ受けられるようになっていれば良い（dummy列は`Person`型に含めていない）
- `parseNamedRecord`は自作型のフィールドラベルではなく、入力csvのヘッダーの値を指定する。
- ヘッダーを指定することで得られた値を自作型の型コンストラクタに引数として引き渡す。


### データ変換
``` csvTest1.hs
{-# LANGUAGE OverloadedStrings #-}
import qualified Data.ByteString.Lazy as BL
import Data.Csv
import qualified Data.Vector as V
import Data.Char(ord)
main :: IO ()
main = do
    csvData <- BL.readFile "tabTest.tsv"
    case decodeByNameWith myOptions csvData of
        Left err -> putStrLn err
        Right (_, v) -> V.forM_ v $ \ p ->
              putStrLn $ mkMsg p

mkMsg :: Person -> String
mkMsg p =   name p 
         <> " earns " 
         <> show (salary p) 
         <> " dollars"

myOptions = defaultDecodeOptions {
      decDelimiter = fromIntegral (ord '\t')
    }
```
- この例では入力ファイルがtab区切りなので、`decodeByNameWith`関数を使いデリミターをオプションとして指定している（myOptions)
- `mkMsg`関数が変換処理の実体。自作型から値を取り出して処理する。

### 全体

``` 全体.hs
{-# LANGUAGE OverloadedStrings #-}
import qualified Data.ByteString.Lazy as BL
import Data.Csv
import qualified Data.Vector as V
import Data.Char(ord)
data Person = Person
    { name   :: !String
    , salary :: !Int
    }
instance FromNamedRecord Person where
    parseNamedRecord r = Person <$> r .: "Namee" 
                                <*> r .: "Salarye"
mkMsg :: Person -> String
mkMsg p =   name p 
         <> " earns " 
         <> show (salary p) 
         <> " dollars"

main :: IO ()
main = do
    csvData <- BL.readFile "tabTest.csv"
    case decodeByNameWith myOptions csvData of
        Left err -> putStrLn err
        Right (_, v) -> V.forM_ v $ \ p ->
              putStrLn $ mkMsg p

myOptions = defaultDecodeOptions {
      decDelimiter = fromIntegral (ord '\t')
    }
```

``` 実行結果.hs
> stack runghc csvTest1.hs
John ,Doe earns 50000 dollars
Jane ,Doe earns 60000 dollars
```