Generic functions on discriminated unions

81 views
Skip to first unread message

Ryan Stradling

unread,
Jan 7, 2016, 10:26:07 PM1/7/16
to F# Discussions


    type Entity<'T> = {InsertTime : DateTime; EventTime : DateTime; Value : 'T}
    type EntityWholeNumber = Entity<uint64>
    type EntityInteger = Entity<int64>
    type EntityReal = Entity<double>


    module EntitySeq=
        type EntitySeq =
            | WholeNumber of seq<EntityWholeNumber>
            | Integer of seq<EntityInteger>
            | Real of seq<EntityReal>

        let last<'T> (x : EntitySeq) : Entity<'T> =
            match x with
            | WholeNumber w -> Seq.last w
            | Integer i -> Seq.last i
            | Real r -> Seq.last r

For the function last I get...
The type ''T' does not match the type 'uint64'

Please note that I really want only a sequence of WholeNumbers, Integers or Reals for a given EntitySeq. I do not want a sequence that could contain whole numbers, integers, and reals for a given Seq. I could model this using classes and inheritance but is there a way without going the OO route. There does not seem to be a straight forward way to do this but will be happy to hear alternatives.


Thanks, Ryan


Christopher Atkins

unread,
Jan 8, 2016, 10:48:02 AM1/8/16
to F# Discussions
You should see a warning that 'T is being constrained to uint64 (something like, "this construct causes the code to be less generic..."). If you were to re-order the match cases in your last function, you'd see that 'T was constrained to a different type.

At first blush I'd recommend removing your EntitySeq type and the Entity... type aliases altogether. The #seq<'T> flexible type that is usually inferred for generic functions is usually sufficient.  Should you really need to do something differently based on the type of 'T somewhere, you can always do something like this: match entity with :? Entity<uint64> -> ... Just bear in mind the Liskov substitution principle.

I suspect that you need to constrain 'T to types that can be e.g. added/substracted/multiplied. If that is the case, you should look into statically resolved type parameters; this article by Tomas Petricek might be a good place to start.

Scott Wlaschin

unread,
Jan 8, 2016, 11:38:23 AM1/8/16
to F# Discussions
I don't think you could model your original design in OO either, as EntityWholeNumber, EntityInteger, etc. do not share a common superclass, and so the return type of the "last" function is ambiguous.

To have a well-defined return type, you will need to create a new type, say SingleEntity that has a case for each of your entity types (equivalent to a superclass). Here's what I mean:

module EntitySeq=

    type SingleEntity =
        | WholeNumber of EntityWholeNumber
        | Integer of EntityInteger
        | Real of EntityReal

    type EntitySeq =
        | WholeNumberSeq of seq<EntityWholeNumber>
        | IntegerSeq of seq<EntityInteger>
        | RealSeq of seq<EntityReal>

    let last x =
        match x with
        | WholeNumberSeq w -> WholeNumber (Seq.last w) 
        | IntegerSeq i -> Integer (Seq.last i)
        | RealSeq r -> Real (Seq.last r)


This whole approach seems a bit over-constrained to me though.

Either a function is generic (in which case you really do not care what the type parameter is) or it isn't -- your EntitySeq is not a generic type, and so you should not expect to be able to create generic functions for it.

Personally, I would probably just use raw sequences and let the type checker ensure that everything matches up. :)



Isaac Abraham

unread,
Jan 8, 2016, 12:45:23 PM1/8/16
to F# Discussions
Will this really compile? Look at this code sample which is invalid F#: -

let foo =
    function
    | 1 -> 1UL
    | 2 -> 2.
    | _ -> 3L

So whilst there may be question about the best way to model this and whether there's a difference between OO and FP approaches, F# won't compile code where an expression can return different types from alternate branches - even different numeric types. So unless you convert all the numbers to ints (losing precision etc. - which defeats the object of what you're trying to do) or boxing i.e. going outside the type system, I don't think that this approach won't work - you can't have a method of type T whose type will change depending on a branch of an expression (unless you return different cases from a discriminiated union :-)

I

Yawar Amin

unread,
Nov 5, 2016, 6:11:38 PM11/5/16
to F# Discussions
Hi Ryan, I'm slightly late to the party :-) but you can achieve this with a typeclass. Actually a typeclass is perfect for this kind of use case because it gives you the best of both worlds: parametric polymorphism with static dispatch. Let me illustrate.

// entity.fsi
module Entity =
  /// Your entity record type.
  type 'a t = { insert_time : DateTime; event_time : DateTime; a : 'a }

  /// 'Last' typeclass. Implementation is hidden so users can't just
  /// create new instances at will.
  type 'a last

  /// Main operation of the typeclass. Takes a typeclass instance and a
  /// sequence of the type we're interested in, and returns the last
  /// element of the sequence.
  val last : 'a last -> 'a seq -> 'a

  // Typeclass instances for the types we're interested in.

  val last_whole_num : uint64 t last
  val last_integer : int64 t last
  val last_real : double t last

// entity.fs
module Entity =
  open System

  type 'a t = { insert_time : DateTime; event_time : DateTime; a : 'a }
  type 'a last = { apply : 'a seq -> 'a }

  let last last_t = last_t.apply

  let last_whole_num : uint64 t last = { apply = Seq.last }
  let last_integer : int64 t last = { apply = Seq.last }
  let last_real : double t last = { apply = Seq.last }

Now client code can do e.g. Entity.last last_integer [ ... ] or whatever other type you may have. This looks weird but in practice you can pass in the specific instance to functions that work with the entities, and the functions themselves can be blissfully unaware of exactly which concrete type they're working with, because they have the Entity.last generic operation.

HTH,

Yawar

On Thursday, January 7, 2016 at 10:26:07 PM UTC-5, Ryan Stradling wrote:
Reply all
Reply to author
Forward
0 new messages