Open gsomix opened 2 years ago
What about ranges with steps?
Here's an example of that form: Fantomas AST
And the specific AST nodes for the ForEach (in a modified JSON form for collapsibility):
It seems like a ForEach
member could be exposed that is provided the start/step/end as well for customized iterations?
I think that effectively iterating a collection or a span or range would be a very neat feature for CEs.
I made a few array pool based builders and For is the only place they still allocate I believe. Such feature is definitely a big improvement for perf-oriented CEs
I did a very quick spike on this on this branch: baronfel/fsharp - range-ce-member. the Range member + For member overload approach seems to work well (though as noted above we'd need a solution for steps).
I did a bit of manual testing with the following builder and CE:
I had to manually desugar the CE (using the commented-out quote member) to get something I could put into sharplab to see the results of.
Here's what the desugaring ends up compiling to:
range@162 = new Range(0, 10);
range@162-1 = @_.range@162;
int num = range@162-1.Start.Value;
int value = range@162-1.End.Value;
if (value >= num)
{
while (true)
{
object[] array = new object[1];
array[0] = num;
PrintfFormat<Unit, TextWriter, Unit, Unit> format = new PrintfFormat<Unit, TextWriter, Unit, Unit, int>("%d%P()", array, null);
PrintfModule.PrintFormatLineToTextWriter(Console.Out, format);
num++;
if (num == value + 1)
{
break;
}
}
}
bs@196 = sm@182.Collector.Close();
which is a nice, compact while loop, which I think was the intent. If there's interest I'm happy to continue exploring/flesh out this design.
I've marked this as approved-in-principle
Just to make sure I understand - the proposed Range
member wouldn't need to return a System.Range
, would it? It could return whatever type, and then as long as there was a matching For
member overload that took that type then everything would line up and compilation would succeed?
If that's the case, then a range expression with a step could be handled in a similar way:
RangeWithStep(beginning, step, end)
member that can return any TFor
member callSo technically you can kind of do this today. However you can't use the range syntax and must use something like a tuple:
The super relevant bits:
member inline b.For(
(start, end_) : struct (int*int),
[<InlineIfLambda>] body: int -> ListBuilderCode<'T>) =
ListBuilderCode<_>(fun sm ->
for i = start to end_ do
(body i).Invoke &sm)
...
b {
for x in struct (0,100) do
printfn $"{x}"
}
So you could make a struct that contains the Start/Step/End
values and utilize that as an overload today.
If you still want to control inputs and how they end up to other members, you could reuse the Source
member (although there's little documentation on that member currently) to do what the proposed Range
member is doing. However it would have to account for the m..n
range syntax. Also one of the downsides currently is there must be a corresponding Source
member for each input type (so the input for either For
/Bind
) and it might be worth relaxing that requirement.
Hmm, I have a few questions about this.
(..)
and (.. ..)
in CEs? Fully implementing a fast, correct version of the range and range-step operators for various types is tricky — cf. https://github.com/dotnet/fsharp/pull/16650, https://github.com/dotnet/fsharp/issues/938#issuecomment-231077004, etc. I do see how the proposed Range
member would enable hooking into the existing optimizations automatically, though.
Addition of
InlineIfLambda
attribute in F# 6 opened the way for high-performance computation expressions (imagine one for zero allocation ZString builder). But we can do better. I propose we change translation rules to allow optimisations of for-loops in CE.Consider following code using list.fs builder:
According to F# spec compiler uses following rules to translate this expression:
where
e1 .. e2 (*)
is range operator from standard library that creates sequence of values andFor (**)
is defined as:So simple for-loop allocates sequence and calls
Using
method that might be undesirable in high-performance code. I propose to add new translation rule:where
range
denotesb.Range(e1, e2)
if builderb
containsRange
method. Otherwise,range(e1, e2)
denotese1 .. e2
.It allows to implement same optimisation compiler does for loops where
compiles into effective
while
loop.Let's draft it! First add
Range
method:Then, add
For
overload:The existing way of approaching this problem in F# is override range operator
(..)
. Surprisingly CE uses operator from the context!Pros and Cons
The advantage of making this adjustment to F# is allowing more high-performance scenarios for CE.
The disadvantages of making this adjustment to F# are increasing complexity of translations rules and compiler.
Extra information
Estimated cost (XS, S, M, L, XL, XXL): L
Affidavit (please submit!)
Please tick this 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 :+1: emoji on this issue. These counts are used to generally order the suggestions by engagement.