F# multi-condition if/else versus matching

2019-06-27 16:03发布

I'm new to F# and have been implementing simple algorithms to learn the language constructs. I implemented the bisection method using if/else and then wanted to learn how to do it using matching.

if fc = 0.0                   then printfn "%i/%i - f(%f) = %f, f(%f) = %f, f(%f) = %f" new_count n a fa b fb c fc
else if ((b - a) * 0.5) < eps then printfn "%i/%i - f(%f) = %f, f(%f) = %f, f(%f) = %f" new_count n a fa b fb c fc
else if new_count = n         then printfn "%i/%i - f(%f) = %f, f(%f) = %f, f(%f) = %f" new_count n a fa b fb c fc
else if fc * fa < 0.0         then bisect a c new_count
else if fc * fb < 0.0         then bisect c b new_count 

I found that using match a, b, fa, fb, fc would cause type errors, where if I took just a single parameter I could essentially ignore the parameter and check my conditions. What is the idiomatic F#/Functional way to use matching for this? Or should I just stick to if/else?

 match a with 
    | a when fc = 0.0              ->  printfn "%i/%i - f(%f) = %f, f(%f) = %f, f(%f) = %f" new_count n a fa b fb c fc
    | a when ((b - a) * 0.5) < eps -> printfn "%i/%i - f(%f) = %f, f(%f) = %f, f(%f) = %f" new_count n a fa b fb c fc
    | a when new_count = n         -> printfn "%i/%i - f(%f) = %f, f(%f) = %f, f(%f) = %f" new_count n a fa b fb c fc
    | a when fc * fa < 0.0         -> bisect a c new_count
    | a when fc * fb < 0.0         -> bisect c b new_count 

2条回答
虎瘦雄心在
2楼-- · 2019-06-27 16:26

Your conditions all deal with disparate things, unrelated to each other, so the string of ifs is just fine. The only thing I'd recommend is using elif instead of else if.

match should be understood along the lines of "given this thing that can be of different flavors, here's how to handle those flavors". One particular strength of match is that the compiler will figure out, and tell you, if you missed any of the "flavors". In particular, the code you gave in your question should produce a compiler warning, complaining that "Incomplete pattern matches on this expression". Think about it: what would be the result of that expression when none of the cases match?

With ifs, this will also be the case. For example, this doesn't compile:

let x = if a < 5 then 7

Why? Because the compiler knows what the result should be when a < 5 (i.e. it should be 7), but what should it be otherwise? The compiler can't decide for you, so it will generate an error.
This, on the other hand, would compile:

let x = if a < 5 then 7 else 8

But in your particular case, the compiler lets you get away with this, because all your branches return a unit (why? because printf returns unit, and all others are recursive). In other words, the following will compile:

let x = if a < 5 then ()

And the following:

let x = if a < 5 then printf "boo!"

The compiler lets you get away with this, because unit is special: it can only ever have one value (namely, ()), so the compiler can decide for you what the result of the expression would be when the condition isn't true.

One practical upshot of this would be that, if you didn't think about your conditions very carefuly, it could conceivably happen so that none of your conditions are true, and so the whole thing will return unit and not print anything. I can't say if that could happen in your particular case, because I don't see the whole function definition.

查看更多
劳资没心,怎么记你
3楼-- · 2019-06-27 16:26

Sometimes, as Fyodor Soikin correctly explains, a series of if, else if, else expressions is the best option, although I'd use elif instead of else if.

What sometimes make sense is to compute some of the values before, and put them into a data structure you can match on - typically a tuple.

Using a simplified version of the above question, imagine that you only need to check the first two cases, you could do it like this:

match fc = 0., ((b - a) * 0.5) < eps with
| true, _ -> "fc is 0"
| _, true -> "((b - a) * 0.5) is less than eps"
| _ -> "etc."

Notice the comma after fc = 0., which makes the match expression into a tuple - more specifically a bool * bool.

This has the disadvantage that it's inefficient, because you'd always be evaluating the expression ((b - a) * 0.5) < eps, even if fc = 0. evaluates to true.

Still, evaluating a simple expression like ((b - a) * 0.5) < eps will be so fast that you probably wouldn't be able to measure it, so if you think this way of expressing the algorithm is more readable, you could decide to trade off that small inefficiency for better readability.

In this case, though, I don't think it's more readable, so I'd still go with a series of if, elif, else expressions.

Here's an example where pre-computing values and putting them into a tuple makes more sense:

match number % 3, number % 5 with
| 0, 0 -> "FizzBuzz"
| _, 0 -> "Buzz"
| 0, _ -> "Fizz"
| _    -> number.ToString()

This is a common implementation of the FizzBuzz kata. Here it make sense because both modulo numbers are needed for the first match, so there's no inefficiency, and the code is quite readable as well.


The point about inefficiency above is true for F# because F# is eagerly evaluated. In Haskell, on the other hand, expressions are lazily evaluated, so there you'd be able to do the following without loss of efficiency:

case (fc == 0.0, ((b - a) * 0.5) < eps) of
  (True, _) -> "fc is 0"
  (_, True) -> "((b - a) * 0.5) is less than eps"
  _ -> "etc."

The second expression in the tuple would only be evaluated if necessary, so if the first case ((True, _)) is matched, there'd be no need to evaluate the second expression.

查看更多
登录 后发表回答