CarbonFlambe
CarbonFlambe

Reputation: 378

F#: How to represent a finite collection with strong typing?

I have a finite set of things all of the same type, and I wish to represent them in a strongly-typed way. I'd like to be able to manipulate the complete set and easily extract the elements. Here is one way:

type Planet = Mercury | Venus | Earth
type PlanetInfo = { Diameter: float }
let planets =
    Map [ Mercury, { Diameter = 100. }
          Venus, { Diameter = 200. }
          Earth, { Diameter = 300. } ]
let venusDiameter = planets.[Venus].Diameter

The good points about this method are:

  1. There are exactly three Planets, as defined by the discriminated union.
  2. We have the whole set in the map planets, which can be manipulated, iterated etc..
  3. planets.[Mars] would cause an error, because "Mars" is not a Planet.

But on the downside:

  1. There is not necessarily a one-to-one mapping between the union and the map. The need to mention each planet twice is a shortcoming. Here is another method which addresses the last point:
type Planet = { Name: string; Diameter: float }
let planets =
    [ { Name = "Mercury"; Diameter = 100. }
      { Name = "Venus"; Diameter = 200. }
      { Name = "Earth"; Diameter = 300. } ]
    |> List.map (fun e -> e.Name, e)
    |> Map
let venusDiameter = planets.["Venus"].Diameter

So now each planet is mentioned in only one place, but planets.["Mars"] fails to cause a compile-time error because the planet identifiers are now "stringly typed".

Is there some way of doing this which has all four good points?

Upvotes: 1

Views: 88

Answers (2)

Piotr Rodak
Piotr Rodak

Reputation: 1941

Another option would be to use the Planet type as the Name member in the PlanetInfo type and initialize the Map using a transformation from list:

module Planets =
    type Planet = 
    | Mercury
    | Venus
    | Earth

    type PlanetInfo = { Name: Planet; Diameter: float}

    let planets : PlanetInfo list = 
        [
            {Name = Mercury; Diameter = 100.}
            {Name = Venus; Diameter = 200.}
            {Name = Earth; Diameter = 300.}
        ]

    let planetsmap = planets |> List.map (fun pi -> pi.Name, pi) |> Map.ofList 

    
    planetsmap.[Mercury].Diameter

This approach doesn't require reflection and offers compile time type checking. So it is pretty much the same as your second approach, Monica.

Upvotes: 3

Chechy Levas
Chechy Levas

Reputation: 2312

How about this?

type Planet =
    |Mercury
    |Venus
    |Earth
    member this.Diameter =
        match this with
        |Mercury -> 100.
        |Venus -> 200.
        |Earth -> 300.

open Microsoft.FSharp.Reflection
let planets =
    FSharpType.GetUnionCases(typeof<Planet>)
    |> Array.map (fun case -> FSharpValue.MakeUnion(case, [||]) :?> Planet)
        

Upvotes: 2

Related Questions