Open dsyme opened 1 year ago
I suppose it could technically subsume yield!
in computation expressions as well.
let xs = [3..10]
let ys = [1; 2; ...xs]
I suppose it could technically subsume yield! in computation expressions as well.
Yes, it could. Which then raises the question of return!
too.
A 'rest' operator for list-like patterns would be very helpful - right now list is privileged in terms of syntax because it can easily be deconstructed into parts of a certain length and 'the rest' - other list-like types (those that are indexable) would be more usable if they has a way to achieve the same result.
A 'rest' operator for list-like patterns would be very helpful - right now list is privileged in terms of syntax because it can easily be deconstructed into parts of a certain length and 'the rest' - other list-like types (those that are indexable) would be more usable if they has a way to achieve the same result.
Yes also allows a "match at end" syntax for arrays and lists.
RIP tooling logic for handling dots 😄
I'd like to see DUs added to suggestion as well on type level, so one can use
type One =
| X of int
| Y of string
type Two =
| ...One
| Z of string
The benefits are less prominent on instance level, but also usable
type One =
| X of int
| Y of string
type Two =
| X of int
| Y of string
| Z of string
let x: One = X 1
let y: Two = ...x
@dsyme, would you mind expanding a bit on the compile time and possibly runtime semantics you envision for the concrete implementation of this on CLR?
Also, would you mind sharing about the background / prior art, that is inspiring this feature?
Spreading record types into other record types seems tempting, but note this would not give rise to subtyping. But then can you spread object types into record types? Seems like a step too far?
Not having support for spreading at type level seems it would force redundant work on type definition, when the author desires to always map all the properties; design-wise, it would not be encouraged, but for the use cases of full mapping being ever desired, rather than selective / explicit one. I think it improves semantics of the code to support it on type definition.
@Lanayx example for DU is also good to consider, if records is going to support this.
Spreading object types into records or even a DU case, seems appealing to me, but not needed for first iteration in landing this feature.
This feature would be useful for https://github.com/isaksky/FsSqlDom which maps MSSQL Dom to DU, where bulk of declarations is done in code generation, it could be done with spreadability of object into DU cases.
I'm also seeing use case where it would be possible to explicitly omit some of the things, #762; I think it should prompt question about ...
being the best notation; I've considered += { a without PropName1; PropName2 }
it may lead to a cascading set or requirements for using spread operators and related structural manipulations in generic code.
Are you considering some of the SRTP constructs would ideally, need to support a notation describing "spreadability semantics" between any two types or, type to set of types?
Are values of a SRTP-constrained type considered spreadable?
It seems to me, this would be powerful to make selective spread operators, which monomorphizes at compile time.
Are values of a class-constrained generic type considered spreadable?
Given the runtime cost, assuming this would be runtime reflection based, I'd see this as least priority than the SRTP approach (now that SRTP is embraced better, due to IWSAM giving "run for the money" :D), people can use automapper stuff, it seems it can be library based, specific to semantics of CLR, not specific to F#, less added value than SRTP semantics.
some subtle corner cases
Sensing inheritance is going to be headache inducing, I believe for a first approach (in order to get the feature out, for the already known effective use cases), it would be fine to bail out, if there is any kind of clash / ambiguity surfacing from multiple or conflicting inheritance scenarios.
The main issue is that it is still work to verify those cases, and report errors that make sense, but this would allow to defer decisions in this area, while seeing the real world usages and deriving feedback from it for, later, opening those use cases that would be guarded.
With this feature, the clone record syntax { a with }
will be improved by spread record syntax id<'targetType> { ...a }
.
The record spread syntax is similar to javascript, so "record field value shorthands" syntax may be a good feature as well.
type A = {
X: int
Y: int
}
type B = {
Z: int
}
type C = {
X: int
Y: int
Z: int
Extra: string
}
let a = { X = 1; Y = 2 }
let b = { Z = 3 }
let Extra = "four" //*
let c = id<C>{ ...a; ...b; Extra } // <- Extra
@brianrourkeboll I agree with you.+1 https://github.com/fsharp/fslang-suggestions/issues/973#issue-796786555
Interesting, I think it might improve developer experience a lot.
Would it be possible to spread anon records into a normal record?
type A = { X: int; Y: int; Z: int }
let x = {| X = 1; Y = 2 |}
let y = {| Z = 3 |}
let a = {...x; ...y }
How about combining anon records?
let x = {| X = 1; Y = 2 |}
let y = {| Z = 3 |}
let z = {| ...x; ...y |}
What would happen in the below scenario?
type A = { X: int; Y: int }
type B = { X: int }
let a : A = { X = 1; Y = 1 }
let b : B = { ...a }
Ideally all of these examples just work :-)
Would it be possible to spread anon records into a normal record?
Yes, that's the intention of the design.
What would happen in the below scenario?
Yes, there could be excess properties. The new object "b" would not have property Y. My understanding is this is different to TypeScript, where all properties are copied across as the JS semantics is used at runtime (TS typing doesn't affect runtime behaviour).
How about combining anon records?
Yes, that would be in scope
@smoothdeveloper
Not having support for spreading at type level seems it would force redundant work on type definition, when the author desires to always map all the properties; design-wise, it would not be encouraged, but for the use cases of full mapping being ever desired, rather than selective / explicit one. I think it improves semantics of the code to support it on type definition.
I'm a bit ambivalent about support in type definitions. The more "programmatic" type definitions become, the harder they are to understand - to work out what properties are in a type you have to "run the type level program in your head". This is sort of OK for expressions - which are "real" programs and have debugging, printing etc. support - but for type level programs it's all compile-time execution. It's basically similar to many of my concerns about type-level programming I've expressed in the past, e.g. #243
Still we could do expressions first and leave types to later.
@Lanayx Regarding https://github.com/fsharp/fslang-suggestions/issues/1253#issuecomment-1464312424 - interesting. I can see there is some kind of potential here, perhaps as a third wave.
The more "programmatic" type definitions become, the harder they are to understand
For what is worth. I agree.
Maybe tooling could help here. Implement the expansion of ...A
at design time in a tooling action.
type A = {
X: int
Y: int
}
type B = {
...A;
Extra: string
}
I've tried chatgpt to validate this proposal and it totally supports it! I has also found new cases! Attaching the responses
First part:
Title: Spread Operator in F#
Author: ChatGPT
Status: Draft
Date: 2023-03-18
Abstract:
This document proposes the addition of a spread operator in the F# programming language. The spread operator is designed to simplify the syntax and improve readability when working with lists, arrays, and other collection types. It allows for the unpacking of elements in a collection to be used as individual arguments in a function call or to merge collections.
Introduction
The F# programming language, as a functional programming language, emphasizes immutability and the use of collections for data manipulation. However, working with collections can sometimes involve verbose syntax, which can reduce readability and maintainability. This document proposes the addition of a spread operator, similar to the one found in languages like JavaScript and Python, to simplify the handling of collections in F#.
Specification
The spread operator is proposed to be implemented using the '...' syntax. The operator should work with the following collection types:
Lists
Arrays
Sequences
2.1. Spreading in function calls
The spread operator can be used to unpack elements from a collection and use them as individual arguments in a function call. This simplifies function calls that require multiple arguments from a single collection.
Example:
fsharp
let addThreeNumbers a b c = a + b + c
let numbers = [1; 2; 3]
let result = addThreeNumbers ...numbers
2.2. Merging collections
The spread operator can be used to merge multiple collections into a single one.
Example:
fsharp
let list1 = [1; 2; 3]
let list2 = [4; 5; 6]
let mergedList = [...list1; ...list2] // [1; 2; 3; 4; 5; 6]
Backward Compatibility
The proposed spread operator does not affect any existing F# features or syntax, and therefore should not introduce backward compatibility issues.
Implementation
The spread operator should be implemented as part of the F# compiler, modifying the existing syntax parsing and type checking to support the new feature.
Acknowledgments
The spread operator is inspired by similar features found in languages such as JavaScript, Python, and Ruby, as well as the numerous proposals and discussions in the F# community.
References
F# Language Specification: https://fsharp.org/specs/language-spec/
ECMAScript Spread Operator: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
Python PEP 448: https://www.python.org/dev/peps/pep-0448/
Second part:
Title: Spread Operator in F# for Records and Discriminated Unions
Author: ChatGPT
Status: Draft
Date: 2023-03-18
Extension to: Spread Operator in F#
Abstract:
This document extends the previous RFC on the spread operator in F# to include support for records and discriminated unions. The spread operator simplifies the syntax and improves readability when working with F# data structures, allowing for the unpacking and merging of fields or cases.
Introduction
Building on the previous RFC on the spread operator in F#, this extension proposes the addition of spread operator support for F# records and discriminated unions. The spread operator can simplify the creation and manipulation of these data structures, improving readability and maintainability.
Specification
The spread operator should be extended to work with the following F# data structures:
Records
Discriminated Unions
2.1. Spreading in records
The spread operator can be used to unpack fields from an existing record and create a new record using the values of those fields. This simplifies the creation of new records based on existing ones.
Example:
fsharp
type Person = { FirstName: string; LastName: string; Age: int }
let john = { FirstName = "John"; LastName = "Doe"; Age = 30 }
let jane = { ...john; FirstName = "Jane" } // { FirstName = "Jane"; LastName = "Doe"; Age = 30 }
2.2. Spreading in discriminated unions
The spread operator can be used to unpack cases from a discriminated union and create a new instance of the union using the values of those cases. This simplifies the creation of new instances based on existing ones.
Example:
fsharp
type Shape =
| Circle of radius: float
| Rectangle of width: float * height: float
let circle = Circle 5.0
let biggerCircle = Circle ...(radius + 2.0) // Circle 7.0
let rectangle = Rectangle (3.0, 4.0)
let tallerRectangle = Rectangle ...(width, height + 2.0) // Rectangle (3.0, 6.0)
Backward Compatibility
The proposed spread operator extension does not affect any existing F# features or syntax and should not introduce backward compatibility issues.
Implementation
The spread operator extension should be implemented as part of the F# compiler, modifying the existing syntax parsing and type checking to support the new feature for records and discriminated unions.
Acknowledgments
The spread operator is inspired by similar features found in languages such as JavaScript, Python, and Ruby, as well as the numerous proposals and discussions in the F# community.
References
F# Language Specification: https://fsharp.org/specs/language-spec/
ECMAScript Spread Operator: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
Python PEP 448: https://www.python.org/dev/peps/pep-0448/
Third part
Title: Type-level Spread Operator in F# for Records and Discriminated Unions
Author: ChatGPT
Status: Draft
Date: 2023-03-18
Extension to: Spread Operator in F#
Abstract:
This document extends the previous RFCs on the spread operator in F# to include type-level support for records and discriminated unions. The type-level spread operator simplifies the process of composing and extending F# data structures by merging the types of existing records or discriminated unions.
Introduction
The previous RFCs introduced the spread operator in F# for lists, arrays, sequences, records, and discriminated unions at the value level. This extension proposes the addition of a type-level spread operator, which simplifies the composition and extension of F# data structures by merging the types of existing records or discriminated unions.
Specification
The type-level spread operator should be extended to work with the following F# data structures:
Records
Discriminated Unions
2.1. Type-level spreading in records
The type-level spread operator can be used to create a new record type by merging the fields from existing record types. This simplifies the composition of complex record types.
Example:
fsharp
type Name = { FirstName: string; LastName: string }
type Age = { Age: int }
type Person = { ...Name; ...Age }
let john: Person = { FirstName = "John"; LastName = "Doe"; Age = 30 }
2.2. Type-level spreading in discriminated unions
The type-level spread operator can be used to create a new discriminated union type by merging the cases from existing discriminated union types. This simplifies the composition of complex discriminated union types.
Example:
fsharp
type Shape2D =
| Circle of radius: float
| Rectangle of width: float * height: float
type Shape3D =
| Sphere of radius: float
| Cube of side: float
type Shape = ...Shape2D | ...Shape3D
let circle: Shape = Circle 5.0
let cube: Shape = Cube 3.0
Backward Compatibility
The proposed type-level spread operator extension does not affect any existing F# features or syntax and should not introduce backward compatibility issues.
Implementation
The type-level spread operator extension should be implemented as part of the F# compiler, modifying the existing syntax parsing and type checking to support the new feature for records and discriminated unions at the type level.
Acknowledgments
The type-level spread operator is inspired by similar features found in languages such as TypeScript, as well as the numerous proposals and discussions in the F# community.
References
F# Language Specification: https://fsharp.org/specs/language-spec/
TypeScript Spread Operator: https://www.typescriptlang.org/docs/handbook/2/objects.html#spread-types
Fourth part
Title: Spread Operator in F# for Combining Modules
Author: ChatGPT
Status: Draft
Date: 2023-03-18
Extension to: Spread Operator in F#
Abstract:
This document extends the previous RFCs on the spread operator in F# to include combining modules without drilling down to the underlying functions. The proposal introduces the spread operator to merge modules into a single module, simplifying the process of combining and organizing functions from different modules.
Introduction
The previous RFCs introduced the spread operator in F# for lists, arrays, sequences, records, discriminated unions, and their respective types. This extension proposes the addition of a spread operator for F# modules, which simplifies the process of combining modules without the need to specify individual functions.
Specification
The spread operator should be extended to work with the following F# language feature:
Modules
2.1. Spread operator for combining modules
The spread operator can be used to merge modules into a single module. This simplifies the process of combining functions from different modules and improves code organization.
Example:
fsharp
module MathOperations =
let add x y = x + y
let subtract x y = x - y
let multiply x y = x * y
module StringOperations =
let concat s1 s2 = s1 + s2
let length s = s.Length
module Util =
...MathOperations
...StringOperations
let result1 = Util.add 5 3
let result2 = Util.subtract 7 2
let result3 = Util.concat "Hello" ", F#"
In this example, the Util module merges the MathOperations and StringOperations modules using the spread operator. This makes it easy to combine modules without having to import specific functions from each module.
Backward Compatibility
The proposed spread operator extension for combining modules does not affect any existing F# features or syntax and should not introduce backward compatibility issues.
Implementation
The spread operator extension for combining modules should be implemented as part of the F# compiler, modifying the existing syntax parsing and type checking to support the new feature for merging modules.
Acknowledgments
The spread operator for combining modules is inspired by similar features found in languages such as JavaScript (ES6) and TypeScript, as well as the numerous proposals and discussions in the F# community.
References
F# Language Specification: https://fsharp.org/specs/language-spec/
JavaScript (ES6) Spread Operator: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax
TypeScript Spread Operator: https://www.typescriptlang.org/docs/handbook/2/objects.html#spread-types
Fifth
Title: Extending Spread Operator to Tuples in F#
Summary
This RFC proposes extending the spread operator to work with tuples in F#. This addition will allow users to merge tuples more easily and create new tuples by combining existing ones.
Motivation
Currently, combining tuples or extracting and merging their elements requires manual handling, which can be cumbersome and error-prone. By extending the spread operator to work with tuples, F# developers will be able to merge tuples more efficiently and write cleaner, more concise code.
Detailed Design
The spread operator will be extended to support tuples, allowing users to merge two or more tuples into a new tuple. The syntax for using the spread operator with tuples will be similar to the one used with records and discriminated unions.
Syntax
The spread operator can be used in tuple construction:
fsharp
let tuple1 = (1, "a")
let tuple2 = (2, "b")
let combinedTuple = (..tuple1, ..tuple2) // combinedTuple = (1, "a", 2, "b")
Nested Tuples
The spread operator can be used with nested tuples as well:
fsharp
let tuple1 = (1, (2, 3))
let tuple2 = (4, (5, 6))
let combinedTuple = (..tuple1, ..tuple2) // combinedTuple = (1, (2, 3), 4, (5, 6))
Restrictions
Using the spread operator with tuples of different types is allowed. However, the type of the resulting tuple will be determined by the combined types of the input tuples.
Implementation
The spread operator for tuples can be implemented as a compiler feature, expanding the spread syntax into tuple construction code.
Drawbacks
Additional complexity: Extending the spread operator to support tuples increases the complexity of the language and the compiler implementation.
Potential confusion: Users might get confused about the syntax and behavior of the spread operator for tuples, especially if they are not familiar with the spread operator in other contexts (e.g., records, discriminated unions).
Alternatives
One alternative is to continue using manual tuple handling and merging, without the spread operator. However, this approach is less ergonomic and can lead to more cumbersome and error-prone code.
Unresolved Questions
Should the spread operator handle optional elements in tuples?
Are there any performance implications or concerns when using the spread operator with large tuples?
Conclusion
By extending the spread operator to work with tuples, F# developers will benefit from more concise and expressive code when working with tuples. This addition will improve the ergonomics and simplicity of tuple manipulation in F#.
2.+. Spreading in tuples
The spread operator can be used to unpack elements from an existing tuple and create a new tuple using the values of those elements. This simplifies the creation of new tuples based on existing ones.
Example:
let john = "John","Doe",30
let jane = "Doctor",...john // "Doctor","John","Doe",30
Would a single let binding (= 1 name + 1 value) be considered spreadable as well? It would increase the overlap with other languages, i.e. the expected behavior. At the same, the chance of errors would probably increase.
let x = {| X = 1; Y = 2 |}
let y = "value for y"
let a = {...x; ...y }
// {| X = 1; Y = 2; y = "value for y" |}
let x = {| X = 1; Y = 2 |} let y = "value for y" let a = {...x; ...y } // {| X = 1; Y = 2; y = "value for y" |}
What language does that?, not Javascript is it?.
const a = {x, y}; // expands to: const a = {x: x, y: y};
Shorthand property names/shorthandProperty, example. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Object_initializer
@Lanayx A fascinating use of ChatGPT! However I'm not particularly inclined to include any of those above examples. They make sense in JS and Python but the F# and .NET type/argument/module system just doesn't unify in quite the same way to make these particularly feasible I think.
I'm going to mark this as approved in principle, though there would be many details to consider.
I suspect the biggest problematic area will be about any spreading involving providing of fulfilling object members (methods, properties, events etc.) as this is quite a large space, especially once extension members are considered. It could be that a phased approach would be needed, taking care not to rule out application in these areas.
We should also consider a negated spread since this suggestion subsumes with
, while we also have a suggestion for without
: https://github.com/fsharp/fslang-suggestions/issues/762.
type A = {
X: int
Y: int
}
type B = {
Y: int
Z: int
}
type C = { ...A; not ...B }
// type C = { X: int }
We should also consider a negated spread since this suggestion subsumes
with
, while we also have a suggestion forwithout
: https://github.com/fsharp/fslang-suggestions/issues/762.type A = { X: int Y: int } type B = { Y: int Z: int } type C = { ...A; not ...B } // type C = { X: int }
Yeah, without
should be part of it. Wondering what's the solution in JS/TS
TS provides Omit construct for type-level
without
It feels very inconsistent - have a language construct for extending types/records and have type-level construct for narrowing them.
For TS I think it is ok, since spread is only for instance level there and constructs are for type level. For F# we are planning to use spread everywhere, so we need to find another way
Correct me if I'm wrong, but using this along with anonymous records I still can't make a function that restricts parameters to records (of any type), and then returns an anonymous record, right?
let joinAndAddX (a: 'a) (b: 'b) =
{| ...a; ...b with X = "Hello" |}
RIP tooling logic for handling dots 😄
I feel that lol
This feature would be incredibly practical and will greatly reduce code duplication in may situation, especially when copuled with the also proposed without
(which may also be called omit to better align with FP terms) feature.
I have always missed this feature since I lernt of the ballerina languages 'type inclusion' concept.
While someone will be creating an RFC for this, specifically for spreading on record types, there will be a choice - to copy attributes together with fields definitions or not. I personally think that attributes should be copied as well.
I propose we consider the merits of a spread operator for F#
It would be used for
with
and this suggestion)Records
Anonymous records
Object expressions
Interface implementations
The existing way of approaching this problem in F# is explicit re-delegation.
Technical details?
Fundamental technical questions are
For example
M<T> : T -> T
, can that be used to implement a requirementM: int -> int
through type specialization? (TBD)Patterns?
Should a spread operator be allowed in patterns? e.g.
Initially there would I think be no need for this.
Types?
How about type syntax? e.g.
Spreading record types into other record types seems tempting, but note this would not give rise to subtyping. But then can you spread object types into record types? Seems like a step too far?
Collections?
Spread operators in other languages usually work with
Bat all of this is necessarily desirable for a spread operator in F#, though each should be considered and if they aren't supported good diagnostics should be given directing people to the right way to do things.
Pros and Cons
The advantages of making this adjustment to F# are conceptual efficiency, introduction of a littler structural matching
The disadvantages of making this adjustment to F# are
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.