Skip to content

Proposal: Add invokeable?(...) as short hand for invokeable?.Invoke(...) and add support for any "invoke-able" type #3257

Description

@nietras

Proposal: Add invokeable?(...) as short hand for invokeable?.Invoke(...) and add support for any "invoke-able" type

It is proposed to extend and generalize the delegate(...) invocation operator to reduce the amount of repetitive code and support new scenarios for succinct invocation of types following a specific pattern. Such a type is referred to as an "invoke-able" type in this proposal and is any type having any method called Invoke or types made invoke-able via extension methods. Hence, C# should use structural matching for the invokeable(...) and invokeable?(...) invocation operator and not match on whether a type is a delegate.

There are two parts to the proposal:

  • Allow invocation to support the null-propagation operator i.e. action?()
  • Extend invocation () to structurally match any type with one or more Invoke methods available incl. generic methods and extension methods, perhaps even static types.

See Member access operators for existing operators.

In this proposal examples are in C# 8 with nullable enabled.

Motivation

A common and easy pattern to use for logging is to simply use/inject a delegate Action<string>. Allowing this to be null with the meaning that logging is disabled. This is simple to implement and has low coupling. But is also a bit tedious currently in C#:

public void M(Action<string>? log) => log?.Invoke("test");

compared to how this is if this couldn't be null:

public void M(Action<string> log) => log("test");

Proposed Solution

Hence, the first part proposes changing this to allow the null-propagation operator for invocation. It already exists for methods ?. and indexers ?[] and it seems natural to extend that so the following is allowed and is simply a short hand for ?.Invoke(...). With null-propagation being exactly the same. delegate() just emits delegate.Invoke() in IL so this does not change generated code.

public void M(Action<string>? log) => log?("test"); // log?.Invoke()

Other examples:

public int? M(Func<int>? get) => get?(); // get?.Invoke()
public string? M(Func<string>? get) => get?(); // get?.Invoke()

Similar Patterns

Below are examples of existing null-propagation for comparison.

public int? M(int[][][] values) => values?[2]?[1]?[0];
public int? M(Foo? foo) => foo?.Bar?.M();

Extended Solution

Since, the invocation operator () for delegates is simply a short hand for .Invoke(), it is also proposed to extend the invocation operator () for any scenario where a type or instance has any method called Invoke available. Whether it be a member method or an extension method. This helps a lot in patterns that define first class invoke-able types. Hence, the invocation operator should be available as soon as the compiler can find a structural match. Examples are given in the two sub-sections.

In fact, this could also be extended to static classes with static methods called Invoke. One could simply invoke this directly. This is up for discussion and not part of the main proposal here. Example can be seen below.

public static class Foo { public static int Invoke() => 42; }
public int M() => Foo(); // Foo.Invoke();

what benefit would there be to this... don't have any good examples now which is also why this is not part of main proposal.

Member method example

public interface IFunc<TResult> { TResult Invoke(); }
public int M<TFunc>(TFunc compute) where TFunc : IFunc<int> => 
    compute(); // compute.Invoke();
public int? M<TFunc>(TFunc compute) where TFunc : IFunc<int> => 
    compute?(); // compute?.Invoke();

the above example is a pattern I have used a lot for numerical libraries where one need to apply a "functor" to each element in an "array". By defining a value type TFunc one can get method inlining which is cruzial for performance. The desire is code generated specifically for that TFunc. Trading code size for performance. Often this involves unrolling loops and hence a lot of .Invoke( in the code. This is just an example and the main point is this gets more succinct with the invocation operator being available for the types involved. Performance will not change in any way for this proposal. IL is the same.

Extension method example

public struct Foo { public int Test() => 42; }
public static class FooExtensions { 
    public static int Invoke(this Foo foo) => foo.Test(); 
}
public class Bar { 
    public int M(Foo foo) => foo(); // foo.Invoke();
    public int? M(Foo? foo) => foo?(); // foo?.Invoke();
}

Above shows how this would also work for extension methods.

Future

In the future C# might get static delegates, traits and hopefully many other things. By extending the invocation operator and adding null-propagation invocation operator these will make it natural to invoke any type or instance that is invoke-able. Hence, Action<> and Func<> like types get easy to use invocation operator support.

There already is a proposal to extend using to be based on structural matching, which makes a lot of sense in the face of ref-types. cc: @gafter
#1623

Generally, I think it would be preferable to direct C# towards being based more on "patterns" rather than exact type matching, since that creates a direct coupling between runtime types and language. When possible this would make C# more "free" and open to other use cases. Letting new code patterns could emerge.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions