the MPTC Dilemma (please solve)

Claus Reinke claus.reinke at talk21.com
Wed Feb 15 08:00:19 EST 2006


here's my take on this (disclaimer: I've got a bit of experience in 
type-class-level programming, but I'm not an expert in type systems;).

Isaac pinned the problem to MPTCs and FDs, because the former 
are closely connected to when people started noticing the problem, 
and the latter are intended as a partial solution. the dilemma, as far as 
standardization is concerned, is that there are several variants of FDs, 
and their interaction with other features, and that there is no clear 
optimum in the design space yet (ghc isn't doing too badly in its
pragmatic way, but for a standard, one would prefer a consistent
story).

however, the underlying problem is not limited to MPTCs, and FDs 
are not the only attempt to tackle the problem. so I agree with Isaac: 
getting a handle on this issue is imperative for Haskell', because it will 
be the only way forward when trying to standardize at least those of 
the many extensions that have been around longer than the previous 
standard Haskell 98. and if Haskell' fails to do this, it fails.

to understand my reason for this strong opinion, it is necessary to 
understand that the problem underlying this dilemma not limited to
some optional extensions one might continue to ignore, but instead 
is part of almost all advanced type-class programming - I'll try to 
illustrate with one example.

first, here's a stripped down example of (label-type-based) 
selection of fields from records (as nested tuples):

    -- | field selection
    infixl #?

    class Select label val rec | label rec -> val where
      (#?) :: rec -> label -> val

    instance Select label val ((label,val),r) where
      ((_,val),_) #? label = val

    instance Select label val r => Select label val (l,r) where
      (_,r)       #? label = r #? label

    -- examples
    data A = A deriving Show
    data B = B deriving Show
    data C = C deriving Show
    data D = D deriving Show

    r1 = ((A,True),((B,'a'),((C,1),((D,"hi there"),((B,["who's calling"]),()))))) 

the intention is to select the first field with a matching type of
label (question: is this intention expressed in the code? how?).

this code loads and works, with ghci-6.4.1 (and -fglasgow-exts, 
-fallow-overlapping-instances), and I can select the first B field like this:

    *Main> r1 #? B
    'a'

the same code does *not even load* with Hugs 98 (Nov 2003) +98
(more recent releases seem to have left out the windows platform):

    Hugs mode: Restart with command line option +98 for Haskell 98 mode

    ERROR "C:\Documents and Settings\cr3\Desktop\haskell\haskell prime\Dilemma.hs":1
    0 - Instances are not consistent with dependencies
    *** This instance    : Select a b (c,d)
    *** Conflicts with   : Select a b ((a,b),c)
    *** For class        : Select a b c
    *** Under dependency : a c -> b

this demonstrates the dilemma, and I hope it also shows why I want these
extensions standardized, at the very least as an addendum defining all the
options out there in current use. this is not about saying that every 
implementation has to support FDs, but about clarifying what is supported.

we can modify the example a bit, to get to the underlying problem. here's
a Select', without the functional dependency:

    -- | field selection
    infixl #.

    class Select' label val rec where
      (#.) :: rec -> label -> val

    instance Select' label val ((label,val),r) where
      ((_,val),_) #. label = val

    instance Select' label val r => Select' label val (l,r) where
      (_,r)       #. label = r #. label

now this loads in both ghci and hugs, but it doesn't quite work to our
satisfaction:

    Main> r1 #. B
    ERROR - Unresolved overloading
    *** Type       : Select' B a () => a
    *** Expression : r1 #. B


    *Main> r1 #. B
    <interactive>:1:3:
        Overlapping instances for Select' B val
             ((B, Char), ((C, Integer), ((D, [Char]), ((B, [[Char]]), ()))))
          arising from use of `#.' at <interactive>:1:3-4
        Matching instances:
          Dilemma.hs:10:0: instance (Select' label val r) => 
                                        Select' label val (l, r)
          Dilemma.hs:7:0: instance Select' label val ((label, val), r)
        (The choice depends on the instantiation of `val'
         Use -fallow-incoherent-instances to use the first choice above)
        In the definition of `it': it = r1 #. B

note, btw, that ghci offers a dangerous-sounding workaround, for the 
adventurous, but this isn't actually going to help, because it only offers 
to default to the recursive instance, soon running out of record to 
select from (and no reordering of source-code seems to affect that 
choice):

    *Main> :set -fallow-incoherent-instances
    *Main> r1 #. B
    <interactive>:1:3:
        No instance for (Select' B val ())
          arising from use of `#.' at <interactive>:1:3-4
        Probable fix: add an instance declaration for (Select' B val ())
        In the definition of `it': it = r1 #. B

back to the problem: there are multiple instances matching the
context required to fulfill our selection request, and since the choice
of instance might affect the program result, neither implementation
is willing to commit to one of the choices (unless explicitly encouraged
by a daring option;).

so far, so good. we can resolve the ambiguity with a type annotation:

    Main> r1 #. B ::Char
    'a'

    *Main> r1 #. B::Char
    'a'

everybody happy? not at all! recall our original FD: we wanted the 
result to be dependent on the field selected, not the field selection 
dependent on the result requested!

    Main> r1 #. B ::[String]
    ["who's calling"]

    *Main> r1 #. B::[String]
    ["who's calling"]

oops. if we go back to the original code (and hence ghci only), we
get the error message we expected, because the first B field in r1
holds a Char value, not a [String]:

    *Main> r1 #? B::[String]
    Top level:
        Couldn't match `Char' against `[String]'
          Expected type: Char
          Inferred type: [String]
        When using functional dependencies to combine
          Select label val ((label, val), r),
            arising from the instance declaration at Dilemma.hs:20:0
          Select B
                 [String]
                 ((B, Char), ((C, Integer), ((D, [Char]), ((B, [[Char]]), ())))),
            arising from use of `#?' at <interactive>:1:3-4

by now, you'll probably have an idea what the problem is (at least,
the problem as I see it): selection from ambiguous instances, and
committing to one of several choices. 

to hammer home the point that this is not specific to MPTCs, but 
inherent in complex type-class level programming, here's the 
ambiguous version again, without any MPTCs:

    class Select' t where
      (#.) :: t

    instance Select' (((label,val),r)->label->val) where
      ((_,val),_) #. label = val

    instance Select' (r->label->val) => Select' ((l,r)->label->val) where
      (_,r)       #. label = r #. label

this version loads fine into hugs, but not into ghci:

    *Main> :r
    Compiling Main             ( Dilemma.hs, interpreted )
    Dilemma.hs:10:0:
        Non-type variables in constraint: Select' (r -> label -> val)
        (Use -fallow-undecidable-instances to permit this)
        In the context: (Select' (r -> label -> val))
        While checking the context of an instance declaration
        In the instance declaration for `Select' ((l, r) -> label -> val)'
    Failed, modules loaded: none.

undecidable instances is ghc jargon for "i don't have to care what
these instances look like. just don't blame me.". once we've signed
that disclaimer, we get the same problems, without any MPTCs
in sight:

    Main> r1 #. B ::Char
    'a'
    Main> r1 #. B ::[String]
    ["who's calling"]

    *Main> r1 #. B::Char
    'a'
    *Main> r1 #. B::[String]
    ["who's calling"]

the reason that this problem starts to show up with MPTCs is that,
with conventional limitations on instances in place, MPTCs were one
of the first extensions that permitted multiple variables to be constrained
by one type-class constraint. once those limitations are relaxed (as
well they should be), there are other uses of type classes that exhibit
the same issue.

the reason that FDs are connected with this problem is that they are
trying to address part of it, by allowing us to turn type-level relations
(type-class constraints involving multiple types) into type-level
functions, so that resolving only a subset of type variables in such a
constraint will be sufficient to commit to an instance. 

so, once again, the problem is *selection from possibly ambiguous
instances*, and *control over that selection*. and this problem seems
to be inherent in type-class-level programming, with or without
MPTCs or FDs. the problem is aggravated by the wish to support
separate compilation of modules, because it means that the 
implementation can never know that there won't be another
fitting instance just around the corner.. this leads to the slightly
ridiculous situation that the implementation will sometimes not
even select the one and only instance we ever intended to write. 

note that I said "possibly ambiguous", because, really, there 
shouldn't be any ambiguity in this case (and in many other cases).
usually, it is just that Haskell (or the Haskell+ implementation at
hand) does not give us the means to express our intentions in
an unambiguous form. and that is not for not trying, either. to wit,
have a look at some of the language features that have tried to
address parts of the problem (or that have been proposed):

- defaults (remember those?)
- functional dependencies 
- "most-specific" rule in overlapping instances
- incoherent instances (is the selection documented anywhere?)
- closed classes (yes, dear compiler, you can see *all* instances)

it may also be worth pointing out the one language feature that
does *not* try to help with the problem:

- ignoring contexts when deciding about overlaps

because, often, the contexts establish mutually exclusive 
pre-conditions to those "overlapping" instances. so, this is a lot
like ignoring guards when matching patterns.. and if we were
able to specify negation of type predicates (the characteristic
predicates of those type relations specified by MPTCs), 
avoiding overlaps by such constraints would be even easier.

well, I could go on, but that's probably more than enough for 
now. thanks for reading with us, we hope you had an enjoyable
trip!-)

cheers,
claus



More information about the Haskell-prime mailing list