Ну, проблема в том, что не ясно, что должен означать пересмотренный 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, поэтому я не знаю, действительно ли что-то из этого компилируется или работает, но я думаю, что основные идеи верны.