3

I was using OverlappingInstances to make a pretty print class that would default to Show whenever I didn't provide a custom instance for some type.

For some reason this seems to break whenever you use a where clause or a let expression.

{-# LANGUAGE FlexibleInstances, UndecidableInstances #-}

class View a where
    view :: a -> String

instance {-# OVERLAPS #-} Show a => View a where
    view = show

-- Works just fine
instance (View a, View b) => View (a, b) where
    view (a, b) = "(" ++ view a ++ ", " ++ view b ++ ")"

-- Does not work
instance (View a, View b) => View (a, b) where
    view (a, b) = "(" ++ a' ++ ", " ++ b' ++ ")"
      where
        a' = view a
        b' = view b

-- Does not work
instance (View a, View b) => View (a, b) where
    view (a, b) = let
        a' = view a
        b' = view b
        in "(" ++ a' ++ ", " ++ b' ++ ")"

Now if I remove the default overlapping instance all of the other instances work just fine.

I am hoping someone could explain to me why this happens, or is it just a bug?

The specific errors I get for each of them are:

Could not deduce (Show a) arising from a use of ‘view’ from the context (View a, View b) bound by the instance declaration at ...
Could not deduce (Show b) arising from a use of ‘view’ from the context (View a, View b) bound by the instance declaration at ...

So for some reason where / let are tricking the type checker into thinking View requires Show, when it doesn't.

semicolon
  • 2,530
  • 27
  • 37
  • 4
    This is called `MonoLocalBinds` (I think you can turn it off with `-XNoMonoLocalBinds`) - basically, when the values are bound in a `let` block, the constraint is solved seperately for that binding (and it finds a constraint which can be discharged immediately through the first instance, so it does that). Such an instance will perpetually cause you these problems - its existence is a design flaw. – user2407038 Aug 10 '16 at 18:39
  • 2
    @user2407038 It seems like you got it backwards, turning ON MonoLocalBinds actually fixed my issue. It only breaks when it is turned off. But either way thanks, It works now! As for the design flaw aspect, I don't see another way to get the behavior I want, and I really don't see what is so bad about it. Regardless this is for designing a complex algorithm which I will then have to convert into C++, so I don't really mind if it is a design flaw, as the code will not be directly used in a production environment. – semicolon Aug 10 '16 at 19:43
  • 4
    @semicolon "I don't see another way to get the behavior I want": define an instance for every type, even if it is the trivial `view = show` instance. There are safe extensions that make this even easier. "I really don't see what is so bad about it": if you write enough code with these instances lying around, you will inevitably write something that uses the wrong instance -- that is, you will execute a _different piece of code_ than the one you were expecting to execute. Depending on the consumer of the value, this problem could range anywhere from an inconvenience to a security-critical bug. – Daniel Wagner Aug 10 '16 at 21:14
  • @DanielWagner If I avoid orphan instances isn't that pretty easy to make impossible? Particularly if it's a class I wrote in my application code, because then no library can possibly export instances for it because they have no idea my application exists. I can see how such a class would be dangerous in a library, but IMO it isn't dangerous in an application. – semicolon Aug 10 '16 at 22:30
  • @semicolon If you never turn on `IncoherentInstances`, and you never write an orphan instance, you may be okay. Do you trust yourself to remember these restrictions, while you are keeping in mind all the other details of your program? What about twelve months from now when it comes time to maintain your program some -- are you sure you'll remember then? – Daniel Wagner Aug 10 '16 at 23:24
  • @DanielWagner Yes I do very much trust myself to not turn on something with word "Incoherent" in the name, particularly seeing as everyone knows it is a very dangerous extension. Likewise I avoid orphan instances because it is pretty well known that they cause problems. So yeah I absolutely will remember. – semicolon Aug 10 '16 at 23:37
  • @semicolon I'm mildly skeptical. You're happy enough using overlapping instances when they serve a need, despite the fact that they're one of the less safe extensions. And it's not just you, of course -- anybody who inherits the code will need to make these judgments as well. Of course in the end the risk is your decision, but make sure you're choosing the risk with care. To me it seems a no-brainer: the safe solution is only a few lines of easily-written code away; probably less than two minutes of effort. – Daniel Wagner Aug 10 '16 at 23:56
  • @DanielWagner I mean `IncoherentInstances` is orders of magnitudes more dangerous than `OverlappingInstances`, it can straight up lead to incorrect behavior, whereas `OverlappingInstances` at worst ends up with different or unexpected behavior, and only if used carelessly. Also defining an orphan instance within a single application when you actually defined the class within said application just seems... moronic. – semicolon Aug 11 '16 at 00:25
  • Using `OverlappingInstances` today means that years down the line when it's really getting in your users' way and driving you batty, you won't be able to get rid of it because you structured your whole interface around it. Use closed type families and MPTC instead; it sucks but less. – dfeuer Aug 11 '16 at 05:20
  • @dfeuer I mean there aren't any users... it's an internal application. I mean it's not even going to be put into a prod environment, it's basically a reference implementation for an algorithm I have been tasked to write that will eventually end up as C++. With that said how would I do this with closed type families and multi param type classes? – semicolon Aug 11 '16 at 14:17

2 Answers2

0

The type family approach looks like this:

{-# LANGUAGE TypeFamilies, MultiParamTypeClasses,
    FlexibleInstances, DataKinds, KindSignatures,
    ScopedTypeVariables #-}

import Data.Proxy


data Name = Default | Booly | Inty | Pairy Name Name

type family ViewF (a :: *) :: Name where
  ViewF Bool = 'Booly
  ViewF Int = 'Inty
  ViewF Integer = 'Inty --you can use one instance many times
  ViewF (a, b) = 'Pairy (ViewF a) (ViewF b)
  ViewF a = 'Default

class View (name :: Name) a where
  view' :: proxy name -> a -> String

instance (Show a, Num a) => View 'Inty a where
  view' _ x = "Looks Inty: " ++ show (x + 3)

instance a ~ Bool => View 'Booly a where
  view' _ x = "Looks Booly: " ++ show (not x)

instance Show a => View 'Default a where
  view' _ x = "Looks fishy: " ++ show x

instance (View n1 x1, View n2 x2) => View ('Pairy n1 n2) (x1, x2) where
  view' _ (x, y) = view' (Proxy :: Proxy n1) x ++ "," ++ view' (Proxy :: Proxy n2) y


view :: forall a name .
        (ViewF a ~ name, View name a)
     => a -> String
view x = view' (Proxy :: Proxy name) x

-- Example:

hello :: String
hello = "(" ++ view True ++ view (3 :: Int)
        ++ view "hi" ++ ")"
dfeuer
  • 48,079
  • 5
  • 63
  • 167
  • Can you explain a little what is going on here? This seems really... indirect... Is there any way to just have a true closed type class? – semicolon Aug 14 '16 at 22:12
  • @semicolon, the idea is to *name* each instance by switching to a multi-parameter type class one of whose parameters is the name. Then the type family selects the appropriate instance, instead of really using Haskell's usual instance selection mechanism. There are a few minor variations on the technique (e.g., you could use an auxiliary type class to push the equality constraint into an instance constraint instead of putting it on a binding). There is no such thing as a true closed type class; this sort of thing is the best you can do. – dfeuer Aug 18 '16 at 00:46
  • Ok I think that makes sense, is there any chance that Haskell will get some form of true closed type classes? Because that would be really nice. Or is there any reason why they should not exist? – semicolon Aug 18 '16 at 03:34
  • @semicolon, some years ago a now-defunct research group came up with "instance chains" as a better-behaved approach to overlapping. I don't know of any plans to add those, or anything similar, to GHC. – dfeuer Aug 18 '16 at 04:07
  • I have since done a lot more with `TypeFamilies` so this makes a lot more sense now. Couldn't you just have two `Name`s, `Default` and `Override`, then `instance Show a => View 'Default a` and `instance View 'Override `, `instance View 'Override `. To limit the boilerplate a little? – semicolon Mar 01 '17 at 06:03
  • @semicolon, I really don't know what you mean. Can you explain more (or, better, try it and see if it compiles and report back)? – dfeuer Mar 01 '17 at 12:58
  • http://pastebin.com/raw/1f72vbTe is what I was more or less thinking of. Looks like my change may have forced me to use undecidable instances though, hmm. Perhaps you need extra types to deal with pairs and such if you don't want undecidable instances. – semicolon Mar 01 '17 at 19:42
0

Thanks @dfeuer for providing an alternative approach, but I figured I should write the fairly quick fix for the question itself:

{-# LANGUAGE MonoLocalBinds #-}

Once that is put at the top of the file everything works just fine. From the research I did it seems like certain extensions (I guess including OverlappingInstances) poke holes in local binding type checking, and MonoLocalBinds sacrifices using local binds polymorphically in order to fix those holes.

semicolon
  • 2,530
  • 27
  • 37