Thanks for the info! I didn't think about just telling the type system about the variance, and this kind of fix is exactly what I was (eventually) getting at. Unfortunately, this solution doesn't seem to work in my actual code.
I wrote a module to read and write binary data with fixed layouts, and I'm using a GADT to enforce typing. A simplification of my actual code:
type (_,_) field =
| Int8 : ('a,'b) field -> ((int -> 'a),'b) field
| String : (int * ('a,'b) field) -> ((string -> 'a),'b) field
| End : ('b,'b) field
let intstring = Int8 (String (3, End))
In this case, intstring would be used as a template for reading/writing an 8-bit integer, followed by a 3-byte long string. The type would be:
val intstring : (int -> string -> 'a, 'a) field