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):
开发者_如何学Gotype 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...
精彩评论