pozorvlak ([info]pozorvlak) wrote,
@ 2007-03-10 12:57:00
Previous Entry  Add to memories!  Tell a Friend!  Next Entry
Entry tags:beware the geek, computers, haskell, programming, rants

Haskell and pusillanimity, part the second
A while ago I wrote a post about what appeared to be the endemic pusillanimity of the Haskell community, tried to find more charitable explanations for my observations, and, I think, mostly succeeded. Unfortunately, the example I gave turned out not to be a good one: it turns out that it is possible to have integer-parametrized types in Haskell (though you have to go through Hell and high water to do so). But the other day, I remembered a much better example.

I'll tell you how I encountered this problem, as a way of leading up to it. I was debugging some code that dealt with deeply-nested lists. Reading the output of "show" was getting painful, and I knew that what I really needed was something like Perl's Data::Dumper, which produces output like the following:

perl -MData::Dumper -e 'print Dumper { fred => [1,2,[3,4]], barney =>
 [[1,2,[3,4]],[1, { wilma => 3, betty => 4 }]] }'
$VAR1 = {
          'barney' => [
                        [
                          1,
                          2,
                          [
                            3,
                            4
                          ]
                        ],
                        [
                          1,
                          {
                            'betty' => 4,
                            'wilma' => 3
                          }
                        ]
                      ],
          'fred' => [
                      1,
                      2,
                      [
                        3,
                        4
                      ]
                    ]
        };
[There's also a CPAN module called Data::Dump::Streamer, which can handle data structures containing closures, printing out the code in the closure (as of v1.11, including bound variables). This makes writing and debugging higher-order functions vastly easier.]

I couldn't find anything like this in the libraries or online, so I had to write my own. Shouldn't be too hard, I thought. OK, so I wanted some polymorphic function to do the dumping, which meant (deep breath) that I was going to have to use typeclasses. So, I defined a typeclass PPrint of pretty-printable data structures, with a single method pprint. Obviously, this function is going to handle lists and maps via recursion, and writing the code for lists was pretty straightforward. But what about the base case? Well, I wanted pprint to default to "show" when I hadn't defined anything else for it to do. I'd also like this falling-back to happen automatically, so my user (ie, me) wouldn't have to write a boilerplate PPrint instance for every single datatype involved in any structure they wanted to pretty-print (which would involve editing code and recompiling, which would break the flow of debugging). Easy enough:
instance (Show a) => PPrint a where pprint = show
Except:
  Illegal instance declaration for `PPrint a'
        (The instance type must be of form (T a b c)
         where T is not a synonym, and a,b,c are distinct type variables)
    In the instance declaration for `PPrint a'
Well, that's not the least comprehensible error message that GHC's given me by some distance, but it still took a few emails to [info]totherme to work out what was going on. Here's the relevant section from YAHT:
The “Eq a =>” part of the definition is called the “context.” We should note that here are some restrictions on what can appear in the context and what can appear in the declaration. For instance, we’re not allowed to have instance declarations that don’t contain type constructors on the right hand side. To see why, consider the following declarations:
class MyEq a where         myeq :: a -> a -> Bool
 instance Eq a => MyEq a where
         myeq = (==)
As it stands, there doesn’t seem to be anything wrong with this definition. However, if elsewhere in a program we had the definition:
instance MyEq a => Eq a where
         (==) = myeq
In this case, if we’re trying to establish if some type is an instance of Eq, we could reduce it to trying to find out if that type is an instance of MyEq, which we could in turn reduce to trying to find out if that type is an instance of Eq, and so on. The compiler protects itself against this by refusing the first instance declaration.
Because clearly there's no known algorithm for detecting a loop in a graph.

For goodness' sake.



(Post a new comment)

I just ran into this
[info]ryani
2007-07-04 02:10 am UTC (link)

Strangely, it also had to do with pretty printing.

One bit of advice is that in ghc you can use -fglasgow-exts to turn on all sorts of nice typeclass features, and there are more flags to go further.

One thing I don't like about ML-style languages without typeclasses is how you have to qualify the names of all your generic functions with the type that they handle. So you have pprintString, pprintInt, pprintMyNewType, etc. Typeclasses simplify that, and theoretically the compiler should be able to statically determine the instance of the typeclass to call in almost all cases. So it's natural to write

class PPrint a where
  pprint  :: a -> String
  pprints :: a -> ShowS
  where pprint = flip pprints ""

But in my application, I often had context data that affected how I wanted something to print. A simple solution would be to instantiate PPrint on tuples, but at the time I thought that another function would be clearer:

class PPrintCtxt ctxt a where
  pprintc :: ctxt -> a -> ShowS

One context I used was integers, which specified that the output should be fixed-width:

instance PPrintCtxt Int Addr where
  pprintc width (MkAddr a) = s ++ spaces where
    s = "#" ++ show a
    l = width - length s
    spaces = [' ' | _ <- [1..l]]

Then I ran into a problem: pprintc 5 addr didn't typecheck! This is because 5 doesn't have type Int, but rather type (Num a => a), and PPrintCtxt has no instance for that type. Annotating the code with an explicit type constructor (5 :: Int) felt wrong to me.

The obvious answer is this:

instance Num a => PPrintCtxt a Addr where ... 

but that doesn't work, and worse, it overlaps with any other instance of PPrintCtxt t Addr.

I'd rather the typeclass system was Turing-complete, personally. The instance declaration looks so tantalizingly like Prolog that it's frustrating and a bit jarring when it can't do prolog-style search.

I'd rather my compile didn't terminate than my runtime didn't terminate, and I'd much rather have additional power available at the expense of occasionally having to press ^C because I made an error that the compiler was unable to detect because it can't solve the halting problem.

(Reply to this)(Thread)

Re: I just ran into this
[info]pozorvlak
2007-07-04 10:36 am UTC (link)
The thing is, I'm coming from duck-typed languages, which manage to avoid the whole problem in (IMHO) a more elegant manner - in effect, every function is polymorphic over the implicit typeclass of "things which support exactly the interface it needs". For this to work, of course, you need to do away with Haskell's restriction that each symbol can have only one definition, which in turn makes it harder to use currying. Haskell pays quite a heavy price for that feature! Including, it seems, the possibility of writing a general data-structure pretty printer à la Data::Dumper :-(

It's possible to do at least some interesting Prolog-type stuff in the type system, though it's not at all easy - see the comments to this post for some links. I tried to use it to test type-level integers for primality once, and gave up in despair.

I'd rather my compile didn't terminate than my runtime didn't terminate
Interesting... I'd rather both were Turing-complete, and hence not guaranteed to terminate. I think this puts me out of sympathy with 90% of the Haskell community, though :-)

(Reply to this)(Parent)

Overlapping and Undecidable instances
[info]cgibbard
2008-01-19 11:46 pm UTC (link)
I don't understand why nobody told you the appropriate compiler option here.

The compiler option "-fallow-undecidable-instances", or in the new style, "-XUndecidableInstances", or in your source file the pragma: "{-# LANGUAGE UndecidableInstances #-}" will allow the sort of instances you'd like. However, you won't be able to define any others for the given class, because instances of the antecedent class could always later be defined (classes are open), and then you'd have overlaps. To allow for overlapping instances you can use "-fallow-overlapping-instances", "-XOverlappingInstances", or "{-# LANGUAGE OverlappingInstances #-}".

These options significantly increase the computational power of the type language and there are more complicated cases in which it's not possible to detect looping. The problem isn't so much with instances like you've written (that's actually just fine, you end up with an instance of both).

For instance:

class C a where
c :: a -> [Integer]

class D a where
d :: a -> [Integer]

instance D a => C a where
c x = 0 : d x

instance C a => D a where
d x = 1 : c x


Here, the expression c "hello" will evaluate to an infinite list of interleaved 0's and 1's. It's more complicated cases which cause problems.

To read more about overlapping and undecidable instances, including an example of a case which will cause nontermination, see the GHC User's Guide.

(Reply to this)(Thread)

Re: Overlapping and Undecidable instances
[info]pozorvlak
2008-01-21 12:52 am UTC (link)
I can only assume that nobody I asked knew about this compiler option :-) I know I should probably be asking such questions on #haskell, but I really try to avoid IRC because it brings my RSI back with a vengeance (and the couple of times I've tried, I haven't even got helpful answers for my pain).

Anyway, thanks: I'll file this away for when I feel strong enough to try Haskell again :-)

(Reply to this)(Parent)


Create an Account
Forgot your login?
Login w/ OpenID
English • Español • Deutsch • Русский…