[AOP] Why are roles favored over genuine aspects #8050
-
TL;DR: The long version: So basically this: role Bar : Foo, ISomething {
// ... ISomething implementation ...
} Instead of this: class Bar : Foo, ISomething {
// ... ISomething implementation ...
} Or worst case this: // Using the new, shiny syntax
sealed class Foo;
class Bar : ISomething {
Foo foo;
// ... ISomething implementation ...
} In theory a new zero-cost abstraction. The proposal seems reasonable, aiming to sidestep the abstraction overhead of class extension. It proposes a more concise means of bundling extension methods, enabling the tagging of objects of a specified base type at design-time to specific extension methods without introducing these methods to all instances of the extended base type: class Foo;
/// <remarks>
/// Runtime type is <see cref="Foo"/>.
/// </remarks>
role Bar : Foo {
// ... Bar specific methods ...
}
/// <remarks>
/// Runtime type is also <see cref="Foo"/>, same as <see cref="Bar"/>.
/// </remarks>
role Baz : Foo {
// ... Baz specific methods ...
} Design-time inheritance without run-time inheritance. This would be achieved by introducing two new class types: For instance, roles would enable annotating a data object (e.g., JSON documents) based on its known content, providing insight into its structure and fields. Leveraging design-time types to confidently access these fields by name, assured of their existence. public role Customer : DataObject {
public string Name => this["Name"].AsString();
} This approach circumvents the need to inadvertently import the same extension methods for other objects of the same base type that might coincidentally exist in the same file. Consider a scenario where a controller manages various untyped responses, some identified as 'Customer' objects and others not. In such cases, roles offer a means to annotate this information on some instances of the base type without incurring unnecessary run-time overhead. This idea falls apart as soon as the object is passed around. Currently, there's no way to check at run-time if e.g. a This would result in either a one-way only cast to its base type, with no reversibility, or a two-way cast with unchecked conversion to the deriving role. However, assuming all role members must adhere to extension rules, this at least wouldn't lead to any new errors. While technically feasible, it would defeat the purpose. Ensuring type-safe casting and supporting type-checking operators at runtime would necessitate a witness object, as pointed out in the proposal's comments, rendering the zero-abstraction cost idea useless. Implementing most interfaces also necessitates some form of instance data, and some interfaces even explicitly call for property implementations, which most of the time means instance data. E.g. IEnumerator isn't possible statically without dirty hacks, because of function re-entry. While I agree, I do feel obligated to contribute an IEnumerable 'extension' which is good enough for most cases. The proposal specifically highlights a use case involving IEnumerable after all. //TLS file
using System;
using System.Collections;
using System.Collections.Generic;
foreach(var i in 1..5)
Console.WriteLine(i);
public static class RangeIteration {
public static IEnumerator<int> GetEnumerator(this Range r)
=> r.Start.IsFromEnd || r.End.IsFromEnd
? throw new NotSupportedException()
: new RangeEnumerator(r.Start.Value, r.End.Value);
public class RangeEnumerator(int current, int end):IEnumerator<int> {
object IEnumerator.Current => Current;
public int Current {get; private set;}
public bool MoveNext() => (Current = current++) <= end;
public void Reset() => throw new NotImplementedException();
public void Dispose() => GC.SuppressFinalize(this);
}
} Sharplab.io link I understand this doesn't work in most cases; however, I feel IEnumerable wouldn't currently warrant a new feature, two keywords, and a runtime overhaul. The second mentioned use-case, an IComparable implementation, would warrant a new feature, and so would IEnumerator (not IEnumerable) honestly, due to current language limitations. While the idea of 'roles' piques my interest, it feels like a third attempt (after extensions and interface default implementation) to avoid the diamond problem. The proposed feature bears resemblance to aspects of Aspect-Oriented Programming (AOP), which many have longed for in C#. Traditional object-oriented programming encounters hurdles when trying to follow the DRY principle with classes without a common ancestor. That is to say: If two classes need the same functionality, and they are not related, you end up writing helper functions or libraries, exposing private members, or duplicating code. AOP offers a remedy to this, by allowing objects to be composed of many fully formed, instantiable components, allowing code reusability across class-trees, though at the cost of an actual subclass, rather than a zero-cost abstraction. Also referred to as the 'Compositional Pattern'. Disambiguation is usually solved via casting if necessary. I also believe optimizing away redundant internal-only classes shouldn't be the users/programmer's job. The compiler does a much better job at tree-shaking anyway. #AgressiveInlining Several third-party frameworks have implemented aspects in C#, indicating a demand for such functionality.
There are two reasons why I'm writing this today:
In conclusion; I'm genuinely curious: Roles especially aren't a new proposal, the idea was brought up years prior, and discarded because too many questions were left unanswered. Why is this the direction the language designers chose to pursue today? |
Beta Was this translation helpful? Give feedback.
Replies: 3 comments 9 replies
-
Because it seems to elegantly provide solutions to many problem spaces we're interested in. More so than aspects. Also, no one has proposed a solution to aspects that doesn't come with serious problems (like being very expensive). As such, we're going with the solution that addresses the scenarios we care about, that has a design that seems workable, and that doesn't go against major goals we have (perf, for example). |
Beta Was this translation helpful? Give feedback.
-
Honestly so would I... Conceptually AOP is nothing but stitching together 2+ base classes, then noting down which classes those were. And I'm not particularly deep in this, but I'd assume that Interface-To-Member mapping which already exists could be adapted for it. I'll try to give that a go. At least for personal use.
…________________________________
From: HaloFour ***@***.***>
Sent: Thursday, April 11, 2024 6:24:55 PM
To: dotnet/csharplang ***@***.***>
Cc: Culp, Benton ***@***.***>; Author ***@***.***>
Subject: Re: [dotnet/csharplang] [AOP] Why are roles favored over genuine aspects (Discussion #8050)
I'd expect AOP, as compile-time instrumentation, to be completely free. You'd only pay for the overhead of thr aspect code itself.
—
Reply to this email directly, view it on GitHub<#8050 (reply in thread)>, or unsubscribe<https://github.com/notifications/unsubscribe-auth/BB7GDOQKBJMJ5ADT7PJRWN3Y422NPAVCNFSM6AAAAABGCRBKUCVHI2DSMVQWIX3LMV43SRDJONRXK43TNFXW4Q3PNVWWK3TUHM4TAOBWGQZDK>.
You are receiving this because you authored the thread.Message ID: ***@***.***>
|
Beta Was this translation helpful? Give feedback.
-
Extensions (both implicit and explicit (or roles)), especially allowing to implement interfaces outside a type is the fundamental of type classes, which is the thing I want it to be in C# the most. This will lead us a safer (without the risk of downcast) and more extensible Spring-like AOP is neither statically verifiable nor efficient at performance, and it can be really expensive to use at runtime. It's convenient but I don't think run-time interception is a right direction for such purpose. |
Beta Was this translation helpful? Give feedback.
Because it seems to elegantly provide solutions to many problem spaces we're interested in. More so than aspects. Also, no one has proposed a solution to aspects that doesn't come with serious problems (like being very expensive). As such, we're going with the solution that addresses the scenarios we care about, that has a design that seems workable, and that doesn't go against major goals we have (perf, for example).