Ответ 1
Два примера: один, где Proxy
необходим, а тот, где Proxy
принципиально не меняет типы, но я все равно его использую.
Proxy
необходимый
Proxy
или какой-то эквивалентный трюк необходим, если какой-либо промежуточный тип, который не отображается в сигнатуре нормального типа, который вы хотите, чтобы потребитель мог указать. Возможно, промежуточный тип изменяет семантику, например read . show :: String -> String
. Если включен ScopedTypeVariables
, я бы написал
f :: forall proxy a. (Read a, Show a) => proxy a -> String -> String
f _ = (show :: a -> String) . read
> f (Proxy :: Proxy Int) "3"
"3"
> f (Proxy :: Proxy Bool) "3"
"*** Exception: Prelude.read: no parse
Параметр прокси позволяет мне показывать a
как параметр типа. show . read
- это глупый пример. Лучшая ситуация может заключаться в том, что какой-то алгоритм использует общую коллекцию внутри, где выбран тип коллекции, имеет некоторые характеристики производительности, которые вы хотите, чтобы потребитель мог контролировать, не требуя (или позволяя) им предоставлять или получать промежуточное значение.
Что-то вроде этого, используя fgl
, где мы не хотим раскрывать внутренний тип Data
. (Возможно, кто-то может предложить соответствующий алгоритм для этого примера?)
f :: Input -> Output
f = g . h
where
h :: Gr graph Data => Input -> graph Data
g :: Gr graph Data => graph Data -> Output
Предоставление аргумента прокси позволяет пользователю выбирать между деревом Патриции или обычной реализацией древовидного графика.
Proxy
как API или удобство реализации
Я иногда использую Proxy
как инструмент для выбора экземпляра typeclass, особенно в рекурсивном или индуктивном экземплярах класса. Рассмотрим класс MightBeA
, который я написал в этом ответе об использовании вложенных Either
s:
class MightBeA t a where
isA :: proxy t -> a -> Maybe t
fromA :: t -> a
instance MightBeA t t where
isA _ = Just
fromA = id
instance MightBeA t (Either t b) where
isA _ (Left i) = Just i
isA _ _ = Nothing
fromA = Left
instance MightBeA t b => MightBeA t (Either a b) where
isA p (Right xs) = isA p xs
isA _ _ = Nothing
fromA = Right . fromA
Идея состоит в том, чтобы извлечь Maybe Int
из, скажем, Either String (Either Bool Int)
. Тип isA
в основном a -> Maybe t
. Здесь есть две причины использовать прокси:
Во-первых, он устраняет сигнатуры типов для потребителя. Вы можете вызвать isA
как isA (Proxy :: Proxy Int)
, а не isA :: MightBeA Int a => a -> Maybe Int
.
Во-вторых, мне легче продумать индуктивный случай, просто передав прокси. С ScopedTypeVariables
класс может быть переписан без аргумента прокси; индуктивный случай будет реализован как
instance MightBeA' t b => MightBeA' t (Either a b) where
-- no proxy argument
isA' (Right xs) = (isA' :: b -> Maybe t) xs
isA' _ = Nothing
fromA' = Right . fromA'
Это не очень большое изменение в этом случае; если сигнатура типа isA
была значительно сложнее, использование прокси-сервера было бы большим улучшением.
Когда использование используется исключительно для удобства реализации, я обычно экспортирую функцию-оболочку, поэтому пользователю не нужно предоставлять прокси-сервер.
Proxy
vs. Tagged
Во всех моих примерах параметр типа a
не добавляет ничего полезного для самого типа вывода. (В первых двух примерах он не имеет отношения к типу вывода, в последнем примере он избыточен выходным типом.) Если бы я вернул a Tagged a x
, потребитель неизменно отменил бы его немедленно. Кроме того, пользователю придется полностью записывать тип x
, что иногда очень неудобно, потому что это сложный промежуточный тип. (Возможно, когда-нибудь мы сможем использовать _
в типе подписей...)
(Мне интересно услышать другие ответы по этому второму вопросу, я буквально никогда ничего не писал с помощью Tagged
(без переписывания его в коротком порядке с помощью Proxy
) и задаюсь вопросом, не пропал ли я что-то. )