1

I would like to define a function in a typeclass that outputs a result of an arbitrary Num type in Haskell. To illustrate this, I will use the below example:

class Exclass c where
    exFunc :: (Num a) => c -> a

For a simple newtype shown below:

newtype Extype a = Extype a

I would like to write an instance of it under Exclass when the encapsulated value is of typeclass Num:

instance (Num b) => Exclass (Extype b) where
    exFunc (Extype x) = x

However, the compiler compains that:

• Couldn't match expected type ‘a’ with actual type ‘b’
  ‘a’ is a rigid type variable bound by
    the type signature for:
      exFunc :: forall a. Num a => Extype b -> a
    at C:\Users\ha942\OneDrive\Documents\Haskell\Qaskell\src\Example.hs:9:5-10
  ‘b’ is a rigid type variable bound by
    the instance declaration
    at C:\Users\ha942\OneDrive\Documents\Haskell\Qaskell\src\Example.hs:8:10-38
• In the expression: x
  In an equation for ‘exFunc’: exFunc (Extype x) = x
  In the instance declaration for ‘Exclass (Extype b)’

Why can type a and b not equal to each other in this case? In foldr, the accumulation function signature is (a->b->b), but it would also take f::a->a->a. Why is it that in this case, the compiler complains? Is there a way to resolve this without declaring a higher-kinded typeclass? Any information is appreciated.

w41g87
  • 79
  • 4
  • You've defined `Exclass`'s method `exFunc` to work for _any_ `a` providing `Num a`. But then your instance only works for an `a` that's the same as the `b` parameter to `Extype`. That's not just any `a`. Do you want the `a` to be derive from the instance's type like that? Then you want to determine `a` from the class's `c` via a `TypeFamily`. (Or possibly `FunctionalDependencies`.) – AntC Sep 29 '21 at 07:40
  • @AntC, please have a look at my answer. – pedrofurla Sep 29 '21 at 07:42
  • 1
    Thanks @pedrofurla, but this is a FAQ/we don't really need more answers. Search for 'rigid type variable'. (I think your answer is over-complicated.) – AntC Sep 29 '21 at 07:51
  • And a FAQ shouldn't go into the whys and why nots? As for complication, I say it's details (over detailed perhaps). And these details are important to ensure understanding. FAQs don't need to be cookbooks. – pedrofurla Sep 29 '21 at 08:00
  • Oh, rigid type variable alone I doubt would help the author understand. – pedrofurla Sep 29 '21 at 08:03
  • 2
    @AntC If it's an FAQ with many good answers, please vote to close the question as a duplicate pointing to one of the earlier copies. That's much more useful than either yet another new answer or suggesting a phrase to Google. – amalloy Sep 29 '21 at 10:43
  • https://stackoverflow.com/questions/44243367/rigid-type-variable-in-haskell, https://stackoverflow.com/questions/64795997/how-do-i-give-a-concrete-value-to-a-haskell-instance – AntC Sep 30 '21 at 04:37
  • You might also like the answers at [Why can a Num act like a Fractional?](https://stackoverflow.com/q/42820603/791604). – Daniel Wagner Sep 30 '21 at 05:05

4 Answers4

3

Suppose for a moment that the compiler accepted your instance. Here is what would go wrong:

> :t Extype (0 :: Rational)
Extype (0 :: Rational) :: Extype Rational
> :t exFunc (Extype (0 :: Rational))
exFunc (Extype (0 :: Rational)) :: Num a => a
> :t exFunc (Extype (0 :: Rational)) :: Complex Double
exFunc (Extype (0 :: Rational)) :: Complex Double :: Complex Double

Uh-oh... the fact that this last term type-checks is a problem. Because now if we try to evaluate it...

exFunc (Extype (0 :: Rational)) :: Complex Double
= { definition of exFunc for Extype a }
(0 :: Rational) :: Complex Double

...we have broken type-safety! We are now treating a Rational as if it were a Complex Double, which is a completely different type that interprets its bits a completely different way!

Daniel Wagner
  • 145,880
  • 9
  • 220
  • 380
  • It would seem that you are saying because the compiler type checker will report an error LATER so that I couldn't define the function now? Shouldn't it be complaining now because this would pass the type check later? Suppose I replace the above function with some non-polymorphic function like ```exFunc2 :: String ->Int```, If I do something like ```exFunc2 x :: Double```, it would still not pass the type check, but it would not compain when I define exFunc2 itself. What is special about exFunc that makes it complain? – w41g87 Sep 30 '21 at 14:07
  • @w41g87 If you wrote something with type signature `String -> Int` but actually returned a `Double`, it would complain right away. And that is what you are doing in `exFunc`: your type signature says your return a `forall a. Num a => a`, which is a polymorphic type, but instead you return `x`, where `x` is the specific, monomorphic type contained in the `Extype x` argument (that happens to have a `Num x` instance, yes, but that doesn't suddenly make `x` polymorphic). – Daniel Wagner Sep 30 '21 at 14:20
  • 1
    @w41g87 Additionally, suppose for a moment you wrote `exFunc2`, gave it a type signature of `String -> Int`, and then wrote an implementation that did actually match that type. Then, as you say, when you write `exFunc2 x :: Double`, that is a type error. But now look at what happened in the hypothetical world in my answer: there is *no type error* at any point! That's a very different scenario; it means the compiler has let an incorrect program type-check, unlike the scenario you describe where you wrote an incorrect program but it didn't type-check. – Daniel Wagner Sep 30 '21 at 14:23
1

The explanation why it doesn't work this way.

We can simulate the instance (Num b) => Exclass (Extype b) with this simplified illustration:

num2num :: (Num a, Num b) => a -> b 
num2num a = a  -- ignoring the wrapper Extype

The above will similarly to the instance above.

Now notice that in pseudo haskell we could write the type of num2num as (Num a => a) -> (Num b => b). I use this contrived notation to give emphasis to the fact we don't know if Num a and Num b are actually the same instance. For example, a could be CBool and b Int8.

On the other hand we can add bit more constraints to our signature we make it work:

num2num' :: (Integral a, Num b) => a -> b 
num2num' a = fromIntegral a
> :t num2num' 10
num2num' 10 :: Num b => b

fromIntegral :: (Integral a, Num b) => a -> b does the conversion from one numeric type to the other.

In

instance (Num b) => Exclass (Extype b) where
    exFunc (Extype x) = x

Num b doesn't have enough functionality to perform the conversion.

Researching your problem I came up with a few simple examples that work and a bit of hacking to "force" it to work:

{-# LANGUAGE InstanceSigs #-}

class Exclass c where         
    exFunc :: Num a => c -> a

--- The simplest

instance Exclass Integer where 
    exFunc a = fromInteger a

instance Exclass Int where
    exFunc a = fromIntegral a

-- Notice Integer and Int already pull the instances for the fromInteger and fromIntegral, so they don't need more constraints.

-- Your use case
-- ^^^^^^^^^^^^^

newtype Extype a = Extype a

instance (Integral b) => Exclass (Extype b) where
    exFunc :: Num a => (Extype b) -> a
    exFunc             (Extype x) = fromIntegral x

-- A hacking "forcing" it, but I'd say it completely defeats the purpose

class Exclass2 c where         
    exFunc2 :: (a ~ c, Num a) => c -> a -- (a ~ c) gives proof that a and c are in indeed the same....

instance Exclass2 Integer where
    exFunc2 = id   -- .. therefore `id` simply works.

I left the hacking as a demonstration that the problem lies in fact that Num a and Num b can't be know by GHC to be the same and making the being the same works around that.

As a final note, numeric types are rich and some would say complicated. I don't know what use cases you have for the Exclass. But I advise you to fill the Xs carefully in instance (X b) => Exclass (Extype b) and consider only the use cases you really have.

You may also consider not using type classes simply implement ad hoc functions.

pedrofurla
  • 12,763
  • 1
  • 38
  • 49
  • If Haskell does not know if (Num a) and (Num b) are the same instance, shouldn't it be valid for all combinations of (Num a) and (Num b)? Isn't that a superset for the scenario where a = b? I guess I am just confused about what scenario the Haskell compiler is trying to prevent here. – w41g87 Sep 29 '21 at 21:50
  • 2
    @w41g87: “Cats are animals, dogs are animals, therefore cats are dogs“, more or less. `Exclass` is the set of types `c` that can be converted to any type `a`, given an instance `Num a` which proves `a` is in `Num`. For a given `T`, the function `exFunc :: (Num a) => T -> a` is the proof that `T` is in `Exclass`. Because `Extype b` is a trivial wrapper around `b`, the instance for `(Num b) => Extype b` is effectively claiming that we can convert *every* type `b` to *any* type `a`, knowing *only* that `a` and `b` are both in `Num`. But we can’t, because all we have is `Num`, which isn’t enough. – Jon Purdy Sep 29 '21 at 22:35
  • "a -> b" means "any type -> any type". We can also write it as "forall a. forall b. a -> b", not sure it helps though. The fact that both are instances of Num doesn't mean they are the same. For example, instantiate num2num with Int and Integer. – pedrofurla Sep 29 '21 at 22:58
  • Hmm, so a higher-kinded typeclass definition might be my only choice then. – w41g87 Sep 30 '21 at 14:13
0

You define

instance (Num b) => Exclass (Extype b) where
    exFunc (Extype x) = x

This exFunc has type (Num b) => (Extype b) -> b (1) but you promised

class Exclass c where
    exFunc :: (Num a) => c -> a

that it will have the type (Num a) => c -> a (2) with a completely independent from c.

Sure, if the function (2) existed for your type (which is what it means to define an instance -- it means to define that function, for that type), a particular use of that function, a particular call you'd make in your code could be at some related types, just like a generally typed a -> b -> b function can be used at a ~ b, as you correctly point out.

But when you're defining that instance, you're not using that function, you're defining it. And you've promised it to have more general type than the one your definition could possibly have.

So when a particular use of the function (1) would correspond to its narrow type, all would seem to be good and well. But another call might actually take it at its word (2) and use it at two independent types. And then what would happen? Kaboom would happen (see the answer by Daniel Wagner for an example).

So this is rejected, and rightfully so.

Will Ness
  • 70,110
  • 9
  • 98
  • 181
0

So I would like to post my own rationale for this question after further research. I will be using proof by negation.

Suppose the instance definition of exFunc :: (Num a, Num b) => (Extype b) -> a is passed with no problem, and we use it to compose with other functions below:

let x = (Extype 1.2) :: (Extype Double)

-- take :: Int -> [a] -> [a]

result :: [a]
result = take (exFunc x) [1..100]

During compilation, since the specific function implementation is transparent to the type checker, it could only see the type declaration of the functions. So if we follow the inference of the type checker:

x :: Extype Double
exFunc :: c -> a0
take :: Int -> [a1] -> [a1]

-- since the first argument passed to take is (exFunc x)
(exFunc x) :: Int
a0 == Int :: *
-- end of line for a0 - type checker cannot make further inference since a0 is not presented anywhere else

-- since exFunc takes x
c == (Extype Double) :: *
-- end of line for c - type checker cannot make any further inference.

In our example, the type checker inferred exFunc :: (Extype Double) -> Int, which is totally legal in the eye of the type checker. However, our code is impossible to compile even though the type checker did NOT throw an error. Therefore, in order for the type checker to fail when the code does not compile, the definition of exFunc must not pass type check.

w41g87
  • 79
  • 4