F# pattern matching: how to match a set of possible types that share the same parameters?

1.9k Views Asked by At

I'm new to F# and not quite familiar with the whole pattern matching idea. I tried to search for a better solution to my problem but I fear I can't even express the problem properly – I hope the question title is at least somewhat accurate.

What I want to do is extract 2 "parameters" from listMethod. listMethod is of one of several types that have a string and an Expression "parameter" (I suspect parameter is the wrong term):

    let (varDecl, listExpr) =
        match listMethod with 
        | Select (var, expr)  -> (var, expr)
        | Where (var, expr)   -> (var, expr)
        | Sum (var, expr)     -> (var, expr)
        | Concat (var, expr)  -> (var, expr)

Then I continue to work with varDecl and at the end have a similar match expression with the actual listMethod code that makes use of several temporary variables I created based on varDecl.

My question now is: How can I make the above code more compact?

I want to match all those types that have 2 parameters (of type string and Expression) without listing them all myself, which is kinda ugly and hard to maintain.

The ListMethod type is declared as follows (the whole thing is a FsLex/FsYacc project):

type ListMethod =
    | Select of string * Expr
    | Where of string * Expr
    | Sum of string * Expr
    | Concat of string * Expr
    | ...
    | somethingElse of Expr

(as of now I only have types of the form string * Expr, but that will change).

I reckon that this is a fairly dumb question for anyone with some experience, but as I've said I'm new to F# and couldn't find a solution myself.

Thanks in advance!

Edit: I'd really like to avoid listing all possible types of listMethod twice. If there's no way I can use wildcards or placeholders in the match expressions, perhaps I can modify the listMethod type to make things cleaner.

One option that comes to mind would be creating only 1 type of listMethod and to create a third parameter for the concrete type (Select, Where, Sum). Or is there a better approach?

4

There are 4 best solutions below

5
On BEST ANSWER

This is probably the standard way:

let (varDecl, listExpr) =
    match listMethod with 
    | Select (var, expr)
    | Where (var, expr)
    | Sum (var, expr)
    | Concat (var, expr) -> (var, expr)

The | sign means or, so if one of these match, the result will be returned. Just make sure that every case has exactly the same names (and types).

As Chuck commented, this is an even better solution:

let (Select (varDecl, expr)
    | Where (varDecl, expr)
    | Sum (varDecl, expr)
    | Concat (varDecl, expr)) = listMethod
0
On

I reckon that this is a fairly dumb question for anyone with some experience, but as I've said I'm new to F# and couldn't find a solution myself.

On the contrary, this is a very good question and actually relatively untrodden ground because F# differs from other languages in this regard (e.g. you might solve this problem using polymorphic variants in OCaml).

As Ankur wrote, the best solution is always to change your data structure to make it easier to do what you need to do if that is possible. KVB's solution of using active patterns is not only valuable but also novel because that language feature is uncommon in other languages. Ramon's suggestion to combine your match cases using or-patterns is also good but you don't want to write incomplete pattern matches.

Perhaps the most common example of this problem arising in practice is in operators:

type expr =
  | Add of expr * expr
  | Sub of expr * expr
  | Mul of expr * expr
  | Div of expr * expr
  | Pow of expr * expr
  | ...

where you might restructure your type as follows:

type binOp = Add | Sub | Mul | Div | Pow

type expr =
  | BinOp of binOp * expr * expr
  | ...

Then tasks like extracting subexpressions:

let subExprs = function
  | Add(f, g)
  | Sub(f, g)
  | Mul(f, g)
  | Div(f, g)
  | Pow(f, g) -> [f; g]
  | ...

can be performed more easily:

let subExprs = function
  | BinOp(_, f, g) -> [f; g]
  | ...

Finally, don't forget that you can augment F# types (such as union types) with OOP constructs such as implementing shared interfaces. This can also be used to express commonality, e.g. if you have two overlapping requirements on two types then you might make them both implement the same interface in order to expose this commonality.

0
On

In case you are ok to do adjustments to your data structure then below is something that will ease out the pattern matching.

type ListOperations = 
    Select | Where | Sum | Concat


type ListMethod =
    | ListOp of ListOperations * string * Expr
    | SomethingElse of int

let test t = 
    match t with
    | ListOp (a,b,c) -> (b,c)
    | _ -> ....

A data structure should be designed by keeping in mind the operation you want to perform on it.

0
On

If there are times when you will want to treat all of your cases the same and other times where you will want to treat them differently based on whether you are processing a Select, Where, Sum, etc., then one solution would be to use an active pattern:

let (|OperatorExpression|_|) = function
| Select(var, expr) -> Some(Select, var, expr)
| Where (var, expr) -> Some(Where, var, expr)
| Sum (var, expr) -> Some(Sum, var, expr)
| Concat (var, expr) -> Some(Concat, var, expr)
| _ -> None

Now you can still match normally if you need to treat the cases individually, but you can also match using the active pattern:

let varDecl, listExp = 
    match listMethod with
    | OperatorExpression(_, v, e) -> v, e
    | _ -> // whatever you do for other cases...