Модуль GHC.Records определяет следующее:
class HasField (x :: k) r a | x r -> a where getField :: r -> a
Ограничение HasField x r a представляет собой утверждение о том, что x является полем типа a, принадлежащим типу записи r. Метод getField возвращает функцию-селектор записи.
Это позволяет определять функции, полиморфные по типам записей с заданным полем. Например, следующий код работает с любым типом записи, содержащим поле name :: String:
foo :: HasField "name" r String => r -> String foo r = reverse (getField @"name" r)
HasField — это магический встроенный класс типов (например, подобно Coercible). Решатель ограничений обрабатывает его особым образом (см. Решение ограничений HasField). Пользователи также могут определять собственные экземпляры HasField (см. Виртуальные поля записей).
6.5.9.1. Решение ограничений HasField
Если решатель ограничений встречает ограничение HasField x r a, в котором r — конкретный тип данных с находящимся в области видимости полем x, он автоматически решит это ограничение, используя селектор поля в качестве словаря и при необходимости унифицируя a с типом поля. Это происходит независимо от того, какие расширения включены.
Например, если в области видимости находится следующий тип данных
data Person = Person { name :: String }
результат будет примерно таким же, как если бы существовал экземпляр
instance HasField "name" Person String where getField = name
за исключением того, что этот экземпляр фактически нигде не генерируется: ограничение решается непосредственно решателем ограничений.
Чтобы соответствующее ограничение HasField могло быть решено, поле должно находиться в области видимости. Благодаря этому сохраняется существующий механизм сокрытия представления: модуль может не экспортировать поле, не позволяя клиентским модулям напрямую обращаться к нему или обновлять его.
Решение ограничений HasField зависит от функций-селекторов полей, генерируемых для каждого определения типа данных:
-
Если у поля записи нет функции-селектора, поскольку его тип позволил бы экзистенциальной переменной выйти за пределы своей области видимости, соответствующее ограничение
HasFieldрешено не будет. Например,{-# LANGUAGE ExistentialQuantification #-} data Exists t = forall x . MkExists { unExists :: t x }не приводит к появлению селектора
unExists :: Exists t -> t x, поэтому ограничениеHasField "unExists" (Exists t) aне будет решено автоматически. -
Если поле записи имеет полиморфный тип (и, следовательно, функция-селектор имеет более высокий ранг), соответствующее ограничение
HasFieldрешено не будет, поскольку это нарушило бы функциональную зависимость поHasFieldи/или потребовало бы импредикативности. Например,{-# LANGUAGE RankNTypes #-} data Higher = MkHigher { unHigher :: forall t . t -> t }приводит к появлению селектора
unHigher :: Higher -> (forall t . t -> t), но не позволяет решить ограничениеHasField "unHigher" Higher a. -
GADT записи может иметь ограниченный тип функции-селектора, что при решении ограничений
HasFieldможет привести к дополнительной унификации. Например,{-# LANGUAGE GADTs #-} data Gadt t where MkGadt :: { unGadt :: Maybe v } -> Gadt [v]приводит к появлению селектора
unGadt :: Gadt [v] -> Maybe v, поэтому решатель сведёт ограничениеHasField "unGadt" (Gadt t) bк унификацииt ~ [v]иb ~ Maybe vдля некоторой новой метапеременнойv— примерно так же, как если бы существовал экземплярinstance (t ~ [v], b ~ Maybe v) => HasField "unGadt" (Gadt t) b
-
Если тип записи имеет традиционный контекст типа данных, ограничение
HasFieldбудет сведено к решению ограничений из контекста. Например,{-# LANGUAGE DatatypeContexts #-} data Eq a => Silly a = MkSilly { unSilly :: a }приводит к появлению селектора
unSilly :: Eq a => Silly a -> a, поэтому решатель сведёт ограничениеHasField "unSilly" (Silly a) bкEq a(и унифицируетaсb) — примерно так же, как если бы существовал экземплярinstance (Eq a, a ~ b) => HasField "unSilly" (Silly a) b
Пример использования решения ограничений HasField для реализации «синтаксиса точечной записи» см. в разделе Перегруженная точечная запись.
6.5.9.2. Виртуальные поля записей
Пользователи могут определять собственные экземпляры HasField, если они не конфликтуют со встроенным механизмом решения ограничений. Это позволяет определять «виртуальные» поля записей для типов данных, в которых таких полей изначально нет.
Например, этот экземпляр позволит обращаться к полю name типа Person также с помощью #fullname:
instance HasField "fullname" Person String where getField = name
Более того, библиотека анонимных записей могла бы предоставлять экземпляры HasField для своих анонимных записей и тем самым быть совместимой с полиморфными селекторами записей, представленными в этом предложении. Например, нечто подобное позволяет использовать getField для доступа к значениям Record, если в списке полей на уровне типов присутствует соответствующая строка:
data Record (xs :: [(k, Type)]) where
Nil :: Record '[]
Cons :: Proxy x -> a -> Record xs -> Record ('(x, a) ': xs)
instance {-# OVERLAPPING #-} HasField x (Record ('(x, a) ': xs)) a where
getField (Cons _ v _) = v
instance HasField x (Record xs) a => HasField x (Record ('(y, b) ': xs)) a where
getField (Cons _ _ r) = getField @x r
r :: Record '[ '("name", String) ]
r = Cons Proxy "R" Nil
x = getField @"name" r
Поскольку такие представления могут поддерживать метки полей с видами, отличными от Symbol, класс HasField является поливидовым (хотя встроенное решение ограничений работает только для вида Symbol). В частности, это позволяет пользователям объявлять метки полей с ограниченной областью видимости, как в следующем примере:
data PersonFields = Name s :: Record '[ '(Name, String) ] s = Cons Proxy "S" Nil y = getField @Name s
Чтобы избежать конфликтов со встроенным механизмом решения ограничений, запрещены следующие пользовательские экземпляры HasField (в дополнение к обычным правилам, например запрету на использование семейств типов в заголовках экземпляров):
-
HasField _ r _, еслиrявляется переменной; -
HasField _ (T ...) _, еслиTявляется семейством данных (поскольку позднее в нём могут появиться поля, объявленные с помощью объявлений экземпляров данных); -
HasField x (T ...) _, еслиxявляется переменной, а уTесть хотя бы одно поле (однако такой экземпляр разрешён, если уTнет полей); -
HasField "foo" (T ...) _, если уTесть полеfoo(однако такой экземпляр разрешён, если такого поля нет).
Если поле имеет тип с более высоким рангом или экзистенциальный тип, соответствующее ограничение HasField не будет решено автоматически (как описано выше), однако ради простоты мы также не разрешаем пользователям определять собственные экземпляры. Если поле не находится в области видимости, соответствующий экземпляр всё равно запрещён, чтобы избежать конфликтов в зависимых модулях.