1

I am reading the book Haskell Programming from first principles and in one of the exercises proposed, the author ask us to write an instance of a class TooMany for (Num a, TooMany a) => (a, a) which should compare if the sum of two numbers is bigger than 42.

Here is my code:

class TooMany a where
    tooMany :: a -> Bool
instance TooMany Int where
    tooMany n = n > 42
instance (Num a, TooMany a) => TooMany(a, a) where
    tooMany (a, b) = tooMany (a + b)

I think my code is correct but I don't know how to write the expression to test it. For TooMany Int I simply wrote tooMany (10 :: Int) in the REPL and that's it, I had a boolean answer.

Can anyone give me a hint on this one?

Guilherme
  • 443
  • 5
  • 22
  • 2
    `tooMany (10, 10)`? (In addition, try `:t (10, 10)` in GHCi and double-check how that fits your code.) – duplode Oct 17 '16 at 03:50
  • Nope, that doesn't work. https://i.imgur.com/3Z2bFLu.png Also, :t (10, 10) returns :: (Num t1, Num t) => (t, t1) – Guilherme Oct 17 '16 at 04:13
  • 2
    @dfeuer is right in that `FlexibleInstances` is needed -- though you presumably have already enabled it, given that you were able to at least define the instance. Other than that, it is indeed necessary to give the tuple the appropriate specialised type through e.g. `(10 :: Int, 10 :: Int)`. A rather strange exercise indeed. – duplode Oct 17 '16 at 04:25

2 Answers2

5

Based on the error message, it appears that you're using not only FlexibleInstances but even OverlappingInstances. As duplode indicates, you will have to explicitly fix the precise argument type in order to call the method with a tuple. GHC will have to be able to see that the tuple is neither (Int, String) nor (Int, Int), and that the components have the same type, before selecting the instance.

This sort of ad hoc class is generally discouraged, and these sorts of utterly wild instances are even more strongly discouraged. In general, class instances should be pretty uniform. If you need more than one instance for tuples, then you're probably doing something wrong. And overlapping instances are essentially for type-directed meta-programming. I personally avoid them like the plague; if you're going to use them, you'll need to be very, very careful about what you're doing and recognize that the API that results is likely to behave in unexpected ways in some cases. There are some techniques for avoiding overlap using type families that make complicated instance structures less wild, but even those need to be designed with great care and will limit you in ways that could bite you later.

dfeuer
  • 48,079
  • 5
  • 63
  • 167
  • I don't `OverlappingInstances` is being used. At least the overlap isn't obvious to me and the error messages seem to be consistent with just `FlexibleInstances`. – badcook Oct 17 '16 at 07:27
  • @badcook, the instances described in the error messages as being at lines 63 and 66, for `(Int, Int)` and `(a, a)`, certainly overlap. – dfeuer Oct 17 '16 at 14:06
  • you're entirely correct. I skimmed through the image far too quickly. – badcook Oct 17 '16 at 14:10
5

First off, as @dfeuer aptly points out, in "real-life" Haskell programming you wouldn't want to use typeclasses for this kind of stuff. The Haskell community generally wants typeclasses with a set of meaningful algebraic laws that govern the behavior of the typeclass instances. These laws are not checked by the compiler (although programmers will often write QuickCheck tests for them).

With that out of the way, I'm assuming the book is teaching you this purely for familiarizing you with the mechanics of typeclasses (that they can have constraints which reference the typeclass itself) and not espousing this typeclass as a particularly good one.

@duplode gives you the correct answer as to what to do in this scenario: annotate your types.

Main> tooMany (10 :: Int, 10 :: Int)
False

So why can't GHC automatically find the correct instance for (10, 10) and instead requires type annotations? Well fundamentally it's because Haskell typeclasses are open, meaning that anyone can at any time make a new instance of a typeclass you've created in another module which you don't know about, e.g. if you put this typeclass in a library, downstream consumers of that library can make their own instances of this typeclass.

In this case, as you've already noticed, numeric literals don't actually have a monomorphic type in Haskell. 10 is of type Num a => a, not Int nor Integer nor Double nor anything else like that. Because typeclasses are open, even though you have only defined a single instance of a Num type for TooMany (namely Int), the Haskell compiler cannot rely on that to infer that there is in fact only one instance of a Num type for TooMany and therefore that (10, 10) must have type (Int, Int). Anyone else who uses your code could define their own instance of TooMany for, say, Doubles.

In particular, a consumer of your module could make a new type with degenerate type instances for TooMany and Num.

-- Imagine you published `TooMany` as a library on Hackage
-- and now a downstream consumer writes the following

-- This example happens to break the laws for Num, 
-- but the compiler doesn't know about typeclass laws
data DegenerateType = DegenerateType

instance Num DegenerateType where
    _ + _ = DegenerateType
    _ * _ = DegenerateType
    negate _ = DegenerateType
    abs _ = DegenerateType
    signum _ = 0
    -- The next line dictates what a numeric literal 
    -- actually means for a DegenerateType
    fromInteger _ = DegenerateType

instance TooMany DegenerateType where
    tooMany _ = True

Now tooMany (10, 10) has different behavior depending on what monomorphic type 10 is.

Main> tooMany (10 :: DegenerateType, 10 :: DegenerateType)
True

Therefore tooMany (10, 10) is not enough to specify the behavior of tooMany and you must use type annotations.

Community
  • 1
  • 1
badcook
  • 3,699
  • 14
  • 25