|
| 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