Here you’re trying to produce different term-level code based on type-level information, so you need a typeclass; and you’re trying to match on types, so you can use an associated type family. This method requires overlapping instances:
{-# Language
AllowAmbiguousTypes,
FlexibleInstances,
TypeFamilies #-}
-- The class of overloads of ‘f’.
class F a where
type family T a -- Or: ‘type T a’
f :: T a -> T a
-- The “default” catch-all overload.
instance {-# Overlappable #-} F a where
type instance T a = a -- Or: ‘type T a = a’
f x = x
-- More specific instance, selected when ‘a ~ Int’.
instance {-# Overlapping #-} F Int where
type instance T Int = Int
f x = x + 1
Then f @Int 1
gives 2
, and f 'c'
gives 'c'
.
But in this case, type family is unnecessary because it happens to be the identity—I include it for the sake of giving a point of generalisation. If you want to produce types by pattern-matching on types just like a function, then a closed type family is a great fit:
{-# Language
KindSignatures,
TypeFamilies #-}
import Data.Int
import Data.Word
import Data.Kind (Type)
type family Unsigned (a :: Type) :: Type where
Unsigned Int = Word
Unsigned Int8 = Word8
Unsigned Int16 = Word16
Unsigned Int32 = Word32
Unsigned Int64 = Word64
Unsigned a = a
Back to your question, here’s the original example with plain typeclasses:
class F a where
f :: a -> a
instance {-# Overlappable #-} F a where
f x = x
instance {-# Overlapping #-} F Int where
f x = x + 1
Unfortunately, there’s no notion of a “closed typeclass” that would allow you to avoid the overlapping instances. In this case it’s fairly benign, but problems with coherence can arise with more complex cases, especially with MultiParamTypeClasses
. In general it’s preferable to add a new method instead of writing overlapping instances when possible.
Note that in either case, f
now has an F a
constraint, e.g. the latter is (F a) => a -> a
. You can’t avoid some change in the type; in Haskell, polymorphic means parametrically polymorphic, to preserve the property that types can be erased at compile time.
Other options include a GADT:
data FArg a where
FInt :: Int -> FArg Int
FAny :: a -> FArg a
f :: FArg a -> a
f (FInt x) = x + 1 -- Here we have ‘a ~ Int’ in scope.
f (FAny x) = x
Or (already mentioned in other answers) a Typeable
constraint for dynamic typing:
{-# Language
BlockArguments,
ScopedTypeVariables #-}
import Data.Typeable (Typeable, eqT)
import Data.Type.Equality ((:~:)(Refl))
f :: forall a. (Typeable a) => a -> a
f x = fromMaybe x do
Refl <- eqT @a @Int
pure (x + 1)