Can “overloading” via FlexibleInstances return dif

2019-05-16 11:03发布

问题:

I'm curious about what kind of "overloading" can be accomplished in Haskell's type classes via "FlexibleInstances".

As a simple test, here is a case of an AdjusterType datatype. It defines an adjust operation that will add a different amount to its value based on whether it contains an Integer or a Double:

{-# LANGUAGE FlexibleInstances #-}

class Adjustable a where
    adjust :: a -> Double

data AdjusterType a = Adjuster a
    deriving Show

instance Adjustable (AdjusterType Integer) where
    adjust (Adjuster a) = fromIntegral (a + 20)

instance Adjustable (AdjusterType Double) where
    adjust (Adjuster a) = a + 0.04

That much works as expected:

Prelude> adjust (Adjuster (1000 :: Integer))
1020.0

Prelude> adjust (Adjuster (3 :: Double))
3.04

Is it possible to make the Integer version of adjust return an Integer, and the Double version return a Double?

Generalizing the signature of adjust and removing the fromIntegral on the integer case doesn't work:

class Adjustable a where
    adjust :: Num n => a -> n

instance Adjustable (AdjusterType Integer) where
    adjust (Adjuster a) = a + 20

This produces an error saying that "n" is a rigid type variable that doesn't match Integer:

Couldn't match expected type ‘n’ with actual type ‘Integer’
    ‘n’ is a rigid type variable bound by
       the type signature for adjust :: Num n => AdjusterType Integer -> n
Relevant bindings include
    adjust :: AdjusterType Integer -> n
In the first argument of ‘(+)’, namely ‘a’
In the expression: a + 20

What type was it expecting here that Integer isn't matching...or would no type actually work and it's just a weird error message? (n is lowercase, so presumably it knows it's not a datatype)

Type constraints in the instance specifications also don't appear to participate in the matching resolution:

instance Integral i => Adjustable (AdjusterType i) where
    adjust (Adjuster a) = fromIntegral (a + 20)

instance RealFloat r => Adjustable (AdjusterType r) where
    adjust (Adjuster a) = a + 0.04

So these act like duplicates, as if they were both Adjustable (AdjusterType x)). The constraint only applies after the resolution is done.

Is there any way to provide an overloaded behavior like above to a type class, or must it always be to a specific instance?

回答1:

Is it possible to make the Integer version of adjust return an Integer, and the Double version return a Double?

You can make the Adjustable type class accept two type parameters instead of one, so it will know what's inside the AdjusterType:

{-# LANGUAGE MultiParamTypeClasses #-}

class Adjustable f a where
    adjust :: f a -> a

Then the instances should be:

instance Adjustable AdjusterType Int where
    adjust (Adjuster a) = a + 20

instance Adjustable AdjusterType Double where
    adjust (Adjuster a) = a + 0.04

And some results from ghci:

> :set +t

> adjust (Adjuster (100 :: Int))
< 120
< it :: Int
> adjust (Adjuster (100 :: Double))
< 100.04
< it :: Double

What type was it expecting here that Integer isn't matching...or would no type actually work and it's just a weird error message?

The return type of adjust is of type forall n . Num n => n, a polymorphic type with a single constraint Num, so your function returning a concrete type won't type check. Wrap your function with fromIntegral will solve the problem since fromIntegral :: (Integral a, Num b) => a -> b.

Is there any way to provide an overloaded behavior like above to a type class, or must it always be to a specific instance?

If you expect the function to behave differently for every distinct type, yes you have to add an instance for each. You may add some default behavior though, by restricting the type parameters of the class:

{-# LANGUAGE DeriveFunctor         #-}
{-# LANGUAGE MultiParamTypeClasses #-}

class Extract f where
  extract :: f a -> a

class (Extract f, Functor f, Num a) => Adjustable f a where
  adjust :: f a -> a
  adjust = extract . fmap (+20)

data AdjusterType a = Adjuster a
  deriving (Functor)

instance Extract AdjusterType where
  extract (Adjuster a) = a

instance Adjustable AdjusterType Int where
-- don't have to write any code here


回答2:

A solution using type families, in particular associated data types, is the following:

{-# LANGUAGE TypeFamilies, FlexibleInstances #-}

class Adjustable a where
    type Elem a :: *
    adjust :: a -> Elem a


data AdjusterType a = Adjuster a
    deriving (Show)


instance Adjustable (AdjusterType Integer) where
    type Elem (AdjusterType Integer) = Integer

    adjust (Adjuster a) = a + 20

instance Adjustable (AdjusterType Double) where
    type Elem (AdjusterType Double) = Double

    adjust (Adjuster a) = a + 0.04

main = do
    let x = Adjuster 1 :: AdjusterType Integer
        y = Adjuster 1 :: AdjusterType Double
    print $ adjust x
    print $ adjust y

It compiles and the output is:

21
1.04