dotnet / dotnet/fsharp

`unit` elimination logic can lead to unutterable & unimplementable signatures

Open
#17,611 1 comment 0 reactions 0 assignees View on GitHub
Area-Compiler-Checking Impact-Low
Dominant language
F#
Stars
4.3k
Forks
876
Avg merge
4d 22h
Merged PRs (30d)
144

Description

(This is not new, is low-impact, is likely a known thing, and likely cannot be fixed, but I'm recording it anyway.)

The way `unit` elimination works in the compiler means that `(())` is sometimes equivalent to `()` and sometimes not (see, e.g., #16254).

Sometimes, both `(())` and `()` may compile to `void` or the absence of a parameter; other times, both may compile to `Microsoft.FSharp.Core.Unit`; other times still, `(())` may compile to `Microsoft.FSharp.Core.Unit` and `()` to the absence of a parameter.

An interesting corollary to this is that it is possible to define an overloaded method each of whose overloads [compiles differently](https://sharplab.io/#v2:DYLgZgzgPgLgngBwKYAIAqKAUBKFBeAWACgVSUBbJcgIyQCcUB9AOgFktc8OyB6HlBAFdqwAJYBjFADcA9qIAmKVjhQBvFAF9iZClVoMW7TDk7c+A4WMmyFSzAFUAdqJhMAhnQDmARlzqNQA) while having an identical F# type signature:

```fsharp
type T () =
member _.M () = () // public void M() { }
member _.M (()) = () // public void M(Unit _arg1) { }
```

It is thus impossible to specify a signature for this type and its method overloads that will compile — both overloads have the same F# signature:

```fsi
member T.M : unit -> unit
```

That means that it is also impossible to declare such overloads on an interface or abstract class in F#. This is another way of saying that it's impossible to write an F# signature describing the C# signature `void M(Unit _arg1)`.

None of these compile:

```fsharp
type U =
abstract M : unit -> unit
abstract M : unit -> unit
```

```fsharp
type U =
abstract M : unit -> unit
abstract M : (unit) -> unit
```

```fsharp
type U =
abstract M : unit -> unit
abstract M : _arg1:unit -> unit
```

It is possible to define such an interface in C#, however:

```fsharp
public interface U
{
void M();
void M(Microsoft.FSharp.Core.Unit _arg1);
}
```

Such a C# interface can then be implemented from F# like:

```fsharp
type T () =
interface U with
member _.M () = ()
member _.M (()) = ()
```

On the other hand, it's also possible to define an interface in C# like the following:

```fsharp
public interface U
{
void M(Microsoft.FSharp.Core.Unit _arg1);
Microsoft.FSharp.Core.Unit M();
}
```

This interface is impossible to implement in F#:

![image](https://github.com/user-attachments/assets/6970162a-42e4-49e3-89bd-4cf350652adb)

What does this all amount to? Not much, since changing the `unit` elimination logic or using parentheses for differentiation (`member M : unit -> unit` = `void M()`, `member M : (unit) -> unit` = `void M(Unit _arg1)`) as is done for tuples would be backwards-incompatible.

Too bad .NET didn't just use `unit` instead of `void` from day 1 🙂

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.