Подтвердить что ты не робот

Как избежать написания этого типа шаблона Haskell

Я часто сталкиваюсь с этой ситуацией, потому что это раздражает.

Скажем, у меня есть тип суммы, который может содержать экземпляр x или множество других вещей, не связанных с x -

data Foo x = X x | Y Int | Z String | ...(other constructors not involving x)

Чтобы объявить экземпляр Functor, я должен это сделать -

instance Functor Foo where
    fmap f (X x) = X (f x)
    fmap _ (Y y) = Y y
    fmap _ (Z z) = Z z
    ... And so on

В то время как то, что я хотел бы сделать, это -

instance Functor Foo where
    fmap f (X x) = X (f x)
    fmap _ a = a

то есть. Я только забочусь о конструкторе x, все остальные конструкторы просто "пройдены". Но, конечно, это не скомпилировалось, потому что a с левой стороны - это другой тип от a в правой части уравнения.

Есть ли способ избежать написания этого шаблона для других конструкторов?

4b9b3361

Ответ 1

Я предполагаю, что мы хотели бы иметь решение для общего случая, когда параметр изменяющегося типа не обязательно находится в правильном положении для DeriveFunctor.

Мы можем различать два случая.

В простейшем случае тип данных не является рекурсивным. Здесь призмы являются подходящим решением:

{-# LANGUAGE TemplateHaskell #-}

import Control.Lens

data Foo x y = X x | Y y | Z String

makePrisms ''Foo

mapOverX :: (x -> x') -> Foo x y -> Foo x' y
mapOverX = over _X

Если наши данные являются рекурсивными, ситуация усложняется. Теперь makePrisms не создает призм, изменяющий тип. Мы можем избавиться от рекурсии в определении, разложив ее на явную фиксированную точку. Таким образом, наши призмы остаются изменяющимися по типу:

import Control.Lens

newtype Fix f = Fix {out :: f (Fix f)}

-- k marks the recursive positions
-- so the original type would be "data Foo x y = ... | Two (Foo x y) (Foo x y)"
data FooF x y k = X x | Y y | Z String | Two k k deriving (Functor)

type Foo x y = Fix (FooF x y)

makePrisms ''FooF

mapOverX :: (x -> x') -> Foo x y -> Foo x' y
mapOverX f = 
   Fix .               -- rewrap 
   over _X f .         -- map f over X if possible
   fmap (mapOverX f) . -- map over recursively
   out                 -- unwrap

Или мы можем разложить преобразование снизу вверх:

cata :: (Functor f) => (f a -> a) -> Fix f -> a
cata f = go where go = f . fmap go . out

mapOverX :: (x -> x') -> Foo x y -> Foo x' y
mapOverX f = cata (Fix . over _X f)

Существует значительная литература об использовании фиксированных точек функторов для общего программирования, а также ряда библиотек, например this или это. Возможно, вы захотите найти "схемы рекурсии" для дальнейших ссылок.

Ответ 2

Есть два основных простых решения.

Сначала для простых типов просто deriving (Functor) с использованием необходимого расширения.

Другим решением является определение другого типа данных:

data Bar = S String | B Bool | I Int  -- "Inner" type
data Foo a = X a | Q Bar              -- "Outer" type

instance Functor Foo where
    fmap f (X a) = X (f a)
    fmap _ (Q b) = Q b -- `b' requires no type change. 

Итак, вы можете написать еще одну строку, чтобы удалить многие.

Это не идеально подходит для сопоставления шаблонов, но, по крайней мере, решает эту проблему.

Ответ 3

Похоже на работу призм.

Отказ от ответственности: я новичок с линзой/призмой.

{-# LANGUAGE TemplateHaskell   #-}

import Control.Lens
import Control.Lens.Prism

data Foo x = X x | Y Int | Z String deriving Show

makePrisms ''Foo

instance Functor Foo where
   -- super simple impl, by András Kovács
   fmap = over _X
   -- My overly complicated idea
   --    fmap f = id & outside _X .~ (X . f)
   -- Original still more complicated implementation below
   --     fmap f (X x) = X (f x)
   --     fmap _ a = id & outside _X .~ undefined $ a

Использование:

*Main> fmap (++ "foo") (Y 3)
Y 3
*Main> fmap (++ "foo") (X "abc")
X "abcfoo"

Ответ 4

В основном для полноты, вот еще способ сделать это:

import Unsafe.Coerce

instance Functor Foo where
    fmap f (X x) = X (f x)
    fmap _ a = unsafeCoerce a

в описанной вами ситуации, это будет фактически безопасным использованием unsafeCoere. Но есть все основания для этого:

  • Безопасность зависит от того, как GHC собирает структуры данных и код; что нормальный программист не должен иметь.
  • Он также не является надежным: если тип данных расширяется с помощью нового конструктора X 'x, предупреждение не будет генерироваться, потому что catch-all делает это определение исчерпывающим, и тогда все будет идти. (спасибо @gallais для этого комментария)

Следовательно, это решение определенно не целесообразно.