Skip to content

Commit b3cdb58

Browse files
authored
RFC FS-1329 - Allow typed bindings(let!, use!, and!) in CE without parentheses. (#802)
1 parent 24eda06 commit b3cdb58

File tree

1 file changed

+308
-0
lines changed

1 file changed

+308
-0
lines changed
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
# F# RFC FS-1329 - Allow typed bindings in CE let!s without parentheses
2+
3+
The design suggestion [Allow typed bindings in CE let!s without parentheses](https://github.com/fsharp/fslang-suggestions/issues/1329) 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/1329)
8+
- [x] Approved in principle
9+
- [x] [Implementation](https://github.com/dotnet/fsharp/pull/10697)
10+
- [x] [Discussion](https://github.com/fsharp/fslang-design/discussions/XXXX)
11+
12+
# Summary
13+
14+
Allow computation expression (CE) bindings type annotations without requiring parentheses:
15+
* `let!` `and!` to accept type annotations on simple patterns without requiring parentheses, making them consistent with regular `let` bindings.
16+
* `use!` to accept type annotations on simple patterns without requiring parentheses, making them consistent with regular `use` bindings.
17+
18+
# Motivation
19+
20+
Currently, when using type annotations with `let!`, `use!`, and `and!` bindings in computation expressions, parentheses are always required around the pattern with its type annotation, even for simple identifiers. This differs from regular `let` and `use` bindings where parentheses are only required for complex patterns.
21+
22+
This inconsistency is:
23+
- **A syntactical foot-gun** that is frustrating and confusing to users
24+
- **Inconsistent** with the general design of F# where `let!` should behave similarly to `let` where possible
25+
- **A common source of compiler errors** for developers working with computation expressions
26+
- **Particularly problematic for F# newcomers** who are already grappling with computation expressions
27+
- **Unhelpful error messages**: Getting "Unexpected symbol ':'" doesn't immediately suggest that parentheses are needed
28+
29+
# Detailed design
30+
31+
## Scenario: `let!` bindings
32+
33+
`let!` bindings with type annotations on simple identifiers require parentheses:
34+
35+
```fsharp
36+
// Before
37+
async {
38+
let! (data: byte[]) = readAsync()
39+
let! (x: int) = async { return 42 }
40+
}
41+
42+
// After
43+
async {
44+
let! data: byte[] = readAsync()
45+
let! x: int = async { return 42 }
46+
}
47+
```
48+
49+
## Scenario: `use!` bindings
50+
51+
`use!` bindings with type annotations on simple identifiers require parentheses:
52+
53+
```fsharp
54+
// Before
55+
task {
56+
use! (conn: SqlConnection) = openConnectionAsync()
57+
use! (resource: IDisposable) = openResourceAsync()
58+
}
59+
60+
// After
61+
task {
62+
use! conn: SqlConnection = openConnectionAsync()
63+
use! resource: IDisposable = openResourceAsync()
64+
}
65+
```
66+
67+
## Scenario: `and!` bindings
68+
69+
`and!` bindings with type annotations on simple identifiers require parentheses:
70+
71+
```fsharp
72+
// Before
73+
async {
74+
let! (userId: int) = getUserIdAsync()
75+
and! (permissions: Permission list) = getPermissionsAsync()
76+
and! (profile: UserProfile) = getProfileAsync()
77+
}
78+
79+
// After
80+
async {
81+
let! userId: int = getUserIdAsync()
82+
and! permissions: Permission list = getPermissionsAsync()
83+
and! profile: UserProfile = getProfileAsync()
84+
}
85+
```
86+
87+
## Scenario: Tuple patterns
88+
89+
Tuple patterns with type annotations require parentheses (same as regular `let`):
90+
91+
```fsharp
92+
// Before (and still required after)
93+
async {
94+
let! (a, b): int * string = asyncTuple()
95+
and! (x, y): float * bool = asyncPair()
96+
}
97+
98+
// After - parentheses still required (consistent with regular let)
99+
async {
100+
let! (a, b): int * string = asyncTuple()
101+
and! (x, y): float * bool = asyncPair()
102+
}
103+
104+
Tuple tuples with each element having direct type annotations require parentheses (same as regular `let`):
105+
106+
```fsharp
107+
let a: int, b: string = (5, 3) // Not allowed
108+
let! a: int, b: string = (5, 3) // Not allowed
109+
110+
let (a, b): int * int = (5, 3) // Allowed
111+
let! (a, b): int * int = (5, 3) // Allowed
112+
let a, b: int * int = (5, 3) // Allowed
113+
let! a, b: int * int = (5, 3) // Allowed
114+
let (a: int, b: string): int * int = (5, 3) // Allowed
115+
let! (a: int, b: string): int * int = (5, 3) // Allowed
116+
117+
// Before (and still required after)
118+
async {
119+
let! (a: int, b: string): int * string = asyncTuple()
120+
and! (x: int, y: bool): float * bool = asyncPair()
121+
}
122+
123+
// After - parentheses still required (consistent with regular let)
124+
async {
125+
let! (a: int, b: string): int * string = asyncTuple()
126+
and! (x: int, y: bool): float * bool = asyncPair()
127+
}
128+
```
129+
130+
## Scenario: Record patterns
131+
132+
Record patterns with type annotations don't require parentheses:
133+
134+
```fsharp
135+
// Before - parentheses required
136+
async {
137+
let! ({ Name = name; Age = age }: Person) = asyncPerson()
138+
and! ({ Id = id }: User) = asyncUser()
139+
}
140+
141+
// After - no parentheses needed
142+
async {
143+
144+
let! { Name = name; Age = age }: Person = asyncPerson()
145+
and! { Id = id }: User = asyncUser()
146+
}
147+
```
148+
149+
## Scenario: Union patterns
150+
151+
Union patterns with type annotations require parentheses (same as regular `let`):
152+
153+
```fsharp
154+
// Before - parentheses required
155+
async {
156+
let! (Union value: int option) = asyncOption()
157+
and! ((Union result): Result<string, Error>) = asyncResult()
158+
}
159+
160+
// After - no parentheses needed (consistent with regular let)
161+
async {
162+
let! Union value: int option = asyncOption()
163+
and! Union result: Result<string, Error> = asyncResult()
164+
}
165+
```
166+
167+
## Scenario: As patterns
168+
169+
As patterns with type annotations require parentheses (same as regular `let`):
170+
171+
```fsharp
172+
// Before - require parentheses
173+
async {
174+
let! ((x as y): int) = asyncInt()
175+
let! (x as y: int) = asyncInt()
176+
and! ((a as b): string) = asyncString()
177+
and! (x as y: int) = asyncInt()
178+
}
179+
180+
// After - no parentheses needed (consistent with regular let)
181+
async {
182+
let! (x as y): int = asyncInt()
183+
let! x as y: int = asyncInt()
184+
and! (a as b): string = asyncString()
185+
and! x as y: int = asyncInt()
186+
}
187+
```
188+
189+
## Scenario: Array and list patterns
190+
191+
Array and list patterns with type annotations don't require parentheses:
192+
193+
```fsharp
194+
// Before - parentheses required
195+
async {
196+
let! ([| first; second |]: int array) = asyncArray()
197+
and! (head :: tail: string list) = asyncList()
198+
}
199+
200+
// After - no parentheses needed (consistent with regular let)
201+
async {
202+
let! [| first; second |]: int array = asyncArray()
203+
and! head :: tail: string list = asyncList()
204+
}
205+
```
206+
207+
**Key principle**: After this change, CE bindings follow the exact same parenthesization rules as regular `let` bindings.
208+
209+
## Grammar Changes
210+
211+
The parser has been extended to accept type annotations in CE binding patterns without parentheses. The grammar for CE bindings has been updated to accept:
212+
213+
```
214+
cexpr =
215+
| ...
216+
| 'let!' pat ':' type '=' expr 'in' cexpr
217+
| 'use!' pat ':' type '=' expr 'in' cexpr
218+
| ...
219+
220+
moreBinders =
221+
| 'and!' pat ':' type '=' expr 'in' moreBinders
222+
| ...
223+
```
224+
225+
Where previously only parenthesized patterns with type annotations were accepted:
226+
227+
```
228+
| 'let!' '(' pat ':' type ')' '=' expr 'in' cexpr
229+
| 'and!' '(' pat ':' type ')' '=' expr 'in' moreBinders
230+
```
231+
232+
## Implementation Details
233+
234+
The implementation in the parser (`pars.fsy`) adds new rules for each binding type:
235+
236+
1. For `let!` and `use!` bindings:
237+
- Regular CE bindings with `opt_topReturnTypeWithTypeConstraints`
238+
- Offside-sensitive CE bindings with `opt_topReturnTypeWithTypeConstraints`
239+
240+
2. For `and!` bindings:
241+
- Regular `and!` bindings with `opt_topReturnTypeWithTypeConstraints`
242+
- Offside-sensitive `and!` bindings with `opt_topReturnTypeWithTypeConstraints`
243+
244+
## Compatibility
245+
246+
* Is this a breaking change?
247+
* No. This change only allows syntax that was previously rejected by the compiler.
248+
249+
* What happens when previous versions of the F# compiler encounter this design addition as source code?
250+
* Older compiler versions will continue to emit error FS0010 when encountering type annotations without parentheses in CE bindings.
251+
252+
* What happens when previous versions of the F# compiler encounter this design addition in compiled binaries?
253+
* This is a purely syntactic change that doesn't affect the compiled output. Older compiler versions will be able to consume binaries without issue.
254+
255+
* If this is a change or extension to FSharp.Core, what happens when previous versions of the F# compiler encounter this construct?
256+
* N/A - This is a syntactic change only.
257+
258+
# Pragmatics
259+
260+
## Diagnostics
261+
262+
The existing error message (FS0010: Unexpected symbol ':' in expression) could be improved when encountered in the context of a CE binding to suggest adding parentheses (for older compilers) or updating the compiler version.
263+
264+
## Tooling
265+
266+
Please list the reasonable expectations for tooling for this feature, including any of these:
267+
268+
* Debugging
269+
* Breakpoints/stepping
270+
* N/A.
271+
* Expression evaluator
272+
* N/A.
273+
* Data displays for locals and hover tips
274+
* N/A.
275+
* Auto-complete
276+
* N/A.
277+
* Tooltips
278+
* N/A.
279+
* Navigation and go-to-definition
280+
* N/A.
281+
* Error recovery (wrong, incomplete code)
282+
* N/A.
283+
* Colorization
284+
* N/A.
285+
* Brace/parenthesis matching
286+
- [UnnecessaryParenthesesDiagnosticAnalyzer](https://github.com/dotnet/fsharp/blob/main/vsintegration/src/FSharp.Editor/Diagnostics/UnnecessaryParenthesesDiagnosticAnalyzer.fs) should be updated to recognize that parentheses are not needed for type annotations in CE bindings, and should not suggest adding them.
287+
288+
## Performance
289+
290+
<!-- Please list any notable concerns for impact on the performance of compilation and/or generated code -->
291+
292+
* No performance or scaling impact is expected.
293+
294+
## Scaling
295+
296+
<!-- 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. -->
297+
298+
* N/A.
299+
300+
## Culture-aware formatting/parsing
301+
302+
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.
303+
304+
* No.
305+
306+
# Unresolved questions
307+
308+
* None.

0 commit comments

Comments
 (0)