Repository navigation
Strawman proposal: Invokable objects (and faking two-phase lookup) #9400
Replies: 2 comments 2 replies
|
I'm personally not a fan of how generics are proposed to work with this. It definitely feels like it exists very specifically to try to workaround the generic arity issue with generic methods but doesn't make a whole lot of sense on its own. |
|
Invokable objects β a case for more expressive object-oriented code I'd like to revisit the idea of supporting invokable objects in C#, similar to C++'s "operator()". I believe this would be a valuable addition to C#, not merely as syntactic sugar, but as a way to improve expressiveness, readability, and domain modeling. The problem Consider classes named "SignIn", "Signal", or "Transform". These names naturally describe both an object and an operation. Today, we often end up writing code such as: var signIn = new SignIn(authenticator); Or even: var result = signIn.SignIn(credentials); The method name often adds no meaningful information. We introduce names like "Execute", "Invoke", "Handle", or "Process" simply because the language requires an explicit method call. Ideally, we could write: var signIn = new SignIn(authenticator); With a corresponding declaration such as: public class SignIn A more expressive example: Transform Consider a transformation object that encapsulates a matrix: public class Transform } We could then write: var rotate = new Transform(rotationMatrix); var result = scale(rotate(vector)); Compare that with today's C#: var result = scale.Apply(rotate.Apply(vector)); The former reads almost like mathematical notation, while still benefiting from encapsulation, strong typing, and compile-time checking. Another natural example would be a signal: var signal = new Signal(frequency, amplitude); This closely resembles the mathematical expression "s(t)". Why not just use static methods or delegates? Static methods cannot naturally encapsulate per-instance state and dependencies. Delegates can provide invocation syntax, but they do not offer the same direct object-oriented modeling experience as a named, callable class with its own state, identity, and additional members. Invokable objects would combine both worlds: the expressiveness of function invocation and the encapsulation of ordinary objects. Implementation considerations This should not require changes to the CLR or .NET runtime. The C# compiler could translate the invocation operator into an ordinary instance method in IL, just as it does for other operators. Of course, language design questions such as overload resolution, generics, and compatibility would need to be addressed. But the underlying concept is well established in C++. Closing thoughts I believe invokable objects would be particularly valuable in object-oriented code, where the name of an object often already expresses its primary behavior. Good abstractions should minimize unnecessary ceremony. Sometimes the most readable API is one where the abstraction almost disappears at the call site. I'd love to see this feature reconsidered. |
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Rationale
Thinking about two-phase lookup in the context of extensions, it occurred to me that we might be able to fake it (at least for the most basic cases) with a variation on the same trick we've used since the inception of C# for faking named indexers.
The simplest version of the problem scenario in extensions looks something like:
People would like to be able to call this as
obj.DoThing<SomeType>(), whereSomeTypeisT2, andT1is inferred as the static type ofobj. Today this doesn't work and you have to sayobj.DoThing<ObjType, SomeType>()instead, which is counterintuitive and loses the benefit of inference.My thought is that we could fake the desired behavior, much like we fake named indexers today. C# doesn't support named indexers (
obj.Stuff[...]) directly, only default indexers (obj[...]). But if we make a property calledStuffthat returns a second type, and that type has a default indexer, then as far as consuming code is concerned, it looks and quacks an awful lot like a named indexer.In the same way
Stuffis a property whose type behaves like an indexer, we'd makeDoThinga property whose type behaves like a generic method group. That's not possible today, of course, but it occurred to me that we could make it possible with a fairly small and self-contained change to the language. Specifically, we could giveDoThing's type anInvoke<T2>method, and implicitly lowerobj.DoThing<SomeType>()toobj.DoThing.Invoke<SomeType>().Note that it would still be up to the extension developer to implement the fakery; I'm definitely not suggesting that extensions would lower into this form automatically. Again, this mirrors the way named indexers are done.
After overthinking that idea for a while, here's what I ended up with.
Detailed proposal
If an expression
exprresolves to a value of type F, and F is a non-delegate type with at least one accessible instance or extension method namedInvoke, then:expr(args)is equivalent toexpr.Invoke(args), including any inference of generic parameters that would be applied in the full form.expr<TypeArgs>(args)is equivalent toexpr.Invoke<TypeArgs>(args).exprcan be target-typed or implicitly converted to any delegate type if the expressionexpr.Invokecould be, with the same result, including generic inference.expr<TypeArgs>is allowed and can be target-typed or implicitly converted to any delegate type ifexpr.Invoke<TypeArgs>could be, with the same result. It doesn't have a natural type, even if the expanded form would be naturally typed as aFuncorAction.Considerations
Foo<T>()between static methodsFoo<T>.Invoke()andFoo.Invoke<T>(). Of course we could specify rules for resolving such things, but I don't see any real benefit that would outweigh the complexity and potential confusion.Invokemethods from this behavior, but that seems overly limiting when we're actively trying to expand the kinds of member that extensions can provide, so my proposal permits them.Thing<T>and a propertyThingwith an instanceInvoke<T>method were both in scope, making the expressionThing<T>potentially ambiguous, but I think it's still okay:Thing<T>(is currently illegal in existing code and would always refer to the method (except afternew)Thing<T>.would always refer to static members of the type; the new conversion can never apply in a situation that would be immediately followed by a.. (This assumes the new conversion wouldn't be applied to receivers of extension methods, but I believe that's already the case for all implicit conversions anyway)nameof()which would never apply the new conversion anyway.<and>as comparison operators. Hopefully the same rules that distinguish type parameters from comparison operators today can handle it.public void this() { ... }to lower to anInvokemethod that's hidden from the language, as the nameItemis hidden for indexers today.All reactions