Skip to content

Commit 424fd1c

Browse files
authored
RFC FS-1031 - Mixing ranges and values to construct sequences (#804)
1 parent 12c886f commit 424fd1c

File tree

1 file changed

+254
-0
lines changed

1 file changed

+254
-0
lines changed
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
# F# RFC FS-1031 - Mixing ranges and values to construct sequences
2+
3+
The design suggestion [Mixing ranges and values to construct sequences](https://github.com/fsharp/fslang-suggestions/issues/1031) has been marked "approved in principle."
4+
5+
This RFC covers the detailed proposal for this suggestion.
6+
7+
- [x] [Suggestion](https://github.com/fsharp/fslang-suggestions/issues/1031)
8+
- [x] Approved in principle
9+
- [x] [Implementation](https://github.com/dotnet/fsharp/pull/18670)
10+
- [x] [Discussion](https://github.com/fsharp/fslang-design/discussions/803)
11+
12+
# Summary
13+
14+
Allow ranges to be directly mixed with individual values in sequence, list, and array expressions without requiring `yield!` for the range. This feature also automatically extends to custom computation expressions that implement the standard builder methods.
15+
16+
# Motivation
17+
18+
Currently, when constructing sequences that mix ranges with individual values, F# requires the use of `yield!` to include the range. This leads to verbose and less readable code for a common pattern.
19+
20+
This inconsistency is:
21+
- **Unnecessarily verbose** for a common pattern in sequence construction
22+
- **Inconsistent** with the intuitive expectation that ranges should compose naturally with other sequence elements
23+
- **A source of friction** for developers who frequently work with sequences mixing ranges and values
24+
- **Less readable** than the proposed syntax, particularly when multiple ranges and values are combined
25+
- **Limiting for custom computation expressions** that could benefit from the same syntax simplification
26+
27+
# Detailed design
28+
29+
## Scenario: Sequence expressions
30+
31+
Sequence expressions can mix ranges and values directly:
32+
33+
```fsharp
34+
// Before
35+
let a = seq { yield! seq { 1..10 }; 19 }
36+
37+
// After
38+
let a = seq { 1..10; 19 }
39+
```
40+
41+
## Scenario: List expressions
42+
43+
List expressions can mix ranges and values directly:
44+
45+
```fsharp
46+
// Before
47+
let b = [ -3; yield! [1..10] ]
48+
let c = [ yield! [1..10]; 19; yield! [25..30] ]
49+
50+
// After
51+
let b = [ -3; 1..10 ]
52+
let c = [ 1..10; 19; 25..30 ]
53+
```
54+
55+
## Scenario: Array expressions
56+
57+
Array expressions can mix ranges and values directly:
58+
59+
```fsharp
60+
// Before
61+
let d = [| -3; yield! [|1..10|]; 19 |]
62+
let e = [| yield! [|1..5|]; 10; yield! [|15..20|] |]
63+
64+
// After
65+
let d = [| -3; 1..10; 19 |]
66+
let e = [| 1..5; 10; 15..20 |]
67+
```
68+
69+
## Scenario: Complex expressions with multiple ranges
70+
71+
The feature supports any number of ranges and values in any order:
72+
73+
```fsharp
74+
// Before
75+
let complex = seq {
76+
0
77+
yield! seq { 1..5 }
78+
10
79+
yield! seq { 11..15 }
80+
20
81+
yield! seq { 21..25 }
82+
}
83+
84+
// After
85+
let complex = seq {
86+
0
87+
1..5
88+
10
89+
11..15
90+
20
91+
21..25
92+
}
93+
```
94+
95+
## Scenario: Step ranges
96+
97+
The feature also works with step ranges:
98+
99+
```fsharp
100+
// Before
101+
let stepped = [ yield! [0..2..10]; 15; yield! [20..5..40] ]
102+
103+
// After
104+
let stepped = [ 0..2..10; 15; 20..5..40 ]
105+
```
106+
107+
## Implementation Details
108+
109+
The implementation modifies the type checker to handle range expressions in sequence contexts by transforming them during the checking phase rather than requiring parser changes. The key components modified are:
110+
111+
### 1. CheckArrayOrListComputedExpressions.fs
112+
113+
- Added detection for range expressions (`SynExpr.IndexRange`) in list/array expressions
114+
- Transforms mixed lists/arrays with ranges into sequence computation expressions
115+
- For example, `[-3; 1..10; 19]` is transformed to `seq { yield -3; yield! 1..10; yield 19 }` during type checking
116+
- The resulting sequence is then converted back to a list or array using `Seq.toList` or `Seq.toArray`
117+
118+
### 2. CheckSequenceExpressions.fs
119+
120+
- Extended to handle mixed ranges in sequence expressions directly
121+
- Detects when a sequence contains range expressions and transforms them appropriately
122+
- Builds the sequence body by converting ranges to `yield!` and values to `yield`
123+
124+
### 3. CheckComputationExpressions.fs
125+
126+
- Added support for computation expressions that contain ranges
127+
- Checks if the builder supports the required methods (`Yield`, `YieldFrom`, `Combine`, `Delay`)
128+
- Transforms sequential expressions with ranges into appropriate yields and yield-froms
129+
130+
## Benefits for Custom Computation Expressions
131+
132+
This feature automatically benefits any custom computation expression builder that implements the standard CE methods (`Yield`, `YieldFrom`, `Combine`, `Delay`). For example:
133+
134+
```fsharp
135+
type MyBuilder() =
136+
member _.Yield(x) = [x]
137+
member _.YieldFrom(xs: seq<_>) = List.ofSeq xs
138+
member _.Combine(a, b) = a @ b
139+
member _.Delay(f) = f
140+
member _.Run(f) = f()
141+
142+
let mybuilder = MyBuilder()
143+
144+
// This now works automatically!
145+
let result = mybuilder { 1; 2..5; 10 } // [1; 2; 3; 4; 5; 10]
146+
```
147+
148+
## Non-interference with Custom Range Operators
149+
150+
This feature only applies to the built-in range syntax and does not interfere with custom implementations of the range operator. If someone defines their own `(..)` operator:
151+
152+
```fsharp
153+
open System.Collections
154+
open System.Collections.Generic
155+
156+
type X(elements: X list) =
157+
member this.Elements = elements
158+
159+
interface IEnumerable<X> with
160+
member this.GetEnumerator() =
161+
(this.Elements :> IEnumerable<X>).GetEnumerator()
162+
163+
member this.GetEnumerator() : IEnumerator =
164+
(this.Elements :> IEnumerable).GetEnumerator()
165+
166+
static member Combine(x1: X, x2: X) =
167+
X(x1.Elements @ x2.Elements)
168+
169+
let (..) a b = seq { X.Combine(a,b) }
170+
171+
let a = X([])
172+
let b = X([])
173+
174+
let whatIsThis = seq { a..b }
175+
176+
let result1 = [ whatIsThis ;a ; b]
177+
let result2 = [ yield! whatIsThis; a; b ]
178+
```
179+
180+
The transformation happens at the syntax tree level for `SynExpr.IndexRange` nodes, which are only created by the built-in range syntax, not by custom operators. This ensures backward compatibility while enabling the new feature.
181+
182+
## Compatibility
183+
184+
* Is this a breaking change?
185+
* No. This change only allows syntax that was previously rejected by the compiler.
186+
187+
* What happens when previous versions of the F# compiler encounter this design addition as source code?
188+
* Older compiler versions will emit a syntax error when encountering ranges without `yield!` in sequence expressions.
189+
190+
* What happens when previous versions of the F# compiler encounter this design addition in compiled binaries?
191+
* This is a purely syntactic change that doesn't affect the compiled output. Older compiler versions will be able to consume binaries without issue.
192+
193+
* If this is a change or extension to FSharp.Core, what happens when previous versions of the F# compiler encounter this construct?
194+
* N/A - This is a syntactic change only.
195+
196+
# Pragmatics
197+
198+
## Diagnostics
199+
200+
The compiler provides clear error messages when:
201+
- The feature is used without the preview language flag enabled
202+
- Invalid range syntax is used
203+
- Type mismatches occur between range elements and other sequence elements
204+
205+
When the feature is not enabled, users see:
206+
```
207+
Feature 'Allow mixed ranges and values in sequence expressions, e.g. seq { 1..10; 20 }' is not available in F# 9.0. Please use language version 'PREVIEW' or greater.
208+
```
209+
210+
## Tooling
211+
212+
Please list the reasonable expectations for tooling for this feature, including any of these:
213+
214+
* Debugging
215+
* Breakpoints/stepping
216+
* N/A.
217+
* Expression evaluator
218+
* N/A.
219+
* Data displays for locals and hover tips
220+
* N/A.
221+
* Auto-complete
222+
* N/A.
223+
* Tooltips
224+
* N/A.
225+
* Navigation and go-to-definition
226+
* N/A.
227+
* Error recovery (wrong, incomplete code)
228+
* N/A.
229+
* Colorization
230+
* N/A.
231+
* Brace/parenthesis matching
232+
* N/A.
233+
234+
## Performance
235+
236+
<!-- Please list any notable concerns for impact on the performance of compilation and/or generated code -->
237+
238+
* No performance or scaling impact is expected.
239+
240+
## Scaling
241+
242+
<!-- Please list the dimensions that describe the inputs for this new feature, e.g. "number of widgets" etc. For each, estimate a reasonable upper bound for the expected size in human-written code and machine-generated code that the compiler will accept. -->
243+
244+
* N/A.
245+
246+
## Culture-aware formatting/parsing
247+
248+
Does the proposed RFC interact with culture-aware formatting and parsing of numbers, dates and currencies? For example, if the RFC includes plaintext outputs, are these outputs specified to be culture-invariant or current-culture.
249+
250+
* No.
251+
252+
# Unresolved questions
253+
254+
* None.

0 commit comments

Comments
 (0)