Istvan
Istvan

Reputation: 8592

Is it possible to use narrow types with Fsharp?

I would think this is something trivial but I have spent some time on it and still, there is no clean way of doing it:

I have a type like:

type EngineTypes =
 | Petrol
 | Diesel
 | Kerosene 

I have a bunch of functions dealing with these just fine.

I would like to make a narrower type and still use the original functions with this new type.

type ValidEngineType =
 | Petrol
 | Diesel

The only way I could achieve this is by:

  type ValidEngineType =
    | Valid of engine : EngineType 
    | NotValid

  let validEngineCheck (engine): ValidEngineType =
    match region with 
    | "petrol" -> Valid EngineType.Petrol
    | "diesel" -> Valid EngineType.Diesel
    | _ -> Invalid

Later in code:

let myEngine = "petrol"

let checkEngine:Option<EngineType> = 
  match (validEngineCheck myEngine) with
  | Valid engine -> Some engine
  | Invalid -> 
      Console.Error.WriteLine("Unsupported engine: {0}", myEngine);
      None

if checkEngine.IsNone then
  Console.Error.WriteLine("Unsupported engine type")
  Environment.Exit 1

// it is now safe to call this
callingOriginalCode checkEngine.Value

Is there an easier way to manage this?

Update1:

In ADA this is called constrained subtype.

https://en.wikibooks.org/wiki/Ada_Programming/Type_System#Constrained_subtype

Upvotes: 1

Views: 152

Answers (2)

MrD at KookerellaLtd
MrD at KookerellaLtd

Reputation: 2807

What you want to do is deal with a sum type in arbitrary way over subsets of value constructors.

You can explicitly model the subsets and embed them in the superset:

type ValidEngineTypes = Petrol | Diesel
type EngineType = Valid of ValidEngineTypes | Kerosene

(you can do something similar with OO interface and inheritance, but lets put that on one side).

If the combinations are largely arbitrary in inconsistent with each other you can model each type explicitly and then create the "sum" type using Choice

type Petrol = Petrol
type Diesel = Diesel
type Kerosene = Kerosene

type EngineTypes = Choice<Petrol,Diesel,Kerosene>
type ValidEngineTypes = Choice<Petrol,Diesel>

if you want to define a function on EngineTypes then

let foo (engine: EngineTypes) = 
    match engine with
    | Choice1Of3 _petrol -> "petrol"
    | Choice2Of3 _diesel -> "diesel"
    | Choice3Of3 _kerosene -> "kerosene"

If you don't like using ChoiceN<a,...> then just do it explicitly using...

type EngineTypes = 
    | PetrolEngine of Petrol
    | DieselEngine of Diesel
    | KeroseneEngine of Kerosene

let foo (engine: EngineTypes) = 
    match engine with
    | PetrolEngine _petrol -> "petrol"
    | DieselEngine _diesel -> "diesel"
    | KeroseneEngine _kerosene -> "kerosene"

P.S. there are things in here that can be more succinct, or idiomatic, I'm just trying to make it as simple and explicit as I can

Upvotes: 1

Sergey Berezovskiy
Sergey Berezovskiy

Reputation: 236328

Looks like you want to partition your engines into valid engines and other engines. That is a perfect task for active patterns. If you have string input as in your code:

let (|ValidEngine|_|) (engine: string) =
  match engine with // case-sensitive check here
  | "petrol" -> Some EngineTypes.Petrol
  | "diesel" -> Some EngineTypes.Diesel
  | _ -> None

And your code becomes

let myEngine = "petrol"
match myEngine with
| ValidEngine engine -> callingOriginalCode engine // it is now safe to call this
| _ ->
    Console.Error.WriteLine("Unsupported engine type")
    Environment.Exit 1

As you can see active pattern not only partition engine string into valid and invalid engines, but this partition also has a payload of engine value. So your callingOriginalCode function will be called with the original EngineTypes parameter, but only when engine type is Diesel or Petrol.

Upvotes: 3

Related Questions