开发者

How to make a F# discriminated union out of another type's union cases?

Imagine this discriminated union:

type Direction = 
    | North
    | South
    | East
    | West

Now imagine that I want a type which only accepts tuples of (North, South) or (East, West). Perhaps this will describe train routes which only run North to South, or East to West. (North, East) and (South, West) should be forbidden, perhaps because the trains don't run like that.

This doesn't work:

type TrainLines = 
    | North, South
    | East, West

Even though that doesn't work, perhaps you can see what I'm trying to do.

This works, but doesn't restrict possibilites to only (North, South) and (East, West):

开发者_如何学Go
type TrainLines = Direction * Direction

Any guidance would be welcomed.


This isn't exactly what you asked for, but I think it's likely that

type TrainLines =
    | NorthSouth
    | EastWest

would do you good. If needed you could add e.g.

    with member this.Directions =
           match this with
           | NorthSouth -> [North; South]
           | EastWest -> [East; West]


You can't do exactly what you want, because North, South, East and West aren't types of their own. So you can't have something like North * South; and North, South is a value of type Direction * Direction, but not the only one. Just like you can't define the type

type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9


Now imagine that I want a type which only accepts tuples of (North, South) or (East, West).

Interesting feature request: sounds like you want "static range constraints", e.g.

//fictional syntax for static range constraints
type TrainLine = (a,b) where (a=North and b=South) or (a=East and b=West)

let northSouth = TrainLine(North,South) // compiles
let northEast = TrainLine(North,East) // does not compile

Such a feature seems plausible in a language with only literals, but where we get into trouble is when we consider values only known at runtime:

let parseDirection userInputString = 
    match userInputString with
    | "North" -> North
    | "South" -> South
    | "East" -> East
    | "West" -> West
    | _ -> failwith "invalid direction"

let directionA = parseDirection(System.Console.ReadLine())
let directionB = parseDirection(System.Console.ReadLine())

//compiler can't enforce constraint because direction values unknown until runtime
let trainLine = TrainLine(directionA,directionB) 

However, F#'s does have a nice set of features in Active Patterns which can help to convert runtime input into a set of known cases and then proceed with static confidence:

let (|NorthSouth|EastWest|Invalid|) (a,b) = 
    match a,b with
    | North,South -> NorthSouth
    | East,West -> EastWest
    | _ -> Invalid

let trainLines = [(North,South); (South,North); (East,West); (North,East);(North,North); (South,East)]

let isValidTrainLine trainLine = 
    match trainLine with
    | NorthSouth -> true
    | EastWest -> true
    | Invalid -> false

let validTrainLines = trainLines |> List.filter isValidTrainLine
//val it : (Direction * Direction) list = [(North, South); (East, West)]


You really want polymorphic variants from OCaml:

[ `North | `South | `East | `West ]
[ `North | `South ] * [ `East | `West ]

but F# is currently incapable of expressing this. I actually find I need this a lot in my work...

You can introduce unnecessary layers of union types:

type ns = North | South
type ew = East | West
type nsew = NorthSouth of ns | EastWest of ew

and then use ns * ew.

Another solution that can sometimes work well is to use an interface to provide a consistency between two separate union types:

type IDir = abstract AsInt : int
type ns =
  | North
  | South
  interface IDir with
    method d.AsInt =
      match d with North -> 0 | South -> 1
type ew =
  | East
  | West
  interface IDir with
    method d.AsInt =
      match d with East -> 2 | West -> 3

Sadly, this imposes all of the disadvantages of OOP...

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜