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

Haskell: Зачем использовать прокси?

В Haskell Proxy - значение типа свидетеля, которое упрощает передачу некоторых типов вокруг

data Proxy a = Proxy

Пример использования приведен в json-schema:

class JSONSchema a where
  schema :: Proxy a -> Schema

чтобы вы могли сделать schema (Proxy :: Proxy (Int,Char)), чтобы получить представление JSON для Int-Char -Tuple (возможно, массив).


Почему люди используют прокси? Мне кажется, что то же самое можно сделать с помощью

class JSONSchema a where
  schema :: Schema a

аналогично тому, как работает класс Bounded. Сначала я подумал, что проще использовать схему некоторого заданного значения при использовании прокси, но это не похоже на правду:

{-# LANGUAGE ScopedTypeVariables #-}

schemaOf :: JSONSchema a => a -> Schema a

schemaOf (v :: x) = schema (Proxy :: Proxy x)  -- With proxy

schemaOf (v :: x) = schema :: Schema x         -- With `:: a`
schemaOf _ = schema                            -- Even simpler with `:: a`

Кроме того, можно подумать о том, действительно ли значения Proxy были искоренены во время выполнения, что является проблемой оптимизации, которая не существует при использовании подхода :: a.

Если подход :: a, принятый Bounded, дает тот же результат с более коротким кодом и меньше забот об оптимизации, почему люди используют прокси? Каковы преимущества прокси-серверов?


EDIT:. Некоторые ответы и комментаторы по праву отметили, что подход :: a прикрывает тип data Schema = ... параметром типа "бесполезный" - по крайней мере, с точки зрения самой простой структуры данных, который никогда не использует a (здесь).

Предполагается использовать phantom тип Tagged s b, что позволяет отделить две проблемы (Tagged a Schema объединяет тип непараметрической схемы с переменной типа a), которая строго лучше, чем подход :: a.

Итак, мой вопрос должен быть лучше . Каковы преимущества прокси-сервера и меченый подход?

4b9b3361

Ответ 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) и задаюсь вопросом, не пропал ли я что-то. )

Ответ 2

В конечном итоге они будут выполнять те же функции, и вы увидите их в любом стиле. Иногда это соответствует phantom пометить ваши значения, иногда вы хотели бы думать о них как о нетипизированных.

Другой альтернативой является использование Data.Tagged.

class JSONSchema a where
  schema :: Tagged a Schema

Здесь у нас есть что-то лучшее из обоих миров, так как Tagged Schema имеет phantom информацию типа, необходимую для разрешения экземпляра, но мы можем тривиально игнорировать эту информацию с помощью unTagged :: Tagged s b -> b.

Я бы сказал, что водительский вопрос, сформулированный в терминах этого примера, должен быть "Должен ли я рассматривать типизированные операции над Schema s?". Если ответ "нет", вы будете смещены в сторону подходов Proxy или Tagged. Если ответ "да", то Schema a - отличное решение.

В качестве заключительной заметки вы можете использовать подход Proxy (несколько взломанный) без импорта. Вы видите это иногда в стиле

class JSONSchema a where
  schema :: proxy a -> Schema

Теперь, когда Proxy превращается в именную переменную типа с нарицательным именем, мы можем сделать что-то вроде следующего

foo :: Schema
foo = schema ([] :: [X])

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