Multiple Exits From F# Function
First of all, as others have already noted, it's not "the F# way" (well, not FP way, really). Since you don't deal with statements, but only expressions, there isn't really anything to break out of. In general, this is treated by a nested chain of if
..then
..else
statements.
That said, I can certainly see where there are enough potential exit points that a long if
..then
..else
chain can be not very readable - especially so when dealing with some external API that's written to return error codes rather than throw exceptions on failures (say Win32 API, or some COM component), so you really need that error handling code. If so, it seems the way to do this in F# in particular would be to write a workflow for it.
Here's my first take at it:
type BlockFlow<'a> =
| Return of 'a
| Continue
type Block() =
member this.Zero() = Continue
member this.Return(x) = Return x
member this.Delay(f) = f
member this.Run(f) =
match f() with
| Return x -> x
| Continue -> failwith "No value returned from block"
member this.Combine(st, f) =
match st with
| Return x -> st
| Continue -> f()
member this.While(cf, df) =
if cf() then
match df() with
| Return x -> Return x
| Continue -> this.While(cf, df)
else
Continue
member this.For(xs : seq<_>, f) =
use en = xs.GetEnumerator()
let rec loop () =
if en.MoveNext() then
match f(en.Current) with
| Return x -> Return x
| Continue -> loop ()
else
Continue
loop ()
member this.Using(x, f) = use x' = x in f(x')
let block = Block()
Usage sample:
open System
open System.IO
let n =
block {
printfn "Type 'foo' to terminate with 123"
let s1 = Console.ReadLine()
if s1 = "foo" then return 123
printfn "Type 'bar' to terminate with 456"
let s2 = Console.ReadLine()
if s2 = "bar" then return 456
printfn "Copying input, type 'end' to stop, or a number to terminate with that number"
let s = ref ""
while (!s <> "end") do
s := Console.ReadLine()
let (parsed, n) = Int32.TryParse(!s)
if parsed then
printfn "Dumping numbers from 1 to %d to output.txt" n
use f = File.CreateText("output.txt") in
for i = 1 to n do
f.WriteLine(i)
return n
printfn "%s" s
}
printfn "Terminated with: %d" n
As you can see, it effectively defines all constructs in such a way that, as soon as return
is encountered, the rest of the block is not even evaluated. If block flows "off the end" without a return
, you'll get a runtime exception (I don't see any way to enforce this at compile-time so far).
This comes with some limitations. First of all, the workflow really isn't complete - it lets you use let
, use
, if
, while
and for
inside, but not try
..with
or try
..finally
. It can be done - you need to implement Block.TryWith
and Block.TryFinally
- but I can't find the docs for them so far, so this will need a little bit of guessing and more time. I might come back to it later when I have more time, and add them.
Second, since workflows are really just syntactic sugar for a chain of function calls and lambdas - and, in particular, all your code is in lambdas - you cannot use let mutable
inside the workflow. It's why I've used ref
and !
in the sample code above, which is the general-purpose workaround.
Finally, there's the inevitable performance penalty because of all the lambda calls. Supposedly, F# is better at optimizing such things than, say C# (which just leaves everything as is in IL), and can inline stuff on IL level and do other tricks; but I don't know much about it, so the exact performance hit, if any, could only be determined by profiling.
In F#, everything's made up of expressions (whereas in many other languages, the key building block is a statement). There's no way to exit a function early, but often this isn't needed. In C, you have an if/else
blocks where the branches are made up of statements. In F#, there's an if/else
expression, where each branch evaluates to a value of some type, and the value of the entire if/else
expression is the value of one branch or the other.
So this C++:
int func(int param) {
if (param<0)
return BadParam;
return Success;
}
Looks like this in F#:
let func param =
if (param<0) then
BadParam
else
Success
Your code is on the right track, but you can refactor it, putting most of your logic in the else
branch, with the "early return" logic in the if
branch.
An option similar to Pavel's, but without needing your own workflow builder, is just to put your code block within a seq
expression, and have it yield
error messages. Then right after the expression, you just call FirstOrDefault
to get the first error message (or null).
Since a sequence expression evaluates lazily, that means it'll only proceed to the point of the first error (assuming you never call anything but FirstOrDefault
on the sequence). And if there's no error then it simply runs through to the end. So if you do it this way you'll be able to think of yield
just like an early return.
let x = 3.
let y = 0.
let errs = seq {
if x = 0. then yield "X is Zero"
printfn "inv x=%f" (1./x)
if y = 0. then yield "Y is Zero"
printfn "inv y=%f" (1./y)
let diff = x - y
if diff = 0. then yield "Y equals X"
printfn "inv diff=%f" (1./diff)
}
let firstErr = System.Linq.Enumerable.FirstOrDefault errs
if firstErr = null then
printfn "All Checks Passed"
else
printfn "Error %s" firstErr
In my opinion, match expressions are the F# analogue of early-exit for calling out erroneous conditions and handling them separately. For your example, I'd write:
[<EntryPoint>]
let main (args:string[]) =
printfn "args.Length is %d" args.Length
match args with
| [| searchstring; filespace |] ->
// much code here ...
int Success
| _ -> printfn "Two arguments must be passed"
int WrongNumberOfArgumentsPassed
This separates the error case nicely. In general, if you need to exit from the middle of something, split functions and then put the error case in a match
. There's really no limit to how small functions should be in a functional language.
As an aside, your use of discriminated unions as sets of integer constants is a little weird. If you like that idiom, be aware that you don't need to include the type name when referring to them.