OverloadedLabels と Haskell Relational Record

Haskell (その2) Advent Calendar 2017 の 18日目のエントリーです。

OverloadedLabels のレコードでの利用

GHC 8.0.1 以降では OverloadedLabels 拡張がサポートされたことにより、 適切な定義を追加しておくことで、レコードのフィールド名の衝突を気にせずに利用できるようになりました。

以下 user's guide の example のコードです。

{-# LANGUAGE DataKinds, KindSignatures, MultiParamTypeClasses,
             FunctionalDependencies, FlexibleInstances,
             OverloadedLabels, ScopedTypeVariables #-}

import GHC.OverloadedLabels (IsLabel(..))
import GHC.TypeLits (Symbol)

data Label (l :: Symbol) = Get

class Has a l b | a l -> b where
  from :: a -> Label l -> b

data Point = Point Int Int deriving Show

instance Has Point "x" Int where from (Point x _) _ = x
instance Has Point "y" Int where from (Point _ y) _ = y

instance Has a l b => IsLabel l (a -> b) where
  fromLabel _ x = from x (Get :: Label l)

example = #x (Point 1 2)

IsLabel クラスのインスタンスを型レベルの文字列 "x" に対して実装することで、 ここでは #x というフィールドを取り出す関数名を衝突を気にせずに利用することができます。

OverloadedLabels の Haskell Relational Record での利用

IsLabelインスタンスは型レベル文字列 x と型 a に対して定義します。

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

この定義を良く見ると気がつきますが、 型レベル文字列と任意の型に対するインスタンスを定義することができます。

レコードでの利用の場合は、型レベル文字列とレコードフィールドを取り出す関数に対して定義を行なっているということです。

Haskell Relational Record では型レベル文字列と Pi a b (レコードからの射影取り出しに利用するラベル) の型に対して定義することで、 射影取り出しのラベルを衝突を気にせずに利用することができます。

Haskell Relational Record でのコード例は例えば以下のようになります。

windowRankByGroup :: Relation () ((Int64, Maybe Int32), (Maybe String, Maybe String))
windowRankByGroup =  relation $ do
  u <- query user
  m <- query membership
  on $ #id u .=. #userId m
  g <- query group
  on $ #id g .=. #groupId m

  let gwindow = do partitionBy $ (! #id) g -- g ! Group.id'
                   asc $ #name u

  return (rank `over` gwindow
          ><
          sum' (#id u) `over` gwindow
          ><
          (#name u
           ><
           #name g))

ラベル #id#name が、衝突を気にせずに利用できていることがわかります。