Связанные типы и элементы контейнера - PullRequest
21 голосов
/ 26 января 2012

Я думаю, что, возможно, в какой-то момент я спрашивал об этом в Haskell-Cafe, но, черт побери, могу ли я найти ответ сейчас ... Поэтому я спрашиваю его снова здесь, так что надеюсь в будущем я смогу найти ответ!

Haskell фантастический при работе с параметрическим полиморфизмом. Но беда в том, что не все параметрически. В качестве тривиального примера, предположим, что мы хотим извлечь первый элемент данных из контейнера. Для параметрического типа это тривиально:

class HasFirst c where
  first :: c x -> Maybe x

instance HasFirst [] where
  first []    = Nothing
  first (x:_) = Just x

Теперь попробуйте написать экземпляр для ByteString. Ты не можешь Его тип не упоминает тип элемента. Вы также не можете написать экземпляр для Set, потому что для этого требуется ограничение Ord - но глава класса не упоминает тип элемента, поэтому вы не можете ограничить его.

Связанные типы предоставляют аккуратный способ полностью решить эти проблемы:

class HasFirst c where
  type Element c :: *
  first :: c -> Maybe (Element c)

instance HasFirst [x] where
  type Element [x] = x
  first []    = Nothing
  first (x:_) = Just x

instance HasFirst ByteString where
  type Element ByteString = Word8
  first b = b ! 0

instance Ord x => HasFirst (Set x) where
  type Element (Set x) = x
  first s = findMin s

Однако теперь у нас появилась новая проблема. Попробуйте "исправить" Functor, чтобы он работал для всех типов контейнеров:

class Functor f where
  type Element f :: *
  fmap :: (Functor f2) => (Element f -> Element f2) -> f -> f2

Это не работает вообще. Это говорит о том, что если у нас есть функция от типа элемента f до типа элемента f2, то мы можем превратить f в f2. Все идет нормально. Однако, по-видимому, невозможно требовать, чтобы f и f2 были контейнером одного и того же вида!

В соответствии с существующим Functor определением, мы имеем

fmap :: (x -> y) -> [x] -> [y]
fmap :: (x -> y) -> Seq x -> Seq y
fmap :: (x -> y) -> IO x -> IO y

Но у нас нет fmap :: (x -> y) -> IO x -> [y]. Это совершенно невозможно. Но приведенное выше определение класса позволяет это.

Кто-нибудь знает, как объяснить системе типов, что я на самом деле имел в виду?

Редактировать

Вышеописанное работает путем определения способа вычисления типа элемента из типа контейнера. Что произойдет, если вы попытаетесь сделать это наоборот? Определить функцию для вычисления типа контейнера из типа элемента? Это работает легче?

Ответы [ 2 ]

22 голосов
/ 26 января 2012

Ну, проблема в том, что не ясно, что должен означать пересмотренный Functor. Например, рассмотрим ByteString. ByteString может быть сопоставлен только путем замены каждого элемента Word8 элементом того же типа . Но Functor для параметрических отображаемых структур. Здесь действительно два противоречивых понятия отображения:

  • Жесткое отображение (т.е. преобразование каждого элемента структуры без изменения его типа)
  • Параметрическое отображение (то есть преобразование каждого элемента структуры в любой тип)

Итак, в этом случае вы не можете объяснить системе типов, что вы имели в виду, потому что это не имеет особого смысла. Однако вы можете изменить то, что имеете в виду:)

Жесткое отображение легко выразить с помощью семейств типов:

class RigidMap f where
  type Element f :: *
  rigidMap :: (Element f -> Element f) -> f -> f

Что касается параметрического отображения, есть несколько способов сделать это. Простейшим способом было бы сохранить текущий Functor как есть. Вместе эти классы охватывают такие структуры, как ByteString, [], Seq и так далее. Однако все они попадают на Set и Map из-за ограничения Ord на значения. К счастью, расширение видов ограничений , появившееся в GHC 7.4, позволяет нам решить эту проблему:

class RFunctor f where
  type Element f a :: Constraint
  type Element f a = ()  -- default empty constraint
  fmap :: (Element f a, Element f b) => (a -> b) -> f a -> f b

Здесь мы говорим, что с каждым экземпляром должно быть связано ограничение класса типов . Например, экземпляр Set будет иметь Element Set a = Ord a, чтобы обозначить, что Set s может быть создан, только если для типа доступен экземпляр Ord. Все, что может появиться в левой части =>, принимается. Мы можем определить наши предыдущие экземпляры точно так, как они были, но мы также можем сделать Set и Map:

instance RFunctor Set where
  type Element Set a = Ord a
  fmap = Set.map

instance RFunctor Map where
  type Element Map a = Ord a
  fmap = Map.map

Однако довольно неприятно использовать два отдельных интерфейса для жесткого отображения и ограниченного параметрического отображения. На самом деле, не является ли последнее обобщением первого? Рассмотрим разницу между Set, которая может содержать только экземпляры Ord, и ByteString, которые могут содержать только Word8 с. Конечно, мы можем выразить это как еще одно ограничение?

Мы применяем тот же трюк, что и к HasFirst (т.е. даем экземпляры для всей структуры и используем семейство типов для раскрытия типа элемента), и вводим новое связанное семейство ограничений:

class Mappable f where
  type Element f :: *
  type Result f a r :: Constraint
  map :: (Result f a r) => (Element f -> a) -> f -> r

Идея заключается в том, что Result f a r выражает необходимые ограничения на тип значения (например, Ord a), а также ограничивает результирующий тип контейнера, как это необходимо; предположительно, чтобы гарантировать, что он имеет тип контейнера такого же типа a с. Например, Result [a] b r предположительно потребует, чтобы r было [b], а Result ByteString b r потребовало бы, чтобы b было Word8, а r было ByteString.

Семейства типов уже дают нам то, что нам нужно для выражения «есть»: ограничение равенства типов. Можно сказать (a ~ b) => ..., чтобы потребовать, чтобы a и b были одного типа. Мы можем, конечно, использовать это в определениях семейства ограничений. Итак, у нас есть все, что нам нужно; к экземплярам:

instance Mappable [a] where
  type Element [a] = a
  type Result [a] b r = r ~ [b]
  -- The type in this case works out as:
  --   map :: (r ~ [b]) => (a -> b) -> [a] -> r
  -- which simplifies to:
  --   map :: (a -> b) -> [a] -> [b]
  map = Prelude.map

instance Mappable ByteString where
  type Element ByteString = Word8
  type Result ByteString a r = (a ~ Word8, r ~ ByteString)
  -- The type is:
  --   map :: (b ~ Word8, r ~ ByteString) => (Word8 -> b) -> ByteString -> r
  -- which simplifies to:
  --   map :: (Word8 -> Word8) -> ByteString -> ByteString
  map = ByteString.map

instance (Ord a) => Mappable (Set a) where
  type Element (Set a) = a
  type Result (Set a) b r = (Ord b, r ~ Set b)
  -- The type is:
  --   map :: (Ord a, Ord b, r ~ Set b) => (a -> b) -> Set a -> r
  -- (note the (Ord a) constraint from the instance head)
  -- which simplifies to:
  --   map :: (Ord a, Ord b) => (a -> b) -> Set a -> Set b
  map = Set.map

Отлично! Мы можем определить экземпляры для любого типа контейнера, который мы хотим, жесткого, параметрического или параметрически-но-ограниченного, и типы работают отлично.

Отказ от ответственности: я еще не пробовал GHC 7.4, поэтому я не знаю, действительно ли что-то из этого компилируется или работает, но я думаю, что основные идеи верны.

6 голосов
/ 26 января 2012

Вы хотите виды ограничений , доступны в ghc 7.4.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...