Модуль 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 не будет решено автоматически (как описано выше), но в интересах простоты мы не разрешаем пользователям определять свои собственные экземпляры. Если поле не находится в области видимости, соответствующий экземпляр по-прежнему запрещен, чтобы избежать конфликтов в модулях вниз по течению.