Cleaning up function type signatures with type classes
Given a function s开发者_开发技巧ignature like this (using the riak package):
put :: (Storable a, Resolvable a, ToJSON a, FromJSON a) => Connection -> a -> IO ()
would it be a bad idea to clean up the function signature by defining a type class like:
class (Storable a, Resolvable a, ToJSON a, FromJSON a) => Persistable a
put :: (Persistable a) => Connection -> a -> IO ()
I'm more than happy to hear that this is a stupid idea, but if so, can you tell me why?
Secondly, assuming it's not a bad idea, I can define a catch-all instance by using UndecidableInstances like so:
instance (Storable a, Resolvable a, ToJSON a, FromJSON a) => Persistable a
However, there has recently been some discussion on Haskell-cafe about the cons of using UndecidableInstances, with the general consensus seemingly that requiring it's use points to poor design decisions. Is it a justified usage in this case, or is it better to require explicit boilerplate instances for each type class implementing Persistable?
First of all, "more boilerplate" is almost never the best option. For example, using TH to auto-generate instances is another alternative that some people might find more palatable.
That said, I think UndecidableInstances
gets a bad rap. Really, what's the worst-case scenario? It makes the compiler go into an infinite loop? A mere trifle. You can write a few lines of pure Haskell 98 that will trigger the worst case complexity for H-M type inference and leave GHC grinding to a halt as it consumes gigabytes of memory, which is frankly worse than an infinite loop that just burns CPU cycles.
The important thing is that the pathological behavior doesn't occur if you're writing sensible code, and is generally easy to diagnose if you do hit it. You won't get subtle bugs, or weird behavior in instance selection, and you certainly won't get invalid programs to compile. This is no different than the possibility of nontermination at the value level that we deal with all the time in programs. In some ways, it's better, because it only causes errors at compile time, not run time.
As it stands, the termination checks used for instances are ridiculously conservative. There are plenty of cases that are provably correct, but can't be written without UndecidableInstances
. And while it would be nice to have a smarter termination checker, or extra features to support common clearly-safe cases (cf. the context alias proposal Heatsink mentioned), there's really no harm at all in using UndecidableInstances
to implement what you want right now. The most I'd suggest is to isolate its use to a single module with just the aliases, so that you don't have to worry about unintentionally writing non-terminating instances elsewhere.
If memory serves me, most of these points were made by Oleg on haskell-cafe, though I don't have a link handy. He does talk about it on his website, though.
In contrast, I think OverlappingInstances
probably deserves a worse reputation than it currently has. :] It's far more conceptually problematic than UndecidableInstances
, to my mind.
I wouldn't recommend it. You get a shorter type signature for put
, but will have to litter your code with boilerplate Persistable
instances. Solving this (second-order) problem with UndecidableInstances
feels like using a steam hammer to crack nuts, since UndecidableInstances
can make the typechecker go in an infinite loop potentially. In my opinion, the cons outweigh the pros.
Context aliases present a more attractive solution, but unfortunately are not supported in any existing compiler.
精彩评论