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

303 Views Asked by At
// 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
2

There are 2 best solutions below

0
On BEST ANSWER

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.

0
On

Generally speaking, I try to have the semantics of my code match the logic of what I'm trying to accomplish. In your case, I would describe your problem as:

There are two different pieces of simple data I may receive. Based on which one I receive, run a specific piece of code.

This maps exactly to option 2. I may or may not nest the functions like you have depending on the context. The other two options create a mismatch between your goal and your code.

Option 1:

I would describe the logic of this one as:

I have information (or context) now from which I can build two different calculations, one of which may need to be run later. Construct both now, and then evaluate the one which is needed later.

This really doesn't match the logic of what you want since there is no change in context or available data. You're still in the same context either way, so the additional complexity of making it lazy simply obfuscates the logic of the function.

Option 3:

I would describe the logic of this one as:

I have some data which may fit into one of two cases. Determining which case holds requires some complex logic, and that logic may also have some overlap with the logic needed to determine the needed return value of the overall function. Move the logic to determine the case into a separate function (an active pattern in this case), and then use the return value of the active pattern to determine the resulting value of the function.

This one conflates the control flow (determining what code should I execute next) with the logic executed in each case. Its much more clear to separate control flow from the following logic if there is no overlap between the two.

So match the structure of your code to the structure of your problem, or you'll end up with people wondering what the additional code complexity accomplishes.