I have some function. And I want to restrict types that can be passed to this function. Let's say only types that are cacheable. I can enumerate such types with a type family like:
type family Cacheable a::Bool where
Cacheable X = 'True
Cacheable _ = 'False
And add such a constraint to my function:
myFunc :: forall a. (Cacheable a ~ 'True) => ....
but this constraint is little bit redundant: I can remove it from the function's signature, and nothing changes. Nothing forces it to be in the signature.
Another approach is to create some typeclass whose usage in the body of myFunc
will force me to add this constraint to myFunc
's signature:
class Cacheable a
ensureCacheable :: ()
ensureCacheable = ()
instance Cacheable X
myFunc :: forall a. (Cacheable a) => ...
myFunc =
let _ = ensureCacheable @a
in ...
but it looks little bit funny.
What is the right/canonical way to do it in Haskell?
Let's suppose that Cacheable
cannot have any reasoning methods. Think about it like about some classifier. Another name is IsQuery
(vs IsCommand
) and queries are cacheable, commands - no.
CodePudding user response:
For the sake of discussion, let's pick a single terminology:
type family CacheableTF a :: Bool where
CacheableTF X = 'True
CacheableTF _ = 'False
class CacheableC a
instance CacheableC X
What's the difference between these? Two aspects:
CacheableTF a
expresses in classical logic the fact thata
is a cacheable type. It's just a boolean after all. Thus you can arbitrarily stack negations on top of this.(Cacheable a ~ 'False) => ....
is a constraint just as valid as the'True
version.
By contrast,CacheableC a
is constructive, it's a proposition, a promise that anything which has this in context will be able to access the methods of the type class. (Of course, in your example the method was pretty useless, but even then you could still build other functions on top of it.) Unlike withCacheableTF
, you can't really use the negation ofCacheableC
at all.
This aspect is directly reflected in the kinds:CacheableTF :: Type -> Bool CacheableC :: Type -> Constraint
CacheableTF
is closed-world,CacheableC
is open-world. If you want this to be an interface where people can later on make their own types cacheable as well, you needCacheableC
. But this power is one of the reasons why class constraints can't be negated: just because the compiler can't find any instance while compiling one module, doesn't mean the type won't have an instance by the time the complete program is linked together.
If you want to make a decision based on whether or not the type is cacheable, you need CacheableTF
. However in practice, you'd typically also need some methods detailing how to cache it, in the True
case, not just trivial methods like in your CacheableC
. This can be accomplished by a more general class that covers both the cacheable- and non-cacheable cases:
class DecideCache a where
type IsCacheable a :: Bool
howToActuallyGoAboutCachingIt
:: (IsCacheable a ~ 'True) => SomeCompl -> Icated -> Stora -> GeMethod