-
Notifications
You must be signed in to change notification settings - Fork 21
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
"for-with" syntactic sugar for folds #1362
Comments
This is interesting. I have wondered about something like this before... Re: adding something like this to the languageFor something like this to work in F#, there would need to be some type-based and/or structural definition of "foldable" as there currently is for enumerables. Even then, types would actually need to conform in their definitions to whatever convention was decided upon — unless this RFC were implemented: https://github.com/fsharp/fslang-design/blob/cd6085fb9f3a50093938d616fde8776d3be2cdad/RFCs/FS-1043-extension-members-for-operators-and-srtp-constraints.md An alternative approach could be to borrow the idea from C# of using some kind of attribute to tie modules and their Maybe you could use a heuristic like "look for a Computation expressionsIt's already possible to implement something like this yourself using computation expressions. All of the problems above still prevent you from writing a more general computation expression that handles any "foldable" type, though (see, e.g., https://github.com/Savelenko/FSharp.Control.Fold?tab=readme-ov-file#making-your-data-types-compatible-with-the-library). I can think of a few other syntaxes off the top of my head. Here are some very rough proofs of concept: https://gist.github.com/brianrourkeboll/830408adf29fa35c2d027178b9f08e3c
You can extend any of these to handle additional foldable types, including non-enumerable ones, although you need to do it one by one: type FoldBuilder<'T> with
member inline builder.For (option : _ option, [<InlineIfLambda>] body : _ -> FoldBuilderCode<_>) =
FoldBuilderCode<'T> (fun sm ->
match option with
| Some x -> (body x).Invoke &sm
| None -> ())
let two = fold 1 { for x in Some 1 -> (+) x }
type FoldBuilder<'T> with
member inline builder.For (result : Result<_, _>, [<InlineIfLambda>] body : _ -> FoldBuilderCode<_>) =
FoldBuilderCode<'T> (fun sm ->
match result with
| Ok x -> (body x).Invoke &sm
| Error _ -> ())
let three = fold 1 { for x in Ok 2 -> (+) x } I guess you could also do something like this, although again you'd still need to explicitly define extensions: [<Extension>]
type FoldBuilderExtensions =
[<Extension>]
static member inline For<
'Input,
'Extensions,
'Intermediate,
'State when ('Input or 'Extensions) : (static member Fold : ('State -> 'Intermediate -> 'State) * 'State * 'Input -> 'State)
> (
_builder : FoldBuilder<'State, 'Extensions>,
foldable : 'Input,
[<InlineIfLambda>] body : 'Intermediate -> FoldBuilderCode<'State>
) =
let folder sm x =
let mutable sm = sm
(body x).Invoke &sm
sm
let inline call folder state input = ((^Input or ^Extensions) : (static member Fold : ('State -> 'Intermediate -> 'State) * 'State * 'Input -> 'State) (folder, state, input))
FoldBuilderCode<'State> (fun sm -> sm <- call folder sm foldable)
[<Extension>]
type FoldExtensions =
[<Extension>]
static member Fold (folder, state, input) = List.fold folder state input
[<Extension>]
static member Fold (folder, state, input) = Array.fold folder state input
[<Extension>]
static member Fold (folder, state, input) = Set.fold folder state input
[<Extension>]
static member Fold (folder, state, input) = Option.fold folder state input
[<Extension>]
static member Fold (folder, state, input) = ValueOption.fold folder state input
[<Extension>]
static member Fold (folder, state, input) = Result.fold folder state input
[<Extension>]
static member Fold (folder, state, input) = Seq.fold folder state input
let fold<'T> state = FoldBuilder<'T, FoldExtensions> state
let sum = fold 0 { for x in [1..100] -> (+) x }
let sum' = fold 0 { for x in Some 1 -> (+) x }
let sum'' = fold 0 { for x in set [1..100] -> (+) x }
let sum''' = fold 0 { for x in [|1..100|] -> (+) x } |
Thanks for the detailed reply! The CEs do indeed look nice. I probably wouldn't hesitate to use them myself if they were built-in, but I do have to wonder if it might be a bit magical-looking for beginners? Maybe instead of unrolling to let result =
let mutable s = s
let enr = ts.GetEnumerator()
while enr.MoveNext() do
s <- f s enr.Current
s We do lose some rigor, but my gut feeling is that that might not be so bad for a semi-imperative language like F#, as long as it's documented that that's what it's doing under the hood. Unless I'm missing something, it should be fairly rare that |
I propose we add syntax sugar to turn
into
The
with
keyword introduces the initial state, and the value inside thefor
expression is expected to be the same as the initial state.The existing ways of approaching this problem in F# are
fold
Folds are widely used yet slightly unwieldy, especially for beginners but even for experienced users. Nested folds tend to get really messy if we aren't extra careful with formatting. There are also several ways to write them:
(state, list) ||> fold f
,list |> fold f state
,fold f state list
. Each style has its detractors, often not without good reason. Only the first offers type inference for bothstate
and `list within the function, but is a somewhat obscure approach.let rec f s ts = ...
Ad-hoc recursive functions are a bit verbose, so we don't use them unless we really need to.
let mutable s in for t in ts do s <- ...
Finally, there probably isn't anything wrong with using a mutable accumulator, but it's just not something people like to reach for because it feels icky.
Pros and Cons
The advantages of making this adjustment to F# are
s
is specified prior to the function)fold
function or for trailing arguments, which tend to be a bit unsightly following longfun
sfor
loopThe disadvantages of making this adjustment to F# are
Yet more syntax to document and learn. On the other hand, it seems quite intuitive and hence easy to remember.
I don't think it should require any changes to the API for CEs, but I could be wrong.
Extra information
Estimated cost (XS, S, M, L, XL, XXL):
M (please correct me if I am wrong)
Related suggestions: (put links to related suggestions here)
Perhaps we can have a
yield
version that translates into ascan
:There is some slight ambiguity here, as
yield
is no longer a required keyword when thefor
is used in a sequence/list/array. Some users may expectfor-with
to behave as ascan
even without theyield
keyword if it is within a list.Please let me know if I should open another issue for this suggestion, and I will update with the link here.
Affidavit (please submit!)
Please tick these items by placing a cross in the box:
Please tick all that apply:
For Readers
If you would like to see this issue implemented, please click the 👍 emoji on this issue. These counts are used to generally order the suggestions by engagement.
The text was updated successfully, but these errors were encountered: