caml-list - the Caml user's mailing list
 help / color / mirror / Atom feed
From: Jeremy Yallop <yallop@gmail.com>
To: David Allsopp <dra-news@metastack.com>
Cc: Leo White <lpw25@cam.ac.uk>, OCaml List <caml-list@inria.fr>
Subject: Re: [Caml-list] Match error with abstract types in modules
Date: Tue, 24 Feb 2015 19:52:33 +0000	[thread overview]
Message-ID: <CAAxsn=GsxouTxpsptw_Kk930721iZuhFAY2BObYmjV0Rbk5wkw@mail.gmail.com> (raw)
In-Reply-To: <E51C5B015DBD1348A1D85763337FB6D9E994424B@Remus.metastack.local>

On 24 February 2015 at 18:26, David Allsopp <dra-news@metastack.com> wrote:
> Leo White wrote:
>> > Please could someone remind me what it is about types in modules which
>> > means that
>> >
>> >   module Foo = struct
>> >     type 'a foo
>> >   end
>> >
>> >   type _ t = A : int Foo.foo t
>> >            | B : float Foo.foo t
>> >
>> >   let foo A = ()
>> >
>> > is non-exhaustive pattern matching, but:
>> >
>> >   type 'a bar
>> >
>> >   type _ u = C : int bar u
>> >            | D : float bar u
>> >
>> >   let bar C = ()
>> >
>> > is exhaustive? Or is it actually a bug (given that foo B is a type
>> error)?
>> >
>>
>> If Foo is replaced with:
>>
>>     module Foo : sig
>>       type 'a foo
>>     end = struct
>>       type 'a foo = unit
>>     end
>>
>> so that `int foo` is equal to `float foo`, then your match isn't really
>> exhaustive.
>
> How so?

In order to understand what's going on here it helps to consider two
relations between types: compatibility and equality.

Equality's simple enough: two types are equal if they are the same
(after resolving all alises).  For example,

   float and float are equal

   int and float are not equal

   string -> bool and string -> bool are equal

   int t and int t are equal for any unary type constructor t

   int list and int option are not equal

   float t and int s might be equal if t and s are aliases.  For
example if we have type 'a t = int and type 'a s = 'a then float t and
int s are both equal to int.

Compatibility's a tiny bit more complicated.  Two types are compatible
if it's *possible* to make them equal by some instantiation of
abstract types.  For example:

   equal types are always compatible

   int list and int option are not compatible

   float t and int s are compatible if t and s are abstract types,
since there is a way to define the abtract types that makes float t
and int s equal.

GADTs are mostly about equality.  The fundamental role of GADTs is to
transport type equalities from one part of a program to another.
However, checking compatibility of GADT indexes is sometimes useful as
well.  In particular, checking the compatibility of indexes during
pattern matches allows the compiler to more precisely identify
redundant cases or inexhaustive matches.

We might imagine three possible strategies that the compiler could
take in checking a pattern match involving GADTs for exhaustiveness
and redundancy.  Suppose we have a function that matches over GADTs:

  let f : type a. (a, int) s -> unit = function ...

Then

  (1) The compiler could simply ignore type information and assume
that all cases could potentially be matched.  This was the approach
used in GHC until recently.

  (2) The compiler could check whether there is some instantiation of
locally abstract types that makes the declared type of the case
variable equal to the type of the pattern for each case.  For example,
if s is defined as follows:

      type (_, _) s =
         X : (float, int) s
       | Y : ('b, 'b) s
       | Z : (bool, float) s

   then there are choices for the variable "a" that make (a, int)
equal to the types of X and Y, but no way to choose a type for "a"
that makes (a, int) equal to the type of Z.

      Unfortunately an approach based on equality breaks down when
abstract types are involved.  (I'll show why below.)

   (3) The compiler could take the same approach as in (2), but check
for a way to make types compatible rather than equal.  This is the
approach taken in OCaml.

Let's look at your example.  You have an abstract type Foo.foo and the
following type definition:

    type _ t = A : int Foo.foo t
             | B : float Foo.foo t

Now, suppose you have a function declared like this:

    let f : int t -> unit = function

Then with approach (1) both A and B would be allowed as patterns, and
leaving them off would lead to a warning about exhaustiveness.  With
approach (2) neither A nor B would be allowed, since int t is not
equal to either int Foo.foo t or float Foo.foo t.  With approach (3)
both A and B are allowed as patterns, since int t is compatible with
both int Foo.foo t and float Foo.foo t.  Allowing both A and B as
patterns is the correct option here, since it's possible to call f
with either A or B if foo is instantiated appropriately:

   module F(Foo: sig type _ foo end) =
   struct
       type _ t = A : int Foo.foo t
                    | B : float Foo.foo t
         let f : int t -> unit = function
           A -> ()
         | B -> ()
  end

  include F(struct type _ foo = int end)

  let () = begin f A; f B end

> But you get no warning about an unusable match case with:
>
>    let foo = function A -> () | B -> ()

The reason is that the types of A and B are compatible with each other.

>> Personally, I dislike this behaviour and would prefer both cases to give
>> an error.
>
> What's to dislike? It's rather useful - the constructors of a GADT are partionable by type.

It is indeed useful, and a nice bonus is that it leads to more
efficient code.  However, one (reasonable) point of view is that there
shouldn't be a difference in behaviour between defining something
directly and having a temporary abstraction barrier in between the
definition and the use.  I tend to agree, except that I think the
desirable fix here is not to stop using the compatibility check to
improve pattern matching compilation but to add to the language a way
to express that int Foo.foo and float Foo.foo are incompatible (i.e.
that Foo.foo is injective).

More generally, I think OCaml's whole approach to exhaustiveness
checking for pattern matching with GADTs is an interesting story, and
I'd like to see it written up somewhere (assuming it isn't already).
The Haskell folk are writing a paper about new approach to
exhaustiveness checking for GADT patterns, but the issues that arise
when you have modular abstraction, as in OCaml, are quite different.

Jeremy.

  parent reply	other threads:[~2015-02-24 19:52 UTC|newest]

Thread overview: 10+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2015-02-24 17:25 David Allsopp
2015-02-24 18:02 ` Leo White
2015-02-24 18:26   ` David Allsopp
2015-02-24 19:11     ` Leo White
2015-02-25  8:41       ` David Allsopp
2015-02-24 19:52     ` Jeremy Yallop [this message]
2015-02-24 20:03       ` Ben Millwood
2015-02-24 21:11         ` Leo White
2015-02-25  8:41           ` David Allsopp
2015-02-25  9:48             ` Gabriel Scherer

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to='CAAxsn=GsxouTxpsptw_Kk930721iZuhFAY2BObYmjV0Rbk5wkw@mail.gmail.com' \
    --to=yallop@gmail.com \
    --cc=caml-list@inria.fr \
    --cc=dra-news@metastack.com \
    --cc=lpw25@cam.ac.uk \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for NNTP newsgroup(s).