fsharp / fslang-suggestions

The place to make suggestions, discuss and vote on F# language and core library features
344 stars 21 forks source link

Add possibility to use break and continue in f# loops. #381

Open baronfel opened 8 years ago

baronfel commented 8 years ago

Submitted by Vladimir on 1/5/2015 12:00:00 AM
12 votes on UserVoice prior to migration

It very often looks better than recursion, and it would be wonderful to have this possibility

Original UserVoice Submission Archived Uservoice Comments

financial-engineer commented 8 years ago

While functional programming purists could say that strengthening imperative programming features in the language is not desirable, I think that both the break and continue keywords are long-awaited additions to the F# functional-imperative language for pragmatic reasons (migration of codebases from Java, C++ or another imperative language, friendliness of the language syntax to programmers with imperative programming background, i.e. the majority of new language users). The break and continue constructs would be useful in two forms: labeled and unlabeled, like in Java, the TIOBE TOP1 language. Labels attached to loops have to be declared in the Java style: a label name followed by a colon. I believe it has the potential to positively affect the F# popularity. Please see details of Java syntax at https://docs.oracle.com/javase/tutorial/java/nutsandbolts/branch.html

DemiMarie commented 7 years ago

I agree, but for a different reason: performance (break and continue may very well be faster).

smoothdeveloper commented 7 years ago

@DemiMarie I think this would need substanciation if we could provide samples highlighting the issue.

From discussions with seasoned F# developers, it seems recursive function or while loop with a boolean condition would have pretty similar cost.

The feature was mentionned by @migueldeicaza : https://www.infoq.com/articles/virtual-panel-dotnet-future

For F#, my request is simple: I am a man that is too attached to his old ways of writing loops. I want my loops to include support for break and continue. I know this is heresy, but that is what I desire the most :-)

Well, going on the road of performance, and not introducing many arbitrary constructs, what about goto and labels? break and continue are limited to the closest loop which is not always what you want.

So far I haven't missed those constructs, and I think they are not obvious to introduce gracefully in expression based F#.

It would be nice to have a summary of downvoters motives.

BentTranberg commented 7 years ago

I am against this. Use of "break" (except in "case"), "continue", "goto" and "return" in C# can make it hard to understand the meaning, especially in large methods where use of these keywords can be hard to spot.

You should be able to quickly make quite a few assumptions just by taking a glance at the structure of the code. These assumptions easily break when any of these mechanisms are allowed. For that reason alone they should all be banned in F#.

Although simple moderate sensible use of "break" (and "return") is very popular and de facto standard pattern in C#, and definitely makes code shorter and more readable, there is no need to have this in F#, since equally simple functional constructs exist. Translating from C# to F# is very simple in these cases.

C# source can be rewritten to not use them, and then easily be ported to F#. If the C# code is hard to rewrite, then it just shows the code had low quality to begin with, and it justifies why it should not be so in F#.

I have translated tens of thousands of lines of C# to F#. The biggest stumbling blocks by far is with "continue", heavy use of "break" in odd places in loops, and not least "goto". Even if these were available in F#, it would be even more important for me to refactor them away as quickly as possible in order to take advantage of the language. In hard cases, that refactoring is easier done in C# anyway.

Although F# is a multi-paradigm language, it is also a functional first language. These mechanisms are impure even in the imperative domain. They would influence even the functional constructs in a negative way, because they make it harder to reason about the code in total.

isaksky commented 6 years ago

I think this, as well as adding normal imperative "return" would be a great change to F#. Right now in F# there are a few alternatives:

  1. Increase nesting (if - else), pattern matching
    • This option is problematic, because it does not scale well as you add more conditions.
  2. Wrap results and compose with functions like Option.map
    • This option is not satisfactory, because it is more verbose, adds allocations, and is less direct than just returning early
  3. Computation expressions
    • Not efficient
    • Would require an ad-hoc computation expression per type you want to compose ( see asyncMaybe, for example. That is not elegant, it is ad-hoc and crazy.)
      • Even this wouldn't work for multiple different types (e.g., ParseResult and HttpRequestResult)

Some people say let rec and small functions is an alternative, but in that case you are at best shuffling the problem around. It is like telling someone callback hell is fine, because you can just split the logic up into more functions.

GratianPlume commented 3 years ago

The following code may help:

module Range =

    let inline (|Range|) (x: Range) = (x.Start.Value, x.End.Value)

    let inline range a b = Range(Index a, Index b)

open Range
type LoopBuilder() =

    member inline __.For(s: seq<_>, f) =
        use s = s.GetEnumerator()
        let mutable result = ValueNone

        while s.MoveNext() && 
            (   result <- f s.Current
                result.IsNone 
            ) do ()
        result

    member inline __.For(Range(x, y), f) =
        let mutable result = ValueNone
        let mutable i = x
        while i < y do 
            result <- f i
            i <- if result.IsNone then 
                    i + 1
                 else 
                    y
        result

    member inline __.For(s: IReadOnlyList<'t>, f) =
        let len = s.Count
        let mutable result = ValueNone
        let mutable i = 0

        while i < len do
            result <- f s.[i]
            i <- if result.IsNone then 
                    i + 1                    
                 else
                    len

        result

    member inline __.Return x = ValueSome x

    member inline __.Zero() = ValueNone

    member inline __.Yield x = x

    member inline __.Combine(x, f) =
        match x with
        | ValueSome x -> x
        | ValueNone -> f()

    member inline __.Delay f = f

    member inline __.Run f = f()

 let loop = LoopBuilder() 

usage:

   // `yield` for default value, used it,  the true type value return.
    let foo2 (arr: int[]) = loop {
        for x in arr do
            for y in arr do 
                if x * y > 10000 then 
                    return x + y
        yield 0
    }

   // without  `yield`, it return a `voption` value
    let foo3 (arr: _ List) = loop {
        for x in arr do
            if x > 10000 then
                return x
    }

   **// It's hard to say, it don't support ` for x in 0 .. 10000 do`, when you write that, it call the `seq` overload.
   // can somebody tell me how?**

    let foo4 (xs: Range) = loop {
        for x in xs do
            if x > 10000 then
                return x
    }

This code looks ugly. I did thousands of experiments, it make the cleanest result:

public static FSharpValueOption<int> foo3(List<int> arr)
{
    int count = ((IReadOnlyCollection<int>)arr).Count;
    FSharpValueOption<int> result = FSharpValueOption<int>.ValueNone;
    for (int num = 0; num < count; num = ((result.Tag != 0) ? count : (num + 1)))
    {
        result = f@38-3(((IReadOnlyList<int>)arr)[num]);
    }
    return result;
}

internal static FSharpValueOption<int> f@38-3(int _arg1)
{
    if (_arg1 > 10000)
    {
        return FSharpValueOption<int>.NewValueSome(_arg1);
    }
    return FSharpValueOption<int>.ValueNone;
}

I do not agree that the calculation expression must be inefficient. However, I would rather if F# added the break return syntax.

yatli commented 3 years ago

@greatim my poorman's break is always async{ ... } without actual async code :p

GratianPlume commented 3 years ago

@greatim my poorman's break is always async{ ... } without actual async code :p Oh no! It's hard to use, and packed with several layers of function.

isral commented 3 years ago

@yatli Please give sample code

yatli commented 3 years ago

simply:

async {
  while true do
    if foo then return
}
GratianPlume commented 3 years ago

simply:

async {
  while true do
    if foo then return
}

It doesn't compile

yatli commented 3 years ago

Uh oh. But why? I thought I understand CE. 😅😅

GratianPlume commented 3 years ago

Uh oh. But why? I thought I understand CE. 😅😅

hmmm, I can see why now. I fixed it for this:

      let foo x =
         async {
           while true do
             if !x then return ()
         }

but the loop won't be breaked.

yatli commented 3 years ago

@greatim yeah that's my confusion -- why doesn't it stop at return? What does it even mean to return multiple times from async? That the async CE can be run multiple times, statefully, like IAsyncEnumerable?

GratianPlume commented 3 years ago

What does it even mean to return multiple times from async?

I don't know what it does. But last time I told my friend that "using async can break the loop". He did it, and hit my face on the next day.

BentTranberg commented 3 years ago

I think as a general rule we should keep posts in these open repos (fsharp/fslang-suggestion, dotnet/fsharp, etc) focused on the issue, so that we won't have to waddle through unrelated stuff repeatedly. In my experience one tends to read or skim through the whole thread from start to end several times. I suggest you sort out side issues like this in one of the channels in F# Slack, unless you feel it's actually relevant to the issue.

daveyostcom commented 1 year ago

I am against this. Use of "break" (except in "case"), "continue", "goto" and "return" in C# can make it hard to understand the meaning, especially in large methods where use of these keywords can be hard to spot.

IDEs should have a separate category for coloring these keywords. I would use red.