Spec-Zone.ru › Haskell 9

6.5.9. Полиморфизм селекторов полей записей

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

© 2002–2007 The University Court of the University of Glasgow. All rights reserved.
Licensed under the Glasgow Haskell Compiler License.
https://downloads.haskell.org/~ghc/9.14.1/docs/users_guide/exts/hasfield.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API