Haskell: Why use Proxy?
Solution 1:
Two examples, one where Proxy
is necessary, and one where Proxy
doesn't fundamentally change the types, but I tend to use it anyway.
Proxy
necessary
Proxy
or some equivalent trick is necessary when there's some intermediate type, not exposed in the normal type signature, that you want the consumer to be able to specify. Perhaps the intermediate type changes the semantics, such as read . show :: String -> String
. With ScopedTypeVariables
enabled, I'd write
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
The proxy parameter allows me to expose a
as a type parameter. show . read
is kind of a stupid example. A better situation may be where some algorithm uses a generic collection internally, where the collection type selected has some performance characteristics that you want to the consumer to be able to control without requiring (or permitting) them to provide or receive the intermediate value.
Something like this, using fgl
types, where we don't want to expose the internal Data
type. (Perhaps someone can suggest an appropriate algorithm for this example?)
f :: Input -> Output
f = g . h
where
h :: Gr graph Data => Input -> graph Data
g :: Gr graph Data => graph Data -> Output
Exposing a proxy argument would allow the user to select between a Patricia tree or a normal tree graph implementation.
Proxy
as API or implementation convenience
I sometimes use Proxy
as a tool to choose a typeclass instance, especially in recursive or inductive class instances. Consider the MightBeA
class I wrote in this answer about using nested 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
The idea is to extract a Maybe Int
from, say, Either String (Either Bool Int)
. The type of isA
is basically a -> Maybe t
. There are two reasons to use a proxy here:
First, it eliminates type signatures for the consumer. You can call isA
as isA (Proxy :: Proxy Int)
rather than isA :: MightBeA Int a => a -> Maybe Int
.
Second, it's easier for me to think through the inductive case by just passing the proxy through. With ScopedTypeVariables
, the class can be rewritten without a proxy argument; the inductive case would be implemented as
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'
This isn't really a big change in this case; if the type signature of isA
was considerably more complex, using the proxy would be a big improvement.
When the use is exclusively for implementation convenience, I'd typically export a wrapper function so the user needn't provide the proxy.
Proxy
vs. Tagged
In all of my examples, the type parameter a
doesn't add anything useful to the output type itself. (In the first two examples, it's unrelated to the output type; in the last example, it's redundant of the output type.) If I returned a Tagged a x
, the consumer would invariably untag it immediately. Furthermore, the user will have to write out the type of x
in full, which is sometimes very inconvenient because it's some complicated intermediate type. (Maybe someday we'll be able to use _
in type signatures...)
(I'm interested to hear other answers on this sub-question; I've literally never written anything using Tagged
(without rewriting it in short order using Proxy
) and wonder whether I'm missing something.)
Solution 2:
Ultimately they will perform the same functionality and you see them in either style. Sometimes it's appropriate to phantom tag your values, sometimes you'd like to think of them as untyped.
The other alternative is to use Data.Tagged
.
class JSONSchema a where
schema :: Tagged a Schema
Here we have something of the best of both worlds since a Tagged
Schema
has phantom type information necessary to resolve the instance but we can trivially disregard that information using unTagged :: Tagged s b -> b
.
I would say the driving question, couched in terms of this example, should be "Do I want to consider typed operations on Schema
s?". If the answer is "no", then you will be biased toward the Proxy
or Tagged
approaches. If the answer is "yes", then Schema a
is a great solution.
As a final note, you can use the Proxy
approach (somewhat hackily) without any imports. You see this sometimes in the style
class JSONSchema a where
schema :: proxy a -> Schema
Now that Proxy
has become a suggestively named type variable only we can do something like the following
foo :: Schema
foo = schema ([] :: [X])
and never have to import Proxy
at all. I personally think this is a complete hack job though which probably will end up confusing readers.