Names for tuple fields and function parameter types
I propose we allow this:
// Function with inferred parameter names and tuple elements - more than one : on the same parameter currently doesn't type check
let transformPoint (scale: float) ((x, y): x: float * y: float) (adjustments: (dx: float * dy: float) list) : resultX: float * resultY: float =
// or require parentheses
// let transformPoint (scale: float) ((x, y): (x: float * y: float)) (adjustments: (dx: float * dy: float) list) : (resultX: float * resultY: float) =
// Apply scaling
let scaled = x * scale, y * scale
// Apply all adjustments
let final =
adjustments
|> List.fold (fun (cx, cy) (dx, dy) -> cx + dx, cy + dy) scaled
// Return named tuple (explicit dot syntax) (structness of tuple is inferrable)
.resultX = fst final, .resultY = snd final
// val transformPoint : scale: float -> x: float * y: float -> adjustments: (dx: float * dy: float) list -> resultX: float * resultY: float
// Partial application preserving names
let scaleAndAdjust = transformPoint (.scale = 2.0) // Named parameters for functions, partially applied function preserves parameter names
// val scaleAndAdjust : x: float * y: float -> adjustments: (dx: float * dy: float) list -> resultX: float * resultY: float
// Usage
let initialPoint = .x = 3.0, .y = 4.0 // Named tuple creation (structness of tuple is inferrable)
// val initialPoint : x: float * y: float
type Points = static member Create(dx, dy) = .dx = dx, .dy = dy
let adjustments = [
.dx = 0.5, .dy = -1.0 // tuple with names
Points.Create(.dx = -1.0, .dy = 2.0) // dot-prefixed syntax can also be applied to method arguments
// note: like in C#, tuple field names only unify if all tuples share the same field names.
]
// val adjustments : (dx: float * dy: float) list
let result = scaleAndAdjust initialPoint adjustments
// val result : resultX: float * resultY: float
match result with resultX = x, resultY = y -> printfn $"{x}, {y}" // Field patterns on tuple patterns
printfn $"{result.resultX}, {result.resultY}" // Accessing tuple fields by name
The existing way of approaching this problem in F# loses all these contextual information regarding function parameter and tuple names.
Pros and Cons
The advantages of making this adjustment to F# are
- Preserving important contextual information that helps robust programming
- Interop with C# for struct tuple field names (emitting the necessary attributes, and consuming C# defined tuples too)
The disadvantages of making this adjustment to F# is more information to track in the type signatures.
Extra information
Estimated cost (XS, S, M, L, XL, XXL): L
Related suggestions:
- Named argument application for functions https://github.com/fsharp/fslang-suggestions/issues/108 https://github.com/fsharp/fslang-suggestions/issues/961#issuecomment-767530376
- Tuple field names https://github.com/fsharp/fslang-suggestions/issues/616 https://github.com/fsharp/fslang-suggestions/issues/1354
- Preserving parameter names on function partial application https://github.com/fsharp/fslang-suggestions/issues/1134
- Dot-prefixed named argument application for methods https://github.com/fsharp/fslang-suggestions/issues/1414
- Named arguments for generic parameters https://github.com/fsharp/fslang-suggestions/issues/610
- , separator for DU case fields instead of ; https://github.com/fsharp/fslang-suggestions/issues/957
- Object/property/field patterns https://github.com/fsharp/fslang-suggestions/issues/968
Affidavit (please submit!)
Please tick these items by placing a cross in the box:
- [x] This is not a question (e.g. like one you might ask on StackOverflow) and I have searched StackOverflow for discussions of this issue
- [x] This is a language change and not purely a tooling change (e.g. compiler bug, editor support, warning/error messages, new warning, non-breaking optimisation) belonging to the compiler and tooling repository
- [x] This is not something which has obviously "already been decided" in previous versions of F#. If you're questioning a fundamental design decision that has obviously already been taken (e.g. "Make F# untyped") then please don't submit it
- [x] I have searched both open and closed suggestions on this site and believe this is not a duplicate
Please tick all that apply:
- [x] This is not a breaking change to the F# language design
- [x] I or my company would be willing to help implement and/or test this
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.
I'm unsure if the leading dot syntax is good, also, there are many places in the language where naming things that are anonymous, should be considered in context of supporting tuple with named fields.
The syntax we would use, ideally, would work for active patterns implementation.
related:
- #1341 discusses the fact active patterns can't have named fields
- #1354 is about consuming tuples with named fields
- #207 and https://github.com/fsharp/fslang-design/blob/main/FSharp-4.6/FS-1030-anonymous-records.md discusses about tuples with named fields
edit: It almost feels that with type directed things, everything with named fields, could use vanilla records syntax, and everything with positional fields, tuple syntax.
Resurrecting := is also possible - (item1 := 1, item2 := 2) if there is no explicitly typed ref box called item1 nor item2 in scope and (:=) is not redefined.
Ideally the same syntax can be reused across named arguments (functions and methods) and tuples - this is why I chose the dot-prefix as suggested in https://github.com/fsharp/fslang-suggestions/issues/1414.
As for the dot issue[^1], I assume Don meant that we wouldn't want to use OCaml's f ~x:3 for labeled function arguments—i.e., specifically using : instead of =—since we already have C.M (x=3) for methods:
Given the issue with
x=y, would it not be too terrible to have OCAML syntax?We would just put it under a
/langversion:5.0switch.There's no chance we would use
~syntax here, given we already support named arguments for member calls.
But using ~ to unambiguously indicate a parameter label in both method and function applications—and maybe tuple construction, active patterns, etc.—while keeping =, might work.
E.g.,
let f x = x
type C = static member M x = x
let x = 3
// Current
f x // 3
f (x=x) // true
C.M (x=x) // 3
C.M ((x=x)) // true
// Proposed: all of the above remain valid, but we can now explicitly label parameters in applications with ~
f ~x=x // 3
C.M (~x=x) // 3
(Note that I am not proposing that ~ be used anywhere other than application.)
~ has the advantage over . that it could be used in function applications without requiring extra parenthesization:
let x = 3
let y = 4
f ~x=x ~y=y
. would require parentheses, or else the parameter label would be interpreted as a member lookup on the function:
let x = 3
f .x=x // This would be parsed as `(f.x) = x`.
[^1]: I'm not sure whether this, https://github.com/fsharp/fslang-suggestions/issues/151, or https://github.com/fsharp/fslang-suggestions/issues/1414 is the best place to discuss it... But this is the latest of those, so here we are.
How would one explain that this choice of usages - named tupled fields and function parameters - is in an abstract way the same concept and therefore should use the same syntactical (~) symbol?
Is it because parameters (even when curried, even when of size 1) can be seen as tuples and vice versa?
@T-Gro Whenever the type signature uses * to link function or method parameters or tuple fields together, they correspond to , in the expression syntax (aside from DU field deconstruction but changing it from ; to , is also approved). Moreover, a syntactic tuple at a function or method parameter is represented by C# method parameters, linking them together.
So it makes sense to use ~ wherever , can appear.
This corresponds to C#'s usage of : named parameters across both methods and tuples.
Not a fan of introducing new syntactic usages for either dots or tildes. Why not indicate element names as part of type annotation rather than tuple construction, the way C# does it?
I think the usage remains marginal, I'd like to see more named-field tuples usage in BCL and overall C# codebases, and consuming them be implemented first, and agree with @kerams that we could limit the scope of "constructing" such tuples to the strict minimum (explicit signature), for the time being.
F# may evolve to use some syntax that covers more named field tuples literals convenience, but the first step is minimum conservative adjustments for interop support.
@kerams
Not a fan of introducing new syntactic usages for either dots or tildes
See https://github.com/fsharp/fslang-suggestions/issues/1414#issuecomment-3052989822 - this is about disambiguation of initialization from equality.
I am happy with that solution (definitely "both") as well. Anything that distinguishes an assignment to parameter/property/field from the equality operator (and only from the equality operator) is fine by me.
Why not indicate element names as part of type annotation rather than tuple construction, the way C# does it?
C# allows both type annotation and tuple construction for tuple field names.
(int a, string) tuple = (1, b: "");
https://github.com/fsharp/fslang-suggestions/issues/1434#issuecomment-3052910332
Why not indicate element names as part of type annotation rather than tuple construction, the way C# does it?
https://github.com/fsharp/fslang-suggestions/issues/1434#issuecomment-3053489828
C# allows both type annotation and tuple construction for tuple field names.
(int a, string) tuple = (1, b: "");
Yes, I've literally been writing C# today using named tuple items, although the primary reason is because in C# you cannot deconstruct tuple parameters. Here's a contrived example:
var d =
someCollection
.SelectMany(x => x.Ys, (x, y) => (Q: x.A, R: y.B + y.C))
.ToDictionary(tup => tup.Q, tup => tup.R);
// Otherwise requires tup.Item1, etc., or { var (a, _) = tup; return a; }
Aside from ./~ prefix, we can also just use ~ as an infix operator as a single ~ is a syntactic error today.
let transformPoint (scale: float) ((x, y): x: float * y: float) (adjustments: (dx: float * dy: float) list) : resultX: float * resultY: float =
let scaled = x * scale, y * scale
let final =
adjustments
|> List.fold (fun (cx, cy) (dx, dy) -> cx + dx, cy + dy) scaled
resultX ~ fst final, resultY ~ snd final
let scaleAndAdjust = transformPoint (scale ~ 2.0)
// or we can also allow ~ without parentheses:
// let scaleAndAdjust = transformPoint scale ~ 2.0
// let scaleAndAdjust = transformPoint scale~2.0
let initialPoint = x ~ 3.0, y ~ 4.0
type Points = static member Create(dx, dy) = ~~dx, ~~dy
// if (~~) resolves to the operator in FSharp.Core, then above expands to: (nameof dx) ~ dx, (nameof dy) ~ dy
let adjustments = [
dx ~ 0.5, dy ~ -1.0
Points.Create(dx ~ -1.0, dy ~ 2.0)
]
let result = scaleAndAdjust initialPoint adjustments
match result with resultX = x, resultY = y -> printfn $"{x}, {y}"
printfn $"{result.resultX}, {result.resultY}"
I am still none the wiser as to why we'd want special expression syntax for this when SynExpr.Typed exists, meaning you could just tack : (dx: float * dy: float) onto any tuple construction (the utility of which I do not understand either).
let initialPoint: (dx: float * dy: float) = 3.0, 4.0
let initialPoint = (3.0, 4.0): (dx: float * dy: float)
Aside from
./~prefix, we can also just use~as an infix operator as a single~is a syntactic error today.
I don't think we'd want that.
I think OCaml's usage of : in ~x:3 instead of F#'s existing = in x=3 (in methods, constructors, union pattern matching, etc.) was probably what Don was referring to in https://github.com/fsharp/fslang-suggestions/issues/151#issuecomment-474111193. So I'm pretty sure we'd want to keep the =.
@kerams
I am still none the wiser as to why we'd want special expression syntax for this when
SynExpr.Typedexists, meaning you could just tack: (dx: float * dy: float)onto any tuple construction.
it's verbose and doesn't play well with struct tuple inference - you'd likely need to write things like 1.0, 2.0 : struct(dx:_ * dy:_) and 1.0, 2.0 : dx:_ * dy:_
No to "inferred names" because the name of a thing should not affect its type. If x is a tuple and let (a, b) = x and let (c, d) are bindings then clearly a,b and c,d are eternal to x and the letters a,b,c,d are not part of the definition of x. This principle then applies to binding within method arguments.
It would be good to support C# ValueTuple names and would be ideal if we could merge this into anonymous records, so that named dotnet ValueTuples are the implementation of F# anonymous records. "To align with changes in dotnet, we changed the anonymous record implementation from reference to value types" would be a perfectly good justification for a small behavioral change here.
... and would be ideal if we could merge this into anonymous records, so that named dotnet ValueTuples are the implementation of F# anonymous records.
Actually anonymous records can be value types
> struct {|A=1|};;
val it: struct {| A: int |} = { A = 1 }
@charlesroddie The original design document of struct anonymous records explicitly chose concrete struct types instead of ValueTuple to enable reflection scenarios (highly relevant in boilerplate-free serialization). C# ValueTuple and F# struct anonymous records are mutually exclusive and switching to a ValueTuple representation for F# struct tuples is a breaking change for these scenarios.
Are the C# named tuples a common thing to be exposed across assembly boundaries? If yes, what are the usage examples?
If working with named tuples from C# is a common pain point, I think it is better to design it as a separate language suggestion targeting just that interop concern. Translating to/from attribute metadata about the names that C# compiler puts on, without affecting the inner working of F#.
@T-Gro https://github.com/fsharp/fslang-suggestions/issues/1354 lists one API member in the BCL (System.Numerics.BigInteger.DivRem), I had queried the BCL using reflection but don't find the script I used to do so.
We should actually scan nuget.org, but that's a whole ordeal.
Even if not for C#, named tuples come naturally from partial application of constructors, methods, and DU cases. They preserve important contextual information just like how partial application of functions should preserve parameter names.
There are 59 methods (230 method overrides in total) returning ValueTuples as result in the BCL.
| Index | Method name | Overrides count |
|---|---|---|
| 1 | System.TupleExtensions.ToValueTuple | 21 |
| 2 | System.Runtime.Intrinsics.Arm.AdvSimd.LoadAndInsertScalar | 21 |
| 3 | System.Math.DivRem | 10 |
| 4 | System.Runtime.Intrinsics.Arm.Sve.Load4xVectorAndUnzip | 10 |
| 5 | System.Runtime.Intrinsics.Arm.Sve.Load3xVectorAndUnzip | 10 |
| 6 | System.Runtime.Intrinsics.Arm.Sve.Load2xVectorAndUnzip | 10 |
| 7 | System.ValueTuple.Create | 9 |
| 8 | System.Runtime.Intrinsics.Arm.AdvSimd.LoadAndReplicateToVector64x3 | 7 |
| 9 | System.Runtime.Intrinsics.Vector512.Widen | 7 |
| 10 | System.Runtime.Intrinsics.Vector64.Widen | 7 |
| 11 | System.Runtime.Intrinsics.Arm.AdvSimd.LoadAndReplicateToVector64x2 | 7 |
| 12 | System.Runtime.Intrinsics.Arm.AdvSimd.LoadAndReplicateToVector64x4 | 7 |
| 13 | System.Runtime.Intrinsics.Arm.AdvSimd.Load2xVector64AndUnzip | 7 |
| 14 | System.Runtime.Intrinsics.Arm.AdvSimd.Load4xVector64AndUnzip | 7 |
| 15 | System.Runtime.Intrinsics.Arm.AdvSimd.Load2xVector64 | 7 |
| 16 | System.Runtime.Intrinsics.Arm.AdvSimd.Load3xVector64 | 7 |
| 17 | System.Runtime.Intrinsics.Arm.AdvSimd.Load4xVector64 | 7 |
| 18 | System.Runtime.Intrinsics.Vector128.Widen | 7 |
| 19 | System.Runtime.Intrinsics.Arm.AdvSimd.Load3xVector64AndUnzip | 7 |
| 20 | System.Runtime.Intrinsics.Vector256.Widen | 7 |
| 21 | System.Runtime.Intrinsics.X86.X86Base.DivRem | 4 |
| 22 | System.Runtime.Intrinsics.Vector256.SinCos | 2 |
| 23 | System.Numerics.Vector.SinCos | 2 |
| 24 | System.Runtime.Intrinsics.Vector128.SinCos | 2 |
| 25 | System.Numerics.BigInteger.DivRem | 2 |
| 26 | System.Runtime.Intrinsics.Vector64.SinCos | 2 |
| 27 | System.Runtime.Intrinsics.Vector512.SinCos | 2 |
| 28 | System.Runtime.DependentHandle.get_TargetAndDependent | 1 |
| 29 | System.Runtime.Intrinsics.X86.X86Base.CpuId | 1 |
| 30 | System.Runtime.InteropServices.NFloat.SinCos | 1 |
| 31 | System.Numerics.ITrigonometricFunctions`1.SinCosPi | 1 |
| 32 | System.Numerics.IBinaryInteger`1.DivRem | 1 |
| 33 | System.Runtime.InteropServices.NFloat.SinCosPi | 1 |
| 34 | System.Math.SinCos | 1 |
| 35 | System.MathF.SinCos | 1 |
| 36 | System.Byte.DivRem | 1 |
| 37 | System.Double.SinCos | 1 |
| 38 | System.Double.SinCosPi | 1 |
| 39 | System.Half.SinCos | 1 |
| 40 | System.Half.SinCosPi | 1 |
| 41 | System.Int16.DivRem | 1 |
| 42 | System.Int32.DivRem | 1 |
| 43 | System.Int64.DivRem | 1 |
| 44 | System.Int128.DivRem | 1 |
| 45 | System.Numerics.ITrigonometricFunctions`1.SinCos | 1 |
| 46 | System.IntPtr.DivRem | 1 |
| 47 | System.SByte.DivRem | 1 |
| 48 | System.Single.SinCos | 1 |
| 49 | System.Single.SinCosPi | 1 |
| 50 | System.UInt16.DivRem | 1 |
| 51 | System.UInt32.DivRem | 1 |
| 52 | System.UInt64.DivRem | 1 |
| 53 | System.UInt128.DivRem | 1 |
| 54 | System.UIntPtr.DivRem | 1 |
| 55 | System.Numerics.Vector2.SinCos | 1 |
| 56 | System.Numerics.Vector3.SinCos | 1 |
| 57 | System.Numerics.Vector4.SinCos | 1 |
| 58 | System.Range.GetOffsetAndLength | 1 |
| 59 | System.Console.GetCursorPosition | 1 |
hot from HN: https://news.ycombinator.com/item?id=44843571
I do not understand how they could develop a language inspired by OCaml but not bring over labeled function arguments. A real L when it comes to ergonomics. And they just have no plans to ever fix this??
@Happypig375 reflection scenarios (highly relevant in boilerplate-free serialization)
Serialization via reflection is an unfortunate thing that used to happen in dotnet. We shouldn't let it stick around in F#.
@smoothdeveloper hot from HN: https://news.ycombinator.com/item?id=44843571
That issue is https://github.com/fsharp/fslang-suggestions/issues/961#issuecomment-767530376 I think someone should open a new issue copying and pasting that comment. I could do it: it's not something that I would use, but it makes sense to me to have more consistency between methods and functions so I would support it.
@charlesroddie But people do use it and reflection was an explicit design goal of anonymous types.
Also, with named tuple fields, there is no requirement to give all fields names - some can have names, some can have no name. Records, anonymous or otherwise, don't allow that.
The important distinction between tuples and records is that tuple fields have a natural order and record fields don't. This semantic difference shouldn't be conflated by having anonymous records be exposed as tuples.
That issue is #961 (comment) I think someone should open a new issue copying and pasting that comment. I could do it: it's not something that I would use, but it makes sense to me to have more consistency between methods and functions so I would support it.
Done in https://github.com/fsharp/fslang-suggestions/issues/1436
As I am revisiting the suggestion, I decided to apply the "core-functional" label to signal this having a big impact on most of today's processing of function type inference (currying, partial application, passing in lambdas with arguments and related IDE services). I want that to signal this as a bigger change in how things work and not just mere syntax sugar.
That made me realize that the existing way for signaling importance of meaning for certain elements/arguments is missing in this suggestion - using types to signal meaning.
In the specific example here, one could as well represent a dx argument by a delta type wrapper, and use e.g. units of measure to separate the x and y dimensions.