Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Nested field update syntax for records #379

Closed
baronfel opened this issue Oct 20, 2016 · 6 comments
Closed

Nested field update syntax for records #379

baronfel opened this issue Oct 20, 2016 · 6 comments

Comments

@baronfel
Copy link
Contributor

baronfel commented Oct 20, 2016

Updated to cover only the suggestion that

let a = { a with B.C = c }

be equivalent to

let a = { a with B = { a.B with C = c }}}

and corresponding for anonymous records and further nesting.


When working with complex state structures, functional programming is, without a great deal of additional tooling (tooling that does not satisfactorily exist in F#), highly inconvenient, verbose, and even error-prone. These problems are so pronounced that many cite them as a reason to label functional programming itself as inapplicable to the domains within which complex state structures are inherent!

As functional programmers, we believe otherwise, and many even believe that it is in the domain of dealing with complex state artifacts that functional programming is especially beneficial and necessary! However, with such weak syntactic constructs such as these -

    let a = { a with B = { a.B with C = c }}}

we win ourselves no favors. And even in the narrow context of such a small problem, simple and promising solutions abound. For example, why doesn't this syntax have a semantics assigned to it in F#? -

    let a = { a with B.C = c }

Or better, given that we know at compile-time that there is a means of constructing A and B, why we can't assign a semantics to this -

    let a = a.B.C $= c

and generate all the backing code for such a pure functional update automatically? And why restrict such nice syntax to only record fields? Why not let it be applicable to all members who support the concept of readability and updatability (EG - lensability) such as with here -

type Entity with
    member gui.Enabled = gui?Enabled : bool
    static member setEnabled (value : bool) (gui : Entity) = gui?Enabled <- value

so that we can finally utter -

    let entity = entity.Enabled $= anotherEntity.Enabled

In other words, why not give the compiler some way of recognizing 'lensability' implicitly for simple cases (and perhaps explicitly with simple declarations in more refined cases such as with Map values), ultimately enabling good syntax to take advantage of it all? And yes, there have been library solutions to the problem of lensing proposed in F#. However, in experience I have found the library solution to be embarrassingly inadequate. This is due to the fact that F# does not provide out-of-the-box (nor shall it in the reasonably near-future) the language constructs necessary to build a sufficiently expressive lens library... and not even for the straight-forward use cases I outlined above! And, even if such language constructs were available, I still doubt that a form of expression as understandable and succinct as the syntax I proposed could be achieved.

For that reason, I did and still do conclude that it is necessary to build lensing as a first class construct in the language. This is so that instead of attempting to use kindedness to express lensing, the simpler (and admittedly more muscled) approaches of syntactic expansion / IL generation may be used. Thus this user suggestion.

(On a side note, perhaps if we had a sophisticated and general syntactic macro system in F# like I suggested here - archive/suggestion-5674940-implement-syntactic-macros, the solution could again be proposed as a library. However, given that such a feature whose design and implementation is daunting and not yet to be even approved in principle, this too is a non-starter for an urgently-needed construct.)
To expand on the design proposal further, et's look at the subtle distinction I've made in the words I've used above. Specifically, we notice a distinction between -
Lensing - viewing or updating a value contained by another functional value
Lenses - a general mechanism by which lensing is achieved
We've already seen the syntax I propose for lensing where the lens itself is contextually implied -

let a = a.B.C $= c // here a new value of a is constructed and bound to a shadowing binding of a.`

Now say we want to pull out the lens itself that it might be passed around for lensing later. Obviously, we will need a syntax for that as well. However, the desired form taken by that syntax is more nebulous, so here are a few options -

let abcLens = lens <@ A.B.C @> // here's a syntax that utilizes code quotations: verbose but maybe easier to implement in the compiler?`
let abcLens = lensOf A.B.C OR lensOf<A.B.C> // here's lens with a new keyword lensOf: succinct, but not perfect backward compatibility
let abcLens = {$ A.B.C $} // ugly, but succinct and probably backward compatible (but maybe not as I've yet to study F#'s AST in-depth).

...and there are presumably many more possible forms. After all, for literally ALL the syntax I've proposed in this thread, I've merely been riffing!
Additionally, there would need to be a syntax for allowing user-defined induction of other things into the category of 'lensability'.
For example, say we have a type whose members are described at run-time like so -

type A with
    static member getB (a : A) = a?B : bool
    static member setB (b : bool) (a : A) = a?B <- value

We should be able to decorate the view and update functions like so -

type A with
    [<Viewable "B">] static member getB (a : A) = a?B : bool
    [<Updatable "B">] static member setB (b : bool) (a : A) : A = a?B <- value

In conclusion, F# needs first-class support for lensing / lenses because it current set of language features is inadequate to provide a library solution, and because lensing / lenses is such a fundamental property of functional programming in stateful systems. While my design proposal certainly will have some flaws that need to be smoothed over, I think it is a good enough start to get something in the works!

Original UserVoice Submission
Archived Uservoice Comments

@dsyme dsyme removed the open label Oct 29, 2016
@dsyme dsyme changed the title Implement first-class lensing / lenses in F# Support first-class lensing / lenses Oct 29, 2016
@colinbull
Copy link

colinbull commented Nov 7, 2016

Another possible alternative to this is to enable type providers to receive declared types see here. In theory then it might be possible to enable something like the following.

 type MyRecord = {
      Id : string
      Name : string
  }

  type MyRecordOptics = OpticProvider<MyRecord>

then with an accompanying Lens library along side the provider you could have the following

   Optic.get MyRecordOptics.id_ my instance

Or something similar. Of course the effectiveness of this option depends what limitations are in place for the type provider RFC.

@TIHan
Copy link

TIHan commented Nov 7, 2016

I understand the reasoning behind this:

let a = { a with B.C = c }

It's simple syntactic sugar to avoid doing this a lot:

let a = { a with B = { a.B with C = c }}}

It makes sense to me.

However, I think introducing, $=, would complicate the language more; especially when you want it to work with non-record fields. Whatever route you take you are just adding more ways to do the same thing in the language. We should be careful here.

I'm happy without using something like this because I would just create separate functions that update the specific parts then call them together. That way it is even more clear of what's going on.

@7sharp9
Copy link
Member

7sharp9 commented Apr 22, 2021

Len's are also currently generated by Myriad in one of the built-in plugins: https://moiraesoftware.github.io/myriad//how-tos/lenses.html

@dsyme
Copy link
Collaborator

dsyme commented Jun 9, 2022

I'm closing this for the reasons described well here: #379 (comment)

@dsyme
Copy link
Collaborator

dsyme commented Mar 1, 2023

I've discussed this with @vzarytovskii and will rename this suggestion to follow the limited suggestion here: #379 (comment) and mark it approved in principle.

The suggestion does however leave a number of "unresolved questions" which people might want to work on

  • It increases the way records are "different" to other constructs in the language - and so can make the cost of moving away from records in modelling higher.
  • For example, using immutable modelling constructs like immutable lists and tuples don't "play well" with this kind of nested update, ideally there would be a way these played nicely with this new syntax
  • Likewise, using immutable classes doesn't play well, related to Allow the record pattern to be implemented explicitly #164

Ideally these would play well. But still the addition is a good one for the case where it's "records all the way down" and doesn't make the problems above fundamentally worse as they already apply to the "with" construct anyway.

@dsyme dsyme reopened this Mar 1, 2023
@dsyme dsyme changed the title Support first-class lensing / lenses Nested field update syntax for records Mar 1, 2023
@vzarytovskii
Copy link

Implemented in
dotnet/fsharp#14821

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants