Skip to content

Create FS-1152-For-loop-with-accumulation.md #807

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 65 commits into
base: main
Choose a base branch
from

Conversation

Happypig375
Copy link
Contributor

@Happypig375 Happypig375 commented Jul 23, 2025

@Happypig375
Copy link
Contributor Author

@T-Gro Review wanted

@Happypig375
Copy link
Contributor Author

@T-Gro How do you feel about the alternatives to choose (value returning vs binding leaking) in the proposal?

@T-Gro
Copy link
Contributor

T-Gro commented Aug 4, 2025

@T-Gro How do you feel about the alternatives to choose (value returning vs binding leaking) in the proposal?

The second sample should come with the standard result ignored warning, like it applies to all expressions.
This will be tricky to explain clearly - even though it syntactically builds upon for, it really changes it from a statement (unit-returning) to an expression.

The value returning makes it apparent what the scope of each binding is.
In the leak sample, the scope really is confusing.

@Happypig375
Copy link
Contributor Author

Happypig375 commented Aug 4, 2025

@T-Gro Would retaining the -> that once meant do yield in place of do help clarify things? let folded = for x in xs with acc = 1 -> acc + x

@T-Gro
Copy link
Contributor

T-Gro commented Aug 4, 2025

They should be treated both as valid, I don't think there is a big difference.

@Happypig375
Copy link
Contributor Author

@T-Gro You mean to treat -> the same as do in the context of fold loops?

@Happypig375
Copy link
Contributor Author

But treating -> the same as do for value-returning version would conflict with the existing meaning of do yield in CEs.

@omcnoe
Copy link

omcnoe commented Aug 14, 2025

As a user of F#, I'd be concerned with the fact that this proposal is overloading the for keyword into two forms, one that evaluates to unit and another new form that actually evaluates to a real value (final accumulator state).

It seems a bit surprising that the with keyword & accumulator state changes the type of the whole for expression.

It might be a more clear design if these two differences could be both represented separately in some way:

  • traditional iter-style loops that are always unit VS loops that evaluate to an actual list of values
  • loops that thread accumulator state VS loops that do not

Maybe it's better to allow the programmer to opt in to either decision independently rather than force-bundling both concepts together? Use the with keyword to define that loop threads an accumulator, and use some other syntax (maybe with yield somewhere to define that a loop is value returning?

@Happypig375
Copy link
Contributor Author

@omcnoe If you really think about it, the traditional for loop just has implicit with state = (), i.e. accumulates to unit. If you change the accumulation, you also change the loop value. They aren't actually independent.

@Tarmil
Copy link
Contributor

Tarmil commented Aug 14, 2025

@Happypig375 More exactly, the traditional for loop has an implicit with () = (), ie it accumulates () and doesn't bind any variable in the body of the loop.

@charlesroddie
Copy link
Contributor

@charlesroddie It's an RFC for a non-approved suggestion. The README of fslang-suggestions explicitly states that doing this and starting a prototype implementation can help convince.

This is true and has been there a long time but I haven't seen this done. It doesn't make any sense to me because going into technical details is not important for viewing reviewing a high-level suggestion, unless the suggestion itself is highly technical in nature (e.g. may not work unless various technical aspects align) or has specific technical uncertainties that are important for evaluating it. These are extremely rare. I think we should qualify the readme with that info.

In this case, what is this RFC supposed to show? Are there technical questions that are uncertain from the suggestion that it resolves that are essential for evaluating the suggestion?

@Happypig375
Copy link
Contributor Author

@charlesroddie The original suggestion reads as this

let result =
    for s with t in ts do
        s + t

into

let result =
    ts
    |> #Collection.fold (fun s t -> s + t) s

Obviously there are a lot of questions surrounding this - what is #Collection? Why s with t in ts? It looks weird, relies on magic module searching, and breaks scoping principles. Meanwhile, the justification of doing this is also unclear. The original suggestion without further improvements is highly likely to be rejected.

After careful design as done in this RFC, the result now becomes

let result =
    for t in ts with s = 0 do
        s + t

into

let result =
    let mutable accum = 0
    for t in ts do
        let s = accum
        accum <- s + t
    accum

All the design choices and justifications are to be included in this RFC for a comprehensive evaluation on the validity of this suggestion, being concrete trumps being ambiguous. This is why making an RFC can also help convince approval.

Also, I don't get why you are complaining about procedures instead of the actual suggestion - it seems not to be constructive behaviour especially when paired with the 👎 reaction on the top post, as seen not just in this PR, but also in other issues, where the 👎 reaction persists despite a proper reply.

@Happypig375
Copy link
Contributor Author

@omcnoe See if 23a218c explains it well

@Martin521
Copy link
Contributor

Also, I don't get why you are complaining about procedures instead of the actual suggestion - it seems not to be constructive behaviour especially when paired with the 👎 reaction on the top post, as seen not just in this PR, but also in other issues, where the 👎 reaction persists despite a proper reply.

I do think that the language design process makes sense and I therefore believe it is ok if somebody points to it.
Also, there can be many reason to not favor the addition of a feature even after "proper replies".
My own opinion in this case: The feature is somehow cool, but I still don't want it in F#, based on my perception of the cost-benefit ratio. And because I believe that for the future of F# it is much more important to focus on a) open issues that bother the average user and b) on maintainability of the compiler.

@krauthaufen
Copy link

In my opinion this construct would make it more complex for newcomers to understand code, intransparent about what is actually executed underneath (Seq.fold, List.fold, a for loop with a mutable variable?) and I don't see how it makes code shorter or easier to read. A for loop with a mutable variable is a few characters longer and familiar to most people. Folds are also more transparent in my opinion and can be used in pipe-chains.
I'm just concerned about F#'s simplicity being eaten up by these corner-case specializations.

@reinux
Copy link

reinux commented Aug 16, 2025

@Happypig375 did establish that folds aren't an edge case by any stretch -- in fact, it's the third most common HOF after map and iter, both of which can be expressed as list/seq comprehensions and bare for loops respectively, and also can't be used in a pipe when looped. fold is used 90% as often as iter and almost twice as often as sum, the next most common HOF.

Granted, map and especially iter are used far more often than we see because they're already written as loops but, folds are certainly used a lot in elm-style programs and simulations, as you often need to update the model over a sequence. Given Zipf's Law, I'd wager that iter and bare for loops in total are used half as often as map and list comprehensions combined, and fold is used a third as often -- which is still quite a lot.

To me, the main downside of using a mutable is that, especially as beginners, we're being encouraged to use mutable as sparingly as possible moreso for the sake of discipline than to avoid any immediate danger of juggling state in a brief loop. And then turning around and instructing them to use mutables as an alternative for something as common as folds sends a mixed message -- something I still remember being a head-scratcher 15+ years ago when I first started with functional programming in F#. It greatly simplifies tutorials and documentation if we don't need to make this concession.

Likewise, the downside of folds, especially for beginners, is that they require memorizing a lot of seemingly arbitrarily ordered arguments: ('S -> 'T -> 'S) -> 'S -> 'T seq -> 'S is pretty overwhelming.

Virtually the only situation you'd ever reach for the ||> operator is to alleviate the problem with folds obscuring the state and xs args behind a giant function argument with potentially several nested brackets. Although it's a dead simple one-liner in the core library as opposed to a language feature as in this proposal, as far as the user is concerned, a new operator is always new syntax. And it flips the argument order and switches from curried to tupled.

Finally, this is a relatively minor point, but if you're trying to draft up a loop or HOF application for a complicated algorithm, you might first try a fold and find that it isn't sufficient. I think both beginners and advanced users alike have this experience routinely. In those situations, refactoring the function argument to an HOF is not as intuitive as adding or removing things to/from a loop.

@krauthaufen
Copy link

Just to clarify, I didn‘t mean to say that folds are not common, I meant to say that the places where you actually benefit from this syntax (meaning shorter or more readable code) seem pretty far fetched to me. The „don‘t use mutables“ argument is actually pretty bad since mutable variables inside a function body don’t harm anyone. Global mutables and mutable objects kill functional programming, not some accumulator variable.

@reinux
Copy link

reinux commented Aug 16, 2025

I wouldn't argue that it's shorter. F# doesn't really go out of its way to make code shorter. It may or may not be more readable to an experienced user either.

The main point is that it can help with adoption if users don't have to go through the whole journey of "Wow, folds are pretty overwhelming, can't I just use a loop? Oh I guess I can but can I do it without using a mutable since I'm trying to practice immutability right now? Oh okay I guess I can use ||> but it's not much of an improvement... I guess I'll stick with folds"

@krauthaufen
Copy link

I don’t see how this should be beginner friendly, you use an imperative construct (a loop) which in its body returns a value which is initialized by using with s = 0 (a keyword suggesting an immutable declaration) where in the loop-body it‘s actually replacing its value with the new one provided and in the end your entire loop returns the overall value. I don‘t claim to have a Solution for your syntactic issues with fold but maybe a simple CE builder for this would also do the job?

@reinux
Copy link

reinux commented Aug 16, 2025

Although it's in a different form, intuitively, it feels a lot like the for loops in C-style languages, with the initialization, the condition and the increment. It's been a while, but I don't remember C for loops ever confusing me. I think it's a pretty big improvement in beginner friendliness. I find that formalized imperative styles are often a good compromise between safety and ease of learning, which is often why list comprehensions are preferred over maps.

The main problem with CEs, which has been pointed out in the RFC, is that they're notoriously difficult to debug.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.