[API Proposal]: Extend the reduce and reduceBack to Collections
Background and motivation
From the requirements point of view: the fold function must provide init, reduce does not require init, which means no longer needs to write boilerplate "match with" code like "extract the first element from generic Collections as init, the remaining elements as sequence, and then pass it to fold".
From a reasonable point of view: list/array/seq all provide native reduce methods, list/array also provides native reduceBack methods. This generic encapsulation is not difficult to implement, and other languages (except fundamental FPs like Haskell) generally provide reduce methods, which is reasonable.
I know there is a reduce function implementation now, but that one is very difficult to use. To be honest, I have no idea how to pass parameters.
API Proposal
let inline reduce (folder: 'State->'T->'State) (foldable: '``Foldable<'T>``) : 'State
let inline reduceBack (folder: 'T->'State->'State) (foldable: '``Foldable<'T>``) : 'State
API Usage
reduce (*) [1; 2; 3; 4; 5] |> should equal 120
reduce (+) ([]: int list) // throws an exception
Alternative Designs
No response
Risks
No response
Are you willing to help with a proof-of-concept (as PR in that or a separate repo) first and as pull-request later on?
Yes, please assign this issue to me.
The existing generic reduce function is a safe function, therefore it operates on Semigroups, more specifically collections which have at least one element.
The reduce functions provided in FSharp.Core are unsafe and although we're not free of unsafe functions here, we love to have some discussions before implementing them.
I think a good compromise could be to implement a tryReduce function. What do you think?
I think the idea of implementing tryReduce is questionable.
- This is a self-made name that does not exist anywhere else (as far as I know), which may cause ambiguity, misleading, or extra learning costs.
- The reduce function provided in FSharp.Core is unsafe, which is not a reason to implement a safe reduce function. Functions with the same name should behave the same way, after all, we are just doing paradigm encapsulation.
- We can also implement a safe reduce function, which is obtained by simplifying the boilerplate code. Specifically, even if the source parameter is actually an empty container (not null), it does not affect our return of Unchecked.defaultof<'State>. Of course, this may introduce other ambiguities,
So I think we should implement the tryReduce function and return the 'State option or something similar. Then keep reduce as an unsafe implementation that encapsulates FSharp.Core.
I would like to hear your opinions again.
If we decide to make reduce "unsafe", based on the fact that in FSharp.Core a function exists already with that name and it's unsafe, we should find another name for the existing safe version of reduce.
Then we have to consider backwards compatibilities, if it's not possible to guarantee, we'll have to hold to v2 for releasing it, however even in this case the PR won't be blocked.
Can we ignore "safety" for now and add the reduce paradigm function of seq/array/list in the current version?
The current reduce function cannot work on these three types, but these three types are the most commonly used types. So there is no conflict, as long as we are willing to ignore the "safety" issue.
We could, but not without adding (or renaming) the existing safe one. Otherwise we'll be kind of losing functionality. Any suggestion for the other function name ?
I believe in your wisdom
I'm not very creative this morning so at the moment I can just propose the name borrowed by Haskell's fold1.
However in Haskell that's a non-safe function, but we don't need to match them.
For the renaming, why not just borrow from Haskell, that is foldl for safe reduce and foldr for safe reduceBack.
For the renaming, why not just borrow from Haskell, that is foldl for safe reduce and foldr for safe reduceBack.
This will lead to breaking updates and affect compatibility.
Personally, I think the root cause is that the current version of the API is poorly designed.
I also think that naming should first consider the consistent meaning in the F# context, and then borrow the conventions of Haskell.
For the renaming, why not just borrow from Haskell, that is foldl for safe reduce and foldr for safe reduceBack.
@BurningLutz those names are following a Haskell convention which doesn't match the one in F#. In F# the convention is, for the left, the function without any suffix and for the right, the function name with Back suffix.
This will lead to breaking updates and affect compatibility.
@fxdmhtt Hmmm, I don't see how it will affect compatibility, but ....
I also think that naming should first consider the consistent meaning in the F# context, and then borrow the conventions of Haskell.
... this is the main point here, we want to stick to F# naming conventions. I just mentioned Haskell the same way I could have mentions Scala or other language.
Personally, I think the root cause is that the current version of the API is poorly designed.
I agree that the API has room for improvement—no design is ever perfect. That said, I do think it’s worth mentioning that, relative to other F# libraries around, this one tries to follow a fairly disciplined approach to API design. Decisions about which functions to expose are made carefully, aiming for consistency and usability across a wide range of types—even if some of those use cases remain more theoretical than practical for now.
That said, if you feel there are fundamental issues with the current design, I’d genuinely appreciate it if you could point them out. I’m always open to improving things where it truly matters and we're collecting input for V2.
Hahahaha, I don't have the ability to find any "fundamental issues", I fully believe in your ability.
I just think that some official APIs need to be converted to proprietary data structures before they can be used in FSharpPlus, which is not convenient and will lead to a low usage rate of these APIs.
I would like to express my sincere gratitude for your hard work.
Thanks, I really appreciate your feedback and kind words.
some official APIs need to be converted to proprietary data structures before they can be used in FSharpPlus,
I'm not entirely sure what you mean here, but I’m guessing you're referring to the wrapping/unwrapping that's sometimes needed—especially to enable different behavior from generic operators.
If that's the case, I'd say a large portion of those situations were addressed with the relatively recent addition of non-sequential applicatives. With that in place, types like ZipList or Validation are much less necessary than they used to be.
That said, we’ll probably keep supporting them since the library is meant to offer multiple ways to approach a problem—so users can pick the one that best fits their style or use case.
@BurningLutz those names are following a Haskell convention which doesn't match the one in F#. In F# the convention is, for the left, the function without any suffix and for the right, the function name with
Backsuffix.
@gusty I see, following the language's convention is important.
But I think I was confused by the conversation between you two.
You said:
The existing generic
reducefunction is a safe function, therefore it operates on Semigroups, more specifically collections which have at least one element.
Which reduce did you refer to? Is this one? But this one is a partial function, it is undefined when the reducible is empty, and a partial function is not considered to be safe.
Further, there really is a safe counterpart of reduce, the fold function. It is defined here.
So, both the unsafe reduce and the safe fold are defined in F#+.
Now I have no idea what's the problem here. Is it the problem that currently there is no Reduce static members defined for builtin collections like list/array/seq inside Foldable?
Now I have no idea what's the problem here. Is it the problem that currently there is no
Reducestatic members defined for builtin collections like list/array/seq inside Foldable?
Absolutely right
Which reduce did you refer to? Is this one? But this one is a partial function, it is undefined when the reducible is empty, and a partial function is not considered to be safe.
@BurningLutz that reduce function is not partial as it will not operate on potentially empty collections, I mean it won't compile, which makes it a total function. Currently there's no way it will throw an exception at runtime.
The proposal is to make it available to potentially empty collections, therefore make it a partial function. The rationale is again implicit F# conventions: the same function exists in F# core and it's partial.
If we do that, we'll need to keep the total function under a different name, hence the question, which name should we use.
I mean it won't compile, which makes it a total function. Currently there's no way it will throw an exception at runtime.
Ok, I think I got it.
You mean the Reduce merely defined a generic reduce function which accepts only two arguments: the binary operator and the reducible. And since there is no Reduce members defined for those collections that may be empty, it is a total function.
I agree with this. In fact I see there are Reduce members for those NonEmpty collections, and they are well-defined.
But I think there do exist some inconsistencies, for example the generic find function. This find is a partial one, which perfectly reflect the original find functions (of array/list/seq etc...) defined in F# core.
But when it comes to reduce, the semantics has changed. It "suddenly" becomes a total one, which is not consistent with the original reduce functions defined in F# core.
I think this is the root that confused OP.
Yes, that's the whole point of this discussion. Any suggestion for the name of the new total function?
I mean it won't compile, which makes it a total function. Currently there's no way it will throw an exception at runtime.
Ok, I think I got it.
You mean the
Reducemerely defined a genericreducefunction which accepts only two arguments: the binary operator and the reducible. And since there is noReducemembers defined for those collections that may be empty, it is a total function.I agree with this. In fact I see there are
Reducemembers for those NonEmpty collections, and they are well-defined.But I think there do exist some inconsistencies, for example the generic
findfunction. Thisfindis a partial one, which perfectly reflect the originalfindfunctions (of array/list/seq etc...) defined in F# core.But when it comes to
reduce, the semantics has changed. It "suddenly" becomes a total one, which is not consistent with the originalreducefunctions defined in F# core.I think this is the root that confused OP.
Yes, but the problem is that now that this is the case, any "error correction" done on the current reduce is a breaking change.
Yes, that's the whole point of this discussion. Any suggestion for the name of the new total function?
My personal suggestion is to make a breaking change.
Even so, it does not affect the existing safe reduce function.
Yes, but the problem is that now that this is the case, any "error correction" done on the current reduce is a breaking change.
How is it a breaking change?
Non-empty collections will still compile with reduce, it's just that if you further change your code, now you will have the possibility to have runtime errors, but that's not a breaking change, since you changed your code.
If we decide to make
reduce"unsafe", based on the fact that in FSharp.Core a function exists already with that name and it's unsafe, we should find another name for the existing safe version of reduce.Then we have to consider backwards compatibilities, if it's not possible to guarantee, we'll have to hold to v2 for releasing it, however even in this case the PR won't be blocked.
I don't think so, but you thought so before.
So can we reach a consensus?
Simply adding a non-safe, generic reduce wrapper for seq/list/array that is completely consistent with the behavior of FSharp.Core is the conclusion of this issue.
How is it a breaking change?
In Haskell, introducing a new instance (or a static member in F#) is considered a potential breaking change. Because users could have already defined their own instance, and this will cause an overlapping/duplicate instance error.
For example, a user could have defined a Reduce member for int array type, making the reduce a total function, and acts like a sum (return 0 if array is empty). I know this is very silly but it has a possibility.
I don't know if such a case will cause breaking change in F# though...
I see, but here is not possible for the user to add a Reduce member for an array type and make it part of the type-classy-like behavior. It would have to be changed in this library.
At some point every single introduction of a new function is a breaking change, in the sense that it might clash with an existing function already named like that, but for obvious pragmatic reasons these kind of (breaking) changes are not taken into consideration.
Simply adding a non-safe, generic reduce wrapper for seq/list/array that is completely consistent with the behavior of FSharp.Core is the conclusion of this issue.
I think at this point in the discussion we all 3 participants so far agree in that. The only blocking question is the name of the new total function.
fold1?reduce1?- something not ending with
1?
Simply adding a non-safe, generic reduce wrapper for seq/list/array that is completely consistent with the behavior of FSharp.Core is the conclusion of this issue.
I think at this point in the discussion we all 3 participants so far agree in that. The only blocking question is the name of the new total function.
fold1?reduce1?- something not ending with
1?
What I mean is, just call reduce directly, by creating a new type Reduce and a static method Invoke.
You are talking about the implementation already. We're not yet there, but yes, that would be the way to implement it.
I think at this point in the discussion we all 3 participants so far agree in that. The only blocking question is the name of the new total function.
* `fold1` ? * `reduce1` ? * something not ending with `1` ?
Just be curious, do we really need to keep this total reduce at all? We already have a perfectly total "reduce" function, the fold. It only has a slightly different calling convention.
I think F# core provides the partial reduce for convenience and the total fold for safety, we could do the same thing.
No, no, no, I mean, the function is called "reduce".
Automatic overloading via type inference.
OK, I think there may be a misunderstanding, so let me recap for clarity:
-
The proposed
reducefunction will operate over all collections, including empty ones. This makes it partial—also referred to as an unsafe function—because it can fail at runtime when given an empty collection. -
The currently existing total (i.e., safe) version of reduce will be replaced by a function with a different name. We’ll need to propose and agree on a suitable name for it.
Just be curious, do we really need to keep this total reduce at all? We already have a perfectly total "reduce" function, the fold. It only has a slightly different calling convention.
Well, I’d argue the difference is more than just a “slightly different calling convention.” fold has a different signature and a higher cognitive load, especially for less experienced developers. While it's true that many total operations can be expressed in terms of a fold, that alone isn't a strong argument for removing purpose-specific total functions like reduce. The presence of a simpler, total reduce makes code more readable and expressive in common use cases—where we want a safe reduction with a natural default or initial value inferred.