fsharp / fsharp/fslang-suggestions

Let the explicit upcast operator `:>` use .NET generic variance

Open
#1,470 3 comments 5 reactions 0 assignees View on GitHub
Dominant language
No language data
Stars
373
Forks
21
PR merge metrics
No merged PRs in 30d

Description

**I propose we** let the explicit coercion operator `:>` perform conversions that .NET generic variance already makes safe, so that a widening the runtime treats as an identity is expressible in F#:

```fsharp
let strings: seq = Seq.ofList [ "a"; "b" ]
let objects = strings :> seq // today: error FS0193, Type constraint mismatch
```

`IEnumerable<'T>` is declared covariant (`out T`), so at run time a `seq` **is** a `seq`. F# does not use that fact anywhere.

The odd part is that the operator which can never fail is the one rejected, while the operator that implies a run-time check is accepted and works:

```fsharp
let ys = strings :> seq // error FS0193
let ys: seq = strings // error FS0001: The type 'obj' does not match the type 'string'
takesObjs strings // error FS0001, at the argument position

let ys = strings :?> seq // compiles, succeeds, returns the *same instance*
```

The same holds for contravariance: `IComparer :?> IComparer` compiles and hands back the identical object, while `:>` is rejected.

The narrowing direction is fine as it stands and this suggestion does not touch it — `seq :?> seq` is a genuine run-time check that correctly throws when the underlying object is not an `IEnumerable`.

**The existing way of approaching this problem in F# is** one of three workarounds, each worse than the conversion the CLR would do for nothing (measured on .NET 10, `obj.ReferenceEquals` taken after the conversion):

| form | cost | checking |
|---|---|---|
| `xs :?> seq` | O(1) — returns the same instance for `seq`, `string list` and `string[]` | run-time `castclass`, although it can never fail |
| `xs \|> Seq.map (fun x -> x :> obj)` | new lazy sequence: closure plus enumerator, work per item | static |
| `xs \|> Seq.cast` | new lazy sequence, unbox per item | none — `[ 1; 2; 3 ] \|> Seq.cast` compiles and throws `InvalidCastException` at run time |

So the idiomatic-looking answer (`Seq.map (fun x -> x :> _)`) is the one that allocates, `Seq.cast` silently gives up static checking, and the cheapest correct form (`:?>`) reads to every F# reader as an unchecked downcast — which is exactly what it is not.

## Pros and Cons

**The advantages of making this adjustment to F# are**

- A conversion that is free at run time becomes free in the source too, instead of costing an allocation and a pass over the sequence.
- `:>` stops being a worse-informed operator than `:?>` for the one case where the compiler could prove the conversion safe.
- It removes the pressure to reach for `Seq.cast`, which throws away static element-type checking entirely and fails only at run time.
- It matches what the runtime, C#, and VB all already do with these same types.

**The disadvantages of making this adjustment to F# are**

- F# keeps its subtyping relation deliberately explicit and minimal. Making the *subsumption* relation variance-aware could disturb type inference and overload resolution in ways that are hard to predict, which is why this suggestion is scoped to the explicitly written `:>` operator and deliberately leaves annotation-driven and argument-position conversions (the second and third failing lines above) alone.
- Variance rules bring their own sharp edges (they apply only to reference type arguments, so `seq :> seq` must keep failing) and a `:>` that succeeds for `string` but not for `int` needs a clear diagnostic.
- It is one more rule in a coercion story that is already not simple to teach.

## Extra information

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

**Related suggestions:**

- https://github.com/fsharp/fslang-suggestions/issues/1450 — defining variant type parameters in F# (the declaration site; this suggestion is about the use site)
- https://github.com/fsharp/fslang-suggestions/issues/1131 — `Func<>`/delegate implicit conversion
- https://github.com/fsharp/fslang-suggestions/issues/3 — implicit upcast of return values (closed)

Context: this came out of review discussion on https://github.com/dotnet/fsharp/pull/20399, where guidance on how to retype a sequence had no good answer to point at.

## 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](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

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.

Contributor guide

No contributing guide indexed for this repository

Research direction

Start by reviewing the explicit `:>` examples and the linked dotnet/fsharp PR #20399; the issue names no source files or tests. A complete contribution would need to establish the language-design decision and demonstrate the requested variance behavior without changing the stated narrowing cases.

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
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.