We just have to pick our poison here. If we use less safe pragmas, we can get more inference, and vice versa; there's a number of combinations.
First: uses overlapping instances, has non-functions as base case, but can't handle polymorphic a
types:
{-# LANGUAGE MultiParamTypeClasses, TypeFamilies, FlexibleInstances #-}
class Listify a b where
listify :: a -> b
instance {-# OVERLAPS #-} r ~ ([a] -> b) => Listify b r where
listify = const
instance (Listify f r, r ~ ([a] -> b)) => Listify (a -> f) r where
listify f (a:as) = listify (f a) as
-- listify (+) [0, 2] -- error
-- listify (+) [0, 2 :: Int] -- OK
-- listify () [] -- OK
Second: uses overlapping instances, has functions as base case, can handle polymorphic types:
{-# LANGUAGE MultiParamTypeClasses, TypeFamilies, FlexibleInstances, FlexibleContexts #-}
class Listify a b where
listify :: a -> b
instance {-# OVERLAPS #-} r ~ ([a] -> b) => Listify (a -> b) r where
listify f (a:_) = f a
instance (Listify (a -> b) r, r ~ ([a] -> b)) => Listify (a -> a -> b) r where
listify f (a:as) = listify (f a) as
-- listify (+) [0, 2] -- OK
-- listify () [] -- error, first arg must be a function
Third: uses incoherent instances, has values in base case, can handle polymorphic types:
{-# LANGUAGE MultiParamTypeClasses, TypeFamilies, FlexibleInstances #-}
class Listify a b where
listify :: a -> b
instance {-# INCOHERENT #-} r ~ ([a] -> b) => Listify b r where
listify = const
instance (Listify f r, r ~ ([a] -> b)) => Listify (a -> f) r where
listify f (a:as) = listify (f a) as
-- listify 0 [] -- OK
-- listify (+) [2, 4] -- OK
Fourth: uses closed type families with UndecidableInstances
as helper for instance resolution, has values in base case, can't handle polymorphic types:
{-# LANGUAGE UndecidableInstances, ScopedTypeVariables, DataKinds,
TypeFamilies, MultiParamTypeClasses, FlexibleInstances, FlexibleContexts #-}
import Data.Proxy
data Nat = Z | S Nat
type family Arity f where
Arity (a -> b) = S (Arity b)
Arity b = Z
class Listify (n :: Nat) a b where
listify' :: Proxy n -> a -> b
instance r ~ (a -> b) => Listify Z b r where
listify' _ = const
instance (Listify n f r, a ~ (a' -> f), r ~ ([a'] -> b)) => Listify (S n) a r where
listify' _ f (a:as) = listify' (Proxy :: Proxy n) (f a) as
listify :: forall a b. Listify (Arity a) a b => a -> b
listify = listify' (Proxy :: Proxy (Arity a))
-- listify (+) [3, 4] -- error
-- listify (+) [3, 4::Int] -- OK
-- listify () [] -- OK
-- listify 0 [] -- error
-- listify (0 :: Int) [] -- OK
From the top of my head, roughly these are the variants one can see in the wild, except for the INCOHERENT
one, because that's extremely rare in library code (for good reasons).
I personally recommend the version with the closed type families, because UndecidableInstances
and type families are by far the least controversial as language extensions, and they still provide a fair amount of usability.