Skip to the content.

拡張可能レコード

標準のレコードが持つ問題点を解決したいくつかのデータ構造・ライブラリ

標準のレコードが持つ問題点

出典: http://nikita-volkov.github.io/record/

  1. 名前問題が解決できていない。つまり同じモジュールの中で同じフィールド名をもつ2つのレコードを定義できない。例えば以下はコンパイルできない。
data A = A { field :: String }
data B = B { field :: String }
  1. 部分的である。以下のコードはコンパイル時に何も警告しないのにランタイム時にエラーになる。
data A = A1 { field1 :: String } | 
         A2 { field2 :: String }

main = print $ field1 $ A2 "abc"
  1. 型コンストラクタ間で違う型を持つ同じ名前のフィールドを定義することが出来ない。
data A = A1 { field :: String } | 
         A2 { field :: Int }
  1. ネストしたレコードの値を更新することは指数関数的に面倒くさくなる
addManStk team = team {
  manager = (manager team) {
    diet = (diet (manager team)) {
      steaks = steaks (diet (manager team)) + 1
    }
  }
}

OverloadedRecordFields

GHCの言語拡張でレコードの問題を解決しようという試み

すべての機能が入るまでの道のりは3ステップに分けられる

Step1. DuplicateRecordFields

同じモジュール内で同じフィールド名をもつレコードを定義できるようにする言語拡張

data Person  = MkPerson  { personId :: Int, name :: String }
data Address = MkAddress { personId :: Int, address :: String }

GHC 8.0 で既に実装されている

Step2. OverloadedLabels

fromLabel @"field" @alpha proxy# の糖衣構文として #field のような書き方ができるようになる。 fromLabel は以下の型クラス IsLabel のメソッド。

class IsLabel (x :: Symbol) a where
  fromLabel :: Proxy# x -> a

具体的に IsLabel のインスタンスが定義されるわけではないので OverloadedLabels だけでは使いみちは少ないが将来的にレコードが宣言されると適切なインスタンスが自動的に生成されることを予定している。

GHC 8.0 で既に実装されている

Step3. Magic type classes

~GHCがコンパイル時にレコードの宣言を見つけると以下のような型クラスのインスタンスを作るようになる。~

-- | HasField x r a はrが型aのフィールドxを持つレコードであることを意味している
class HasField (x :: Symbol) r a | x r -> a where
  -- | レコードからフィールドを取り出す
  getField :: Proxy# x -> r -> a

-- | UpdateField x s t bはsが型tのレコードに型bの値をセットできる
--   フィールドxを持つレコードの型であることを意味している
class UpdateField (x :: Symbol) s t b | x t -> b, x s b -> t where
  -- | フィールドをレコードにセットする
  setField :: Proxy# x -> s -> b -> t

例えば

data T = MkT { x :: Int }

のようなレコードが定義されると

instance HasField "x" T Int where
  getField _ = x

instance UpdateField "x" T T Int where
  setField _ (MkT _) x = MkT x

のようなインスタンスが宣言される。

IsLabel とは以下のようなインスタンスによって結びついている。

instance (HasField x r a) => IsLabel x (r -> a) where
  fromLabel = getField (proxy# :: Proxy# x)

ライブラリ

レコードのように扱える独自のデータ構造を使って標準のレコードが持つ問題を解決しようという試み

Vinyl

Extensible records for Haskell with lenses.

record

レコードを表す独自の文法を定義しコンパイル時のプリプロセッサ record-preprocessor を走らせることを前提としている。

Example


-- Uses the Strict Record syntax.
type Person =
  {! 
    name :: String, 
    birthday :: {! year :: Int, month :: Int, day :: Int }, 
    country :: Country
  }

-- Uses the Lazy Record syntax.
type Country =
  {~
    name :: String,
    language :: String
  }

Bookkeeper

GHCが OverloadedLabels 拡張までを提供していることを前提に作られたレコードライブラリ。Template Haskellを使っていないのも特徴。

Example

type Person = Book '[ "name" :=> String, "age" :=> Int ]

julian :: Person
julian = set #name "Julian K. Arni" $ set #age 28 $ emptyBook

-- もしくは

julian :: Person
julian = emptyBook
       & #age =: 28
       & #name =: "Julian K. Arni"

-- > get #name julian
-- "Julian K. Arni"

rawr

GHCが OverloadedLabels 拡張までを提供していることを前提に作られたレコードライブラリ。TemplateHaskellを使っていないのも特徴。

type Foo = R ( "a" := Int, "b" := Bool )

foo :: Foo
foo = R ( #a := 42, #b := True )

-- > #a foo
-- 42

-- > foo ^. #a
-- 42

-- > -- Extensible Records
-- > R ( #foo := True ) :*: R ( #bar := False )
-- R ( bar := False, foo := True )

Currently, records with up to 8 fields are supported.

という制約がある(参考)

extensible

{-# LANGUAGE OverloadedLabels #-}

type Animal = Record
  [ "name" :> String
  , "collective" :> String
  , "cry" :> Maybe String
  ]

swan :: Animal
swan = #name @= "swan"
  <: #collective @= "lamentation"
  <: #cry @= Nothing
  <: nil

-- > swan ^. #name
-- "swan"

Others

理論