Saturday, 11 May 2024

Discriminated Unions Part One - The F# side of things

I decided to look more into what the discussion of Discriminated unions in C#, or their lack of it is all about. I will first look at the F# side of things. How can we create a discriminated union in F# ? And then I will look at how we can implement the F# program in C# in the next article for the topic Discriminated unions. In this article we will look at some F# code that shows how discriminated unions are built-in supported in F#. Discriminated unions are special containers that can hold different types. This is not supported in C# without adding some additional plumbing code and it is not considered true discriminated unions, although in C# we can get close to Discriminated unions. For the rest of the article, we will call discriminated unions for DU. Let's first declare a DU in F# that describes different types of geometric figures.


type Shape =
    | Rectangle of width : float * length : float
    | Circle of radius : float
    | Prism of width : float * depth:float * height : float
    | Cube of width : float


The '*' operator in F# means when it is used in type definitions above as a separator of the properties that each type got,
e.g. Rectangle of width : float * length : float means
that the type Rectangle got two properties, width of type float and length of the same type.

Let's add some methods to our F# program, calculating area and calculating volume. We also want our F# to be fault tolerant so either we get a result or we get an error, for example this additional DU which is also generic.

type Result<'T> =
    | Success of 'T
    | Error of string    

We also neeed a way to print errors if we want to not crash the program, say if want to calculate the volume of a circle or a rectangle, which is not supported since it is 2D figures.


let handleResult (result: Result<float>) =
    match result with
    | Success value -> printfn "%A" value
    | Error msg -> printfn "Error: %s" msg; () // Return NaN for error cases


To add some functionality to the discriminated unions we add the module below:


module ShapeOperations =
    let CalcArea(shape : Shape) : Result<float> =
        match shape with 
        | Rectangle (width, length) -> Success(width * length)
        | Circle (radius) -> Success(Math.PI * radius**2)
        | Prism (width, depth, height) -> (2.0*(width*depth) + 2.0*(width+depth)*height)
        | Cube (width) -> Success(6.0 * width * width)
        // | _ -> failwith "Area calculation is not supported"
    let CalcVolume(shape : Shape) : Result<float> = 
        match shape with 
        | Prism (width, height, depth) -> Success(width * height * depth)
        | Cube (width) -> Success(width**3)        
        | _ -> Error(sprintf "Volume calculation is not supported  for: %A" shape)


The rest of the code is shown below where we instantiate geometric figures and calculate the area and volume of them and output their values.

 
let rect = Rectangle(length = 1.3, width = 10.0)
let circle = Circle (2.0)
let prism = Prism(width = 15, depth = 5.0, height = 7.0)
let cube = Cube(3)

let rectArea = ShapeOperations.CalcArea rect 
let circleArea = ShapeOperations.CalcArea circle
let prismArea = ShapeOperations.CalcArea prism
let cubeArea = ShapeOperations.CalcArea cube

let circleVolume = handleResult (ShapeOperations.CalcVolume circle)
let prismVolume = ShapeOperations.CalcVolume prism
let cubeVolume = ShapeOperations.CalcVolume cube
let rectVolume = ShapeOperations.CalcVolume rect

printfn "\nAREA CALCULATIONS:"
printfn "Circle area: %A" circleArea
printfn "Prism area: %A" prismArea 
printfn "Cube area: %A" cubeArea 
printfn "Rectangle area %A" rectArea 

printfn "\nVOLUME CALCULATIONS:"
printfn "Circle volume: %A" circleVolume 
printfn "Prism volume: %A" prismVolume 
printfn "Cube volume: %A" cubeVolume 
printfn "Rectangle volume: %A" rectVolume          
                      

We get this output after running the program :


Error: Volume calculation is not supported  for: Circle 2.0

AREA CALCULATIONS:
Circle area: Success 12.56637061
Prism area: Success 430
Cube area: Success 18.0
Rectangle area Success 13.0

VOLUME CALCULATIONS:
Circle volume: ()
Prism volume: Success 525.0
Cube volume: Success 27.0
Rectangle volume: Error "Volume calculation is not supported  for: Rectangle (10.0, 1.3)"


As we can see, creating DUs in F# is easy, we use the '|' operator to define multiple types and we can create generic DUs too and match different types with functional expressions. In the next article we will look at the code shown here and test out if we can recreate it in C# using different constructs. C# has gotten more support of functional programming in 2020 and most likely it will involve records, pattern matching (newer switch based syntax) and extension methods.

1 comment:

  1. Nice article.
    In calcArea for Prism, the call to the Success method is missing.

    ReplyDelete