11

As far a I can tell, F# doesn't have any support for existential types. So, I'm looking for another way to express my idea.

I've got a data structure and its contents can be interpreted in a number of different ways. In this particular example I'll suppose it can be viewed as an int or a real:

type Packed = (* something sensible *) unit

type PackedType = | PackedInt
                  | PackedReal


let undefined<'a> : 'a = failwith "undefined"

let unpackInt : Packed -> int = undefined
let unpackReal : Packed -> real = undefined

let packInt : int -> Packed = undefined
let packReal : real -> Packed = undefined

where real is something meaningful, say:

type real = int * int

let addReal : real -> real -> real = undefined

Now, I need a function addPacked : PackedType -> Packed -> Packed -> Packed. I'd like it to be general, that is:

type NumberOp = [forall t] { opPack : 't -> Packed; opUnpack : Packed -> 't; opAdd : 't -> 't -> 't }

let getNumberOp (t : PackedType) =
    match t with
    | PackedInt -> { opPack = packInt; opUnpack = unpackInt; opAdd = (+) }
    | PackedReal -> { opPack = packReal; opUnpack = unpackReal; opAdd = addReal }


let addPacked (t : PackedType) (a : Packed) (b : Packed) =
    let { opPack = pack; opUnpack = unpack; opAdd = add } = getNumberOp t
    pack <| add (unpack a) (unpack b)

Here I ended up with NumberOp being existetial. So I'm asking whether there is another way to express this. I can't alter Packed, [un]pack* functions and addPacked's type.

I've found this answer. It states that there is a “well-known pattern”, but the text is difficult to read and I couldn't make it work.

kirelagin
  • 13,248
  • 2
  • 42
  • 57
  • 2
    See also http://stackoverflow.com/questions/15690053/impredicative-polymorphism-in-f – Mauricio Scheffer Apr 29 '13 at 18:18
  • How would this type-check? You are trying to add the unpacked versions of `a` and `b` in `addPacked` but add has signature `'a -> 'a -> 'a` and you don't know if the types of what is packed in `a` and `b` are the same. – namesis Jun 23 '22 at 12:37
  • @namesis it is I who chooses how to interpret the packed value – kirelagin Sep 04 '22 at 21:59
  • With reflection? – namesis Sep 05 '22 at 22:35
  • That is just how my definition works: the pack/unpack functions are allowed to be used in any combination, e.g. I can do `packInt 10` and then call `unpackReal` on the result and get something meaningful. Or I might call `unpackInt` on the result instead—it is just my choice. – kirelagin Sep 07 '22 at 00:22

1 Answers1

17

In general, you can encode the type

∃t.F<t>

as

∀x.(∀t.F<t> → x) → x

Unfortunately, each universal quantification requires creating a new type in F#, so a faithful encoding requires two types in addition to F. Here's how we can do this for your example:

type 't NumberOps = {
    opPack : 't -> Packed
    opUnpack : Packed -> 't
    opAdd : 't -> 't -> 't 
}

type ApplyNumberOps<'x> = 
    abstract Apply :  't NumberOps -> 'x

// ∃ 't. 't NumberOps
type ExNumberOps =
    abstract Apply : ApplyNumberOps<'x> -> 'x

// take any 't NumberOps to an ExNumberOps
// in some sense this is the only "proper" way to create an instance of ExNumberOps
let wrap n = { new ExNumberOps with member __.Apply(f) = f.Apply(n) }

let getNumberOps (t : PackedType) =
    match t with
    | PackedInt -> wrap { opPack = packInt; opUnpack = unpackInt; opAdd = (+) }
    | PackedReal -> wrap { opPack = packReal; opUnpack = unpackReal; opAdd = addReal }

let addPacked (t : PackedType) (a : Packed) (b : Packed) =
    (getNumberOps t).Apply 
        { new ApplyNumberOps<_> with 
            member __.Apply({ opPack = pack; opUnpack = unpack; opAdd = add }) = 
                pack <| add (unpack a) (unpack b) }

There's a lot of boilerplate here, unfortunately. Also, I've used the unhelpful names Apply for the abstract members of each helper type - feel free to substitute something more meaningful if you can find names that you prefer. I've tried to keep fairly close to your style, but in my own code I'd probably avoid destructuring the record within addPacked, using the field accessors directly instead.

kvb
  • 54,864
  • 2
  • 91
  • 133
  • Thanks. But I've spent a few hours trying to understand this, and still I don't quite get how the code works. Even worse, I hoped that looking at the inferred types will make things a bit more clear, but the code fails to compile. The compiler says that `Apply` in your `new ApplyNumberOps<_>` has a wrong type and as such doesn't override the abstract method. – kirelagin Apr 30 '13 at 12:54
  • 1
    @kirelagin - If you use anything other than `unit` as the definition for `Packed` it will compile - `unit` is a bit special due to .NET interop and doesn't always play nicely with generics. – kvb Apr 30 '13 at 13:46
  • 1
    Just to add my little grain of sand; I've been searching for something like this the past week. Here is a great article about exactly this that touches a bit of every component that leads to an implementation of existential types. https://www.gresearch.co.uk/2018/08/17/squeezing-more-out-of-the-f-type-system-introducing-crates/ – Rodrigo Oliveri Dec 09 '19 at 20:09
  • Is there an intuitive explanation about the equivalence between `∃t.F` and `∀x.(∀t.F → x) → x` ? – namesis Jun 23 '22 at 12:30
  • @namesis A somewhat handwavy explanation looks at it like a game: You get to pick any type `x` and give me any generic function `∀t.F → x`; if you do so, I promise to give you a value of type `x`. If I have a value of type `F` for some specific type `u`, then this is easy: I can just use `u` as the generic type argument and pass my value to the function and return you the output. So that direction of the equivalence is fairly intuitive. – kvb Jun 23 '22 at 16:08
  • @namesis For the other direction, there's no way to produce a value of some arbitrary type (well, in F# you can use `Unchecked.defaultof<_>`, but let's consider that "cheating"), and the only thing you're giving me that I can use to produce one is that generic function, so I must have a value of F (at some type) if I'm able to give you an `x` for whatever `x` you choose. – kvb Jun 23 '22 at 16:10