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)