fsharp / fsharp/fslang-suggestions

Property references

Open
#1,409 10 comments 1 reaction 0 assignees View on GitHub
area: object-programming area: structs-byrefs-and-span probably not
Dominant language
No language data
Stars
373
Forks
21
PR merge metrics
No merged PRs in 30d

Description

**I propose we** have the possibility to use the `&` operator to get the "reference" to a normal property (or indexer), to be able to bind a property to a name in the same way one can already refer to a variable through `byref`.

This involves the following additions:
* `propref<'T>` (`propref<'T, ByRefKinds.InOut>` ‒ a reference to a mutable property of type `'T`.
* `inpropref<'T>` (`propref<'T, ByRefKinds.In>` ‒ a reference to a readable property of type `'T`.
* `outpropref<'T>` (`propref<'T, ByRefKinds.Out>` ‒ a reference to a writeable property of type `'T`.

These types relate in the same ways as the existing `byref`/`inref`/`outref`. Moreover, `byref<'T>` is assignable to `propref<'T>`, `inref<'T>` to `inpropref<'T>`, and `outref<'T>` to `outpropref<'T>`.

The actual `propref<'T, 'Kind>` type could be implemented through a byref-like struct that contains at least these conceptual fields:
* `holder : TypedReference` that identifies the object holding the property.
* `index : TypedReference` ‒ a reference to the index (or indexes).
* `accessor`, an object of an implementation-defined type that exposes the property on the type of `holder`. This does not depend on a concrete instance of the type and thus could be cached and shared during compilation among all references to the same property on that type. A possible implementation of `accessor` could be broken down into:
* `getter` ‒ an optimized function that retrieves the value of the property from `holder` and `index`.
* `setter` ‒ likewise for setting the property.

The conversion from plain `byref` just stores it in `holder`, leaves `index` zero-initialized, and uses trivial implementations for `getter` and `setter`. The F# library might also offer functions for safe manual construction of this type from arbitrary functions. As an optimization, it is possible to merge `holder` and `index` and store just a single reference to a byref-like struct that holds all the relevant references (the object and all indexes), which could be just the object itself if there are no indexes.

An expression of the form &object.Property or &object[index] is interpreted in this way:
* If `Property` or the indexer returns `byref` or `propref` already, that value is used as usual.
* Otherwise (in the situation that would originally raise a compiler error), a `propref` is constructed:
* If `Property` is `static`, `holder` is zero-initialized.
* If `object` is of a reference type, it is stored in a new variable (to ensure its storage location is not mutated) and the reference to that is used as `holder`.
* If `object` is of a value type and can have its address taken (is a variable, field, a `byref` value etc.), the reference to it is stored in `holder`. Otherwise `&` is not permitted (this disallows expressions where assignment would not make sense, e.g. assigning to a property of a non-reference function call result).
* If an index or indexes are used, they are stored in a sufficient variable and referenced by `index`.
* `getter` and `setter` are initialized accordingly (for example as function pointers to compiler-generated functions that dereference `holder` and access the property).

Reading from a `propref` value results in a call to `getter`, and mutating it in a call to `setter`, i.e. it behaves the same as a normal `byref` (including `&` itself, so there is no `byref>`). As a consequence, a code like this would compile and work as expected:
```fs
let incr (x: propref) = x <- x + 1

do
let list = System.Collections.Generic.List()
list.Add(0)
incr &list[0]
```

In case it turns out `TypedReference` cannot be used for this purpose, an alternate definition could be `propref<'T, 'Context, 'Kind>`, where `holder` and `index` are combined directly into a `context` generated by the compiler. This however likely requires `allows ref struct` (at least implicitly) for situations where `'Context` is supposed to be inferred.

**The existing way of approaching this problem in F# is** not generalizable for all situations and types (structs and byref-like structs). For records, [Lens](https://fsprojects.github.io/FSharpPlus/lens.html) could be used, and for other cases function pairs could be used, but byref-like types do not offer any syntax that would make constructing them with this purpose easy to use or optimize.

## Pros and Cons

**The advantages of making this adjustment to F# are**:
* Uniformity with normal references, so that existing code that works with `byref`s can be easily extended to work with `propref`s under most of the same assumptions (unless pointers are involved). This includes extensions and operators.
* Having a compiler-understood "generalized storage location" type that can be in a lot of cases inlined and optimized away, while offering the flexibility of treating it as a referenvalue in other contexts. With functions to construct it from arbitrary code, this would result in a usable "abstract reference" type, similarly to how events are exposed in the language.
* The possibility to define a truly universal replacement for `incr` and similar functions that need to operate on "variables", such as compound assignment and other operations.

**The disadvantages of making this adjustment to F# are** introducing a new special type with potentially complicated semantics or interop (but dedicated `Set` and `Get` methods could be used for exposing it to C#).

An alternative mechanism for expressing the same kind of reference could be extending the use of the normal `byref` type to properties but restricting it only to `inline` contexts, so that the compiler can track all accesses to the reference and translate them to accessing the property. This could work without introducing new special types, but risks introducing a split in how references work.

Another option is to make `&` work only for arguments by translating it to a temporary variable reference that is initialized using the value of the property, and the property is set to the new value after the call, which is how VB.NET does it. That would also work in the important cases but has different semantics (the value is only read and stored once).

## Extra information

**Estimated cost (XS, S, M, L, XL, XXL):** M

**Related suggestions:** #867 (& comments)

## Affidavit (please submit!)

* [x] This is not a question (e.g. like one you might ask on [StackOverflow](http://stackoverflow.com)) 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](https://github.com/dotnet/fsharp)
* [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](http://github.com/fsharp/fslang-suggestions/issues) and believe this is not a duplicate

* [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.

Contributor guide

No contributing guide indexed for this repository

Research direction

The issue names no implementation files, tests, or specific entry points. Start by reviewing the F# compiler and tooling repository and the competing designs described here. Done requires an agreed language design followed by implementation and tests for property references and their semantics.

Written by the indexing model from the issue text.

Assessment

Domain
compilers
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
25/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.