Search code examples
f#pattern-matchinglazy-evaluationcode-readabilityactive-pattern

Splitting out blocks of code in F# pattern matching for readability


// Standard pattern matching.
let Foo x =
  match x with
  | 1 ->
      // ... lots of code, only evaluated if x == 1
  | 2 ->
      // ... lots of code, only evaluated if x == 2

// Standard pattern matching separated out, causing exception.
let Bar x =
  let valueOne = //... lots of code, evaluated always. Exception if value <> 1.
  let valueTwo = //... lots of code, evaluated always. Exception if value <> 2.

  match x with
  | 1 -> valueOne
  | 2 -> valueTwo

In a pattern matching using "match", the code for each pattern can be large, see Foo above, making me want to split out the blocks as separate calls for improved readability.

The issue with this can be that the call will be evaluated even though the pattern is not matched, as in Bar above.

  • Option 1: lazy eval.
  • Option 2: forward the argument/parameters.
  • Option 3: forward the argument/parameters and use Active pattern.

What is the preferred method for improving the readability where code under each pattern can be large. Or are there any other obvious solution to the problem?

// ===== OPTION 1 =====
// Pattern matching separated out, lazy eval.
let Foo x =
  let valueOne = lazy //... lots of code, evaluated on Force().
  let valueTwo = lazy //... lots of code, evaluated on Force().

  match x with
  | 1 -> valueOne.Force()
  | 2 -> valueTwo.Force()

// ===== OPTION 2 =====
// Pattern matching separated out, with arguments.
let Foo x =
  let valueOne a = //... lots of code.
  let valueTwo a = //... lots of code.

  match x with
  | 1 -> valueOne x
  | 2 -> valueTwo x

// ===== OPTION 3 =====
// Active Pattern matching separated out, with arguments.
let Foo x = 
  let (|ValueOne|_|) inp =
    if inp = 1 then Some(...) else None

  let (|ValueTwo|_|) inp =
    if inp = 2 then Some(...) else None

  match x with
  | ValueOne a -> a
  | ValueTwo b -> b

Solution

  • I would probably just extract the two bodies of the pattern matching into functions that take unit:

    let caseOne () = 
      // Lots of code when x=1
    
    let caseTwo () = 
      // Lots of code when x=2
    
    let Foo x =
      match x with
      | 1 -> caseOne()
      | 2 -> caseTwo()
    

    This is similar to your solution using lazy, but as we are never re-using the result of the lazy value, there is really no reason for using lazy values - a function is simpler and it also delays the evaluation of the body.

    If you then find that there is some commonality between caseOne and caseTwo, you can again extract this into another function that both of them can call.