1

I'm new to F# and playing around a bit, I already did work with C#. One thing that recently confused me a bit was returning values in functions. I do unterstand that everything is an expression (as described here) and that the following code won't compile nor execute:

let foo x =
   if x = 0 then x + 2 //The compiler will complain here due to a missing else branch.
   x - 2

Instead, you would have to write that:

let foo x =
   if x = 0 then x + 2
   else x - 2

I do see why that is and a similar problem also occurs when having a loop.

But consider a function which equals a "contains" function of a list in C#:

public static bool Contains(int num, List<int> list) {
   foreach (int i in list) {
      if (i == num) return true;
   }
   return false;
}

If you would translate that one by one, the compiler will tell you there is missing an else branch and I do unterstand why:

let Contains (num : int) list =
   for i in list do
      if i = x then true //Won't compile due to a missing else branch!
   false

Instead, you'd probably write a recursive function like that:

let rec Contains (num : int) list =
   match list with
   |  [] -> false
   |  head::tail -> head = num || (Contains num tail)

And I'm be fine with that, but in some situations, it is hard to do pattern matching. Is there really no way to do the described above? Couldn't one use a keyword like yield (or something similar to "return" in C#)?

Julian
  • 71
  • 6
  • 4
    I don't know F#, but I do know that you shouldn't be translating between languages word-by-word. Embrace the functional-style! Try using a higher order function here. – Sweeper Apr 09 '21 at 08:00
  • I know that you shouldn't translate word-by-word, that really is a bad idea, but in some cases it would be useful to have something similar to a style in C#. – Julian Apr 09 '21 at 08:12
  • Could you give one / some example(s)? – CaringDev Apr 09 '21 at 08:58
  • 1
    The solution to C# break or return in loops is very simple. Study the functions in the List, Seq and Array modules. To start with, look at tryFind. – Bent Tranberg Apr 09 '21 at 17:35
  • @BentTranberg Yeah, that, of course, is the easiest and usual way. But in the question I refer to a more general case. If you would need a function that noone has written yet. – Julian Apr 10 '21 at 18:48
  • 1
    The module functions are very general indeed. Those that are special, e.g. List.max, can be implemented using the more general ones. Whatever your C# loop does, it can be implemented with these functions. Spaghetti code de luxe might be an exception, but then clean it up before translating to F#. – Bent Tranberg Apr 10 '21 at 19:15

3 Answers3

3

I agree with you: early exits in C# can be convenient, especially to increase readability by avoiding nested if.

In F#, to avoid nested if or too big pattern matching (i.e. with too much cases or an input tuple with too much items), we can extract sub-functions: with meaningful name, it improves the code readability.

With lists and sequences (IEnumerable) in C#, code is generally more declarative using built-in methods or LINQ than using for/foreach loops. It's not always true but it's worth trying. By the way, in some situations, ReSharper can suggest to refactor for/foreach loops to LINQ queries.

→ This coding style is easily translatable to F#:

  • List:
    • C# → list.Contains(num)
    • F# → [1;2;3] |> List.contains 1
  • Sequence:
    • C# → list.Any(x => x == num)
    • F# → seq { 1;2;3 } |> Seq.exists ((=) 1)
    • F# → seq { 1;2;3 } |> Seq.contains 1
Romain Deneau
  • 2,841
  • 12
  • 24
2

An early return in C# is really just a GOTO. And I would consider that bad style, except for the "bouncer pattern" (validation). The argument there usually is to get rid of mental overload as early as possible. Given F#'s expressiveness, most validation can happen through types, so the otherwise ubiquitous if param = null || invalid(param) return style is not (or at least less) needed. Once you embrace the functional style, you'll find that there is less and less need for it. If needed, you can always simulate the goto :-)

exception Ret of string

let needsEarlyReturn foos =
    try
        for f in foos do
            if f > 42 then Ret "don't do" |> raise
            // more code
            if f > 7 then Ret "that ever" |> raise
        "!"
    with Ret v -> v
CaringDev
  • 8,391
  • 1
  • 24
  • 43
1

Keep in mind that the F# compiler converts tail recursion into a loop. So the solution is really to use a tail-recursive helper function even if you're using a type that doesn't have the pattern matching support of the F# list. Some examples:

A contains method for any IEnumerable<int>:

let contains item (xs : int seq) =
    use e = xs.GetEnumerator()
    let rec helper() = e.MoveNext() && (e.Current = item || helper())
    helper()

A similar method for a System.Collections.Generic.List<int>:

let contains item (xs : ResizeArray<int>) =
    let rec helper i = i < xs.Count && (xs.[i] = item || helper (i + 1)) 
    helper 0

In general, you can use a helper function for early exit like this (to keep the examples simple, the loop is infinite; the only way out is through the early exit):

let shortCircuitLoop shouldReturn getNextValue seed =
    let rec helper accumulator =
        if shouldReturn accumulator then accumulator else helper (getNextValue accumulator)
    helper seed

A C# version of this:

T ShortCircuit<T>(Func<T, bool> shouldReturn, Func<T, T> getNextValue, T seed) {
    while (true)
    {
        if (shouldReturn(seed))
            return seed;
        seed = getNextValue(seed);
    }
}

These examples are for illustration only, of course, as a pedagogical exercise. In production code, as noted by Romain Deneau, you'll want to use the library functions such as Seq.contains, List.contains, and so on.

phoog
  • 42,068
  • 6
  • 79
  • 117