Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

F#/OCaml: How to avoid duplicate pattern match?

Have a look at this F#/OCaml code:

type AllPossible =
    | A of int
    | B of int*int
    | ...
    | Z of ...

let foo x =
    ....
    match x with
    | A(value) | B(value,_) ->                   (* LINE 1 *)
        (* do something with the first (or only, in the case of A) value *)
        ...
        (* now do something that is different in the case of B *)
        let possibleData = 
            match x with
            | A(a) -> bar1(a)
            | B(a,b) -> bar2(a+b)
            | _ -> raise Exception    (* the problem - read below *)
        (* work with possibleData *)
    ...
    | Z -> ...

So what is the problem? In function foo, we pattern match against a big list of types. Some of the types share functionality - e.g. they have common work to do, so we use "|A | B ->" in LINE 1, above. We read the only integer (in the case of A), or the first integer (in the case of B) and do something with it.

Next, we want to do something that is completely different, depending on whether we work on A or B (i.e. call bar1 or bar2). We now have to pattern match again, and here's the problem: In this nested pattern match, unless we add a 'catchAll' rule (i.e. '_'), the compiler complains that we are missing cases - i.e. it doesn't take into account that only A and B can happen here.

But if we add the catchAll rule, then we have a far worse problem: if at some point we add more types in the list of LINE1 (i.e. in the line '|A | B ->' ... then the compiler will NOT help us in the nested match - the '_' will catch them, and a bug will be detected at RUNTIME. One of the most important powers of pattern matching - i.e. detecting such errors at compile-time - is lost.

Is there a better way to write this kind of code, without having to repeat whatever work is shared amongst A and B in two separate rules for A and B? (or putting the A-and-B common work in a function solely created for the purpose of "local code sharing" between A and B?)

EDIT: Note that one could argue that the F# compiler's behaviour is buggy in this case - it should be able to detect that there's no need for matching beyond A and B in the nested match.

like image 371
ttsiodras Avatar asked Dec 16 '22 11:12

ttsiodras


2 Answers

If the datatype is set in stone - I would also prefer local function.

Otherwise, in OCaml you could also enjoy open (aka polymorphic) variants :

type t = [`A | `B | `C]
let f = function
| (`A | `B as x) ->
  let s = match x with `A -> "a" | `B -> "b" in
  print_endline s
| `C -> print_endline "ugh"
like image 101
ygrek Avatar answered Dec 29 '22 19:12

ygrek


I would just put the common logic in a local function, should be both faster and more readable. Matches nested that way is pretty hard to follow, and putting the common logic in a local function allows you to ditch the extra matching in favour of something that'll get inlined anyway.

like image 38
valderman Avatar answered Dec 29 '22 19:12

valderman