20

What are the best ways to use F# Discriminated Unions from C#?

I have been digging into this problem for a while, I have probably found the simplest way, but as it is rather complex, there may be some other thing I don't see...

Having a discriminated union, e.g.:

type Shape =
    | Rectangle of float * float
    | Circle of float

the usage from C# I found would be (avoiding using vars, to make the type obvious):

Shape circle = Shape.NewCircle(5.0);
if (circle.IsCircle)
{
    Shape.Circle c = (Shape.Circle)circle;
    double radius = c.Item;
}

In C#, the NewXXXX static methods always create object of the Shape class, there is also a method IsXXXX to check if the object is of the type; if and only if yes, it is castable to the Shape.XXXX class, and only then its items are accessible; constructor of the Shape.XXXX classes are internal, i.e. unaccessible.

Is anyone aware of a simpler option to get the data from a discriminated union?

ildjarn
  • 62,044
  • 9
  • 127
  • 211
Tomas Pastircak
  • 2,867
  • 16
  • 28
  • Must you use the discriminated union? All of the F#/C# interop code bases I have seen expose typed factories around this sort of thing to make it nicer. AFAIK there is no simpler way. – Simon Whitehead May 24 '14 at 09:48
  • 2
    Duplicate of [What is the simplest way to access data of an F# discriminated union type in C#?](http://stackoverflow.com/questions/17254855/what-is-the-simplest-way-to-access-data-of-an-f-discriminated-union-type-in-c) and also http://stackoverflow.com/questions/5090770/fsharpchoice-in-c-sharp – Mauricio Scheffer May 24 '14 at 14:52
  • Concrete example: https://github.com/mausch/EdmundsNet/blob/b5ca7a7d7a883f4f0b2f7f7a7af032534e792cdb/EdmundsNet/Vehicles.fs#L119-L129 – Mauricio Scheffer May 24 '14 at 14:53
  • For now I will stick with `dynamic` in C#. `dynamic circle = ...; double radius = circle.Item;` – nawfal Jul 15 '14 at 10:50
  • @nawfal using `dynamic` here will prevent you from doing static analysis on the types (one of the main benefits of discriminated unions) – Mauricio Scheffer Jul 24 '14 at 17:57
  • @MauricioScheffer yes. Given the limitation today it was an easy alternative I suggested. – nawfal Jul 25 '14 at 07:16

3 Answers3

18

If you are writing a library in F# that is exposed to C# developers, then C# developers should be able to use it without knowing anything about F# (and without knowing that it was written in F#). This is also recommended by F# design guidelines.

For discriminated unions, this is tricky, because they follow different design principles than C#. So, I would probably hide all processing functionality (like calculating area) in the F# code and expose it as ordinary members.

If you really need to expose the two cases to C# developers, then I think something like this is a decent option for a simple discriminated union:

type Shape =
    | Rectangle of float * float
    | Circle of float
    member x.TryRectangle(width:float byref, height:float byref) =
      match x with
      | Rectangle(w, h) -> width <- w; height <- h; true
      | _ -> false
    member x.TryCircle(radius:float byref) =
      match x with
      | Circle(r) -> radius <- r; true
      | _ -> false

In C#, you can use it in the same way as the familiar TryParse methods:

int w, h, r;
if (shape.TryRectangle(out w, out h)) { 
  // Code for rectangle
} else if (shape.TryCircle(out r)) {
  // Code for circle
}
Tomas Petricek
  • 240,744
  • 19
  • 378
  • 553
  • 3
    Why do you always recommend non-exhaustive matching, when it's very easy to do it exhaustive? – Mauricio Scheffer May 24 '14 at 14:46
  • 2
    Mainly because the approach above is easy to use and follows standard coding style that people in C# know already (e.g. it is quite common to use `TryParse` functions and it does not require you to put all your code inside lambda - most C# uses of lambda functions are fairly small-scope IMO). – Tomas Petricek May 24 '14 at 15:54
  • 2
    Would you please at the very least mention the word "totality" in these answers, so that people have something to look up to properly evaluate this? Please at least consider including objective terms (e.g. totality) in addition to subjective/fuzzy ones (e.g. familiarity/idiomatic). I've wasted countless hours in my career because I didn't know these things. I wouldn't like other people to go through the same pain. – Mauricio Scheffer May 25 '14 at 05:11
7

According to the F# spec, the only interop available is through the following instance methods

  • .IsC...

  • .Tag (which gives an integer tag to each case)

  • .Item (on the subtypes to get the data - this is only present when there is more than one union case)

However, you are free to write methods in the F# to make the interop easier.

John Palmer
  • 25,356
  • 3
  • 48
  • 67
4

Assuming that we need to calculate the area of each Shape polymorphically.

In C# we would normally create a hypothetical object hierarchy and a Visitor. In this example, we would have to create a ShapeVisitor class and then a derived ShapeAreaCalculator visitor class.

In F#, we can use Pattern Matching on the Shape type:

let rectangle = Rectangle(1.3, 10.0)
let circle = Circle (1.0)

let calculateArea shape =
    match shape with
    | Circle radius -> 3.141592654 * radius * radius
    | Rectangle (height, width) -> height * width

let rectangleArea = calculateArea(rectangle)
// -> 1.3 * 10.0

let circleArea = calculateArea(circle)
// -> 3.141592654 * 1.0 * 1.0
Nikos Baxevanis
  • 10,868
  • 2
  • 46
  • 80