-
Notifications
You must be signed in to change notification settings - Fork 1k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Proposal: Type aliases / abbreviations / newtype #410
Comments
If I understand correctly, part of what you'd like is already available: namespace ConsoleApplication1 {
using UserId = Int16;
using FirstName = String;
using SurName = String;
using IntMapToString = Dictionary<int, string>;
class Program {
static void Main(string[] args) {
}
void Test(UserId userId, FirstName firstName, SurName surName) {
// Do stuff here.
}
}
} Using Edit: I didn't see mentioned the |
@sirgru That only works within a single compilation unit (a single source file). This should work intra and inter assembly. |
Yes. I think there could be value in allowing it to be scoped the same way as the namespace and allowing this to compile: using IntMap<T> = Dictionary<int, T>; |
In reality it's just really annoying having to specify it for every source file, and has none of the constraints I listed. It's also a major maintenance headache if you want to change the type being aliased. I think most people who use |
This proposal adds on #259, improving it in a way that would be extremely helpful for me.
|
My preoccupations is they resembles too much C typedef, then when I'll use another library I find an Calling and int "Year" seems yet more a typedef without any real value something as C "size_t" that in reality is simply an it :-( |
I think in addition to the desires of #259 proposal, this proposal brings a couple of functional programming habits into C#. For example, the desire to clearly carve out the limitations of a type and carry them explicitly in the type definition. IMHO this does not work in C# because all users have to construct that specific limited type for every argument of the methods they are calling. This brings a lot of extra work on the client's side and "over-specification" that does not necessarily bring further clarity. They aim to bring the "documentation of limitations with the type" but it ends up being a burden to bring it everywhere. The classic, dead-simple way to do it, check arguments and bail as soon as they are invalid: class DateTime {
public DateTime(int year, int month, int day) {
if(!IsValidDate(month, day)) throw new System.ArgumentOutOfRangeException("Unexpected value range for month / day.");
// Do work
}
private bool IsValidDate(int month, int day) {
// Do work
return true;
}
}
class User {
void Operation() {
var date = new DateTime(year: 2017, month: 4, day: 6);
}
} To me, this is 10x clearer because:
var y = new List<NonEmpty<int>, int>(new int [0]); // Compiles, but throws at run-time so, the only purpose of carrying these properties within the type is to carry the documentation with the definition. The exception will be thrown at the call site either way, and no further compile time safety is gained. If there is a lot of these constraints on a type, then in a general case some other abstraction can be made in an OOP way and the checks can be done there. I think this proposal sees the bulky-ness of the solutions, and tries to create additional language features to combat that. |
Tooling can help here. A tooltip that shows the alias when you hover over it would solve that issue. F12ing to the definition seems trivial too. This is an issue with all polymorphic types, so I'm not convinced it's a negative against this proposal.
It isn't simply an
I agree, see my comment on the Constrained Types proposal. Extending this to contain a predicate of some sort would be great, but I already know there's value to strongly typing simple types as I'm using it with the Func<Year, Month, Day, DateTime> I think a constrained type system should be part of a general type system improvement, rather than something specifically for alias types. But I'd be happy to flesh that out if people thought it would be more appropriate here? (EDIT: Now fleshed out in the original issue)
You're taking one example, then using a technique that isn't enforced by the compiler and saying 'this works'. Are you saying you've never been confused over a signature that takes ints, strings, bools, etc.? Have you never had an
No, it's to enforce type-safety for the life of the object. The self documenting declarative nature of it is a very, very nice side-effect.
Having an exception thrown at the source of the error is a most valuable feature. Just like nullable references are walking time-bombs that need to be constantly checked, so are integer and string values, because they don't have a 'context'. The example of a NonEmptyList, you want to know at the source of the instantiation that the type isn't going to work later when it's propagated through myriad functions. |
I have added some example syntax for how a possible constrained alias might work: public alias Year = int
where value > -5000 && value < 5000;
public alias Month = int
where value >= 1 && value <= 12;
public alias Day = int
where value >= 1 && value <= 31;
public alias NonEmptyList<A> = List<A>
where value.Count > 0;
public alias Username = string
where value.Length > 0;
public alias Password = string
where value.Length > 6 &&
value.Exists(Char.IsLetter) &&
value.Exists(Char.IsDigit) &&
value.Exists(Char.IsPunctuation); The idea would be that the |
That sounds an awful lot like the contracts proposal(s). |
What I was trying to say is that for the vast majority of general cases this kind of overt argument specification isn't needed, in Visual Studio there's the popup which displays the name of the next argument so the chance of such error is really low in this day and age. Where there is lack of clarity for the reader, named arguments can be used. The non-null reference types are coming in the next versions of C#. I can understand the need to put these base types in context, but if the constraints are tied to the type then the new type can be created manually just like you've shown above. I believe most of the arguments in most APIs are user-defined types, and where the arguments are base types they can have specific constraints on the constructor, and all further work is not with base types but the "now properly constructed" type. I would find it superfluous to have a Year type whose only purpose is to be a type for the constructor at one place, I would rather have the checks inside the constructor and work with Date from there. Same general logic can apply in other cases. Just my opinion though. |
@sirgru I think you're possibly thinking a bit too narrowly about short term usage of base types. Of course we can do this validation manually every time a value is used, like we do with Another example that gets away from what might seem like the trivial This is common thinking in functional languages, where the types are the 'point of truth' and not relying on imperatively injected validation throughout an app. The type system validates much more at compile time rather than relying purely on runtime checks (which may be missing, and are therefore impossible for the compiler to reason about). |
@scottdorman #259 is explicitly asking for aliases internal to an assembly; my only need is for aliases that act externally. |
One major problem with this proposal is that sometimes type predicates are dependent on others, e.g. A more common example is public alias NonNegative = int where value >= 0;
public alias Index<T>(ICollection<T> collection) = NonNegative
where value <= collection.Count;
void Foo(IList<T> list, Index(list) index) { ... } Where you want it to use the collection |
I really like this feature but it could be achieved by Source Code Generator as I listed in Code Generator Catalog. On the other hand, if you want constraints on types, "defaultability" checking might be also needed. |
@ufcpp Source generators don't help when what you're doing is using type aliases to rename an existing type without breaking binary compatibility of assemblies built against that type. |
That isn't a problem with this proposal, it's a problem with the example in this proposal. What you're suggesting is a much more complicated proposal which is more akin to dependently types languages. I'm not saying that's not desired (I'd love it), but it's out of the scope of this relatively simple proposal. |
Overall I think this would make code much easier to understand and like how it introduces some additional type safety. I see the value of creating new "primitive" types with built-in validation that match the real-world domain. Although you can validate method parameters and throw before stuffing invalid data into a string or int we lose meaning. EmailAddress is easier to grok than string. Custom primitives help solve the "primitive obsession" code smell. Func and Action delegates are easier to understand without parameter names. Tuples are easier to understand without component names. I think there are a few cases: (1) An alias that is meant to be EXACTLY the same as the type it aliases. It just provides a shorter more intuitive name. Implicit casting to the alias is sometimes desirable like when a method or variable expects a (2) A type that RESTRICTS the domain of the other type through its name but NOT through the values it accepts. For example if PersonId and ProductId are both represented by Guid, a method that expects a PersonId should not accept a ProductId without some kind of cast. (3) A type that RESTRICTS that legal values of the other type like
|
Hopefully it will help to solve this spaghetti: because currently available using syntax will look like: Ugly... Type aliases are very required... |
+100 for type alias'. Especially for string/int type types. |
If somebody means global type aliases.. then it is important to allow them to be nested in class to narrow scope. |
I think the simple aliasing (without constrains) can come first. And it should be discussed by language design team meeting as a simple but meaningful feature. Constrained types can be discussed later along with method contracts. |
I agree with @gulshan, a more restricted feature for aliasing should come first. I imagine it could even avoid creating new types at all, something like this: public newtype Email : string
public void SendEmail(Email to, IEnumerable<Email> cc, string subject, string body) { ... } This is equivalent to: public void SendEmail(
[NewType("Email")] string to,
[NewType("IEnumerable<Email>")] IEnumerable<string> cc,
string subject,
string body)
{ ... } This way programs compiled by older C# versions will be able to consume the API as is, using strings, but more modern programs will get an error when they try to pass a different newtype (say, Phone) instead of |
@orthoxerox With that approach, if I upgrade to a new compiler, my code suddenly stops compiling? I think that's not considered acceptable. |
|
@orthoxerox It won't. Nullable reference types only cause warnings, not errors. And even then, it will be opt-in, based on the current proposal. From #790:
|
I have run into a problem which reminded me of this issue. I am working on a library which exposed some delegate types meant to be passed by application code. The problem I found is that delegates don't seem to be compatible with methods. You can pass lambda expressions but if you want to pass a method you are out of luck. This was fixed by switching from delegates to Func and Action types. Now this is a bit awkward because either I have to explicitly define the types in each parameter or have using statements in each file. Of course I opted for using statements, but that meant I had to go through each file which needed them and add them separately. This is a case where better aliasing mechanics would be useful. Alternatively it would be nice to see some improvements to delegate type resolution and things like that. |
What do you mean by this? Delegates can certainly be pointed to methods as long as the signatures match. Can you post a short repro? |
@HaloFour Looks like you're right. The culprit is an implicit operator which I'm using to convert tuples to structs. It seems that the implicit operator cannot take a tuple with a method if the type it's looking for in the tuple is a delegate, but it works if I use a Func or Action type instead. |
I have to say that I'm truly surprised (to say the least) by the fact that typedef equivalence is still not supported in C# in 2018(!). Not long ago I had the need to rename an existing type to a new name without breaking client code on binary & source-code level, and coming from a C++ background, I naturally searched for ways to do that in C#, and what I found out instead is an utterly utterly broken "using alias" that nobody actually uses (pardon the pun) and everybody complains about (I don't even bother to paste the links it's everywhere). The current status of C# "using alias" suffers from two major issues:
that basically rendered it unusable for serious purpose. And don't start suggesting inheritence or classes... It's semantically evil to peg everything square into the OO round hole. (I don't dislike OO BTW) And I don't really understand what the debate is all about and why the delay, it's not some rocket science langauge feature, it exists in so many mature and modern languages, and has been proven to be useful in so many scenarios. C++ even has a using that supports tempates. Why oh why.. |
Because every feature starts at -100 points and no one on the development team has (to date) seen enough value in typedef to champion the proposal. Spending a few minutes thinking about the idea, I strong suspect that an implemenation of typedef would involve some fairly deep engineering in the CLR, and that there would be some nasty edge cases. E.g. Assume you have a type void WriteName(Foo foo) => Console.WriteLine(foo.GetType().Name);
var foo = new Foo();
var fooName = foo.GetType().Name; // Should be `Foo`
var bar = new Bar();
var barName = bar.GetType().Name; // Should be `Bar`
WriteName(foo); // What should this write to the console? Foo, probably.
WriteName(bar); // What should this write to the console? Bar? Foo? Wilbur? The method But ... then we have a really odd situation, where This is a very deep rabbit-hole that reveals some nasty problems pretty quickly. Given the prevalence of code in the C# ecosystem that relies on class naming (and other conventions) to find related functionality, all of those problems would have to be resolved in useful and non-surprising ways for typedef to make it a viable feature. |
@theunrepentantgeek I think what you were describing is the equivalence of "strong typedef" (i.e. the typedef creates a new type"), but what about starting with the equivalence of traditional C++ typedef? (i.e. bar.GetType() == the original type that Bar aliased from)? |
@pongba For a start, convention based libraries would still fail For example, a seralization library might expect there to be supporting classes with the suffixes public class Customer { ... }
public class CustomerReader { ... }
public class CustomerWriter { ... } A user of that library would expect to be able to write this public typedef Vendor : Customer
public class VendorReader { ... }
public class VendorWriter { ... } But, those classes wouldn't be used. In fact, if the classes Even a simple typedef where you're just defining a different name for an existing type runs into difficulties like this very quickly. I think the key here is a fundamental difference between C++ and C#. In my (admittedly limited) understanding of C++, type names are largely or entirely elided from the compiled binary, having little or not use at runtime. This contrasts with C# where the name of a type is a fundamental thing that's used heavily at runtime for type identification, loading, and so on. |
@theunrepentantgeek Thanks! That cleared things up quite a bit! :) |
This comment has been minimized.
This comment has been minimized.
Any update on this? This is a crucial feature when it come to Domain Driven Design and modelling a domain. It's so easy in F# to assign domain specific aliases to existing types. And no, the This feature proposal should really be pushed by anyone who is serious about DDD. |
Any updates on this, it'd would help with strongly typed ids a lot. |
@orosbogdan Explicit extensions seems to subsume these use cases. |
Last time implementation strategy (lowering) were mentioned it was stated that you couldn't overload on extensions; has that changed since then to allow them to? |
Whatever strategy we'd go with would likely be the same as what we'd do for any strongly-typed-id scenario. :) |
.. fair enough 😅 |
Closing as a duplicate of #1239. |
F# has a feature called type abbreviations where a more complex type can have an easier to use alias. I am finding more and more (especially as I use structs as wrapper types to avoid null) that creating sub-classes for more bespoke behaviour is either impossible or unnecessarily heavyweight.
For further discussion is the notion of a predicate for the
value
that is run on construction and can be used to constrain the type further:The constraint would be injected into the constructor of the generated type alias (which would throw an
ArgumentOutOfRange
exception if it returnsfalse
). And could also be used be tooling to do flow analysis on whether 'bad values' were likely to get into the type.One technique that I have been using a lot recently is to embed validation into a type. Although I think this is likely to be very niche (because it's bulky to use right now), I think it opens up a ton of useful functionality that is similar to dependently typed languages.
Here's a simple example with a bespoke list implementation. This makes use of the concepts idea that has been previously investigated by the Roslyn team:
The
PRED
generic argument allows for validation behaviour to be injected into the type and the value. And therefore the compiler can do a ton of validation for free:That means
Product
can't ever get a type that represents an empty list. e.g.Clearly passing around
List<NonEmpty<int>, int>
is very annoying, and so a type-alias of:Would be great, and would make
Product
much more declarative and easy to parse:This is just one example of where this would be useful and lead to more explicit and declarative code (in my humble opinion). Obviously with the constraints it would be possible to do this:
I would prefer it to go much further than the F# version, and make the alias work more like Haskell's
newtype
. That is it's essentially not implicitly convertible with the type it's aliasing. That makes it trivial to represent lighterweight concepts like:We could then have methods like:
Which would kill another common set of bugs in C#, namely that
int
,string
, etc. are essentially 'untyped' (they have a type, but the type doesn't represent the data stored within it).Can anyone say they've never fallen over on something like this ^^^.
I think calling
new
to instantiate an aliased type is fine, but I would prefer this:To this:
We should allow explicit conversions though:
I expect under-the-hood for the compiler to create a new type for this, which should be a lightweight struct:
So
Year
would be:And then the compiler can inject the
Value
indirection and re-wrap with the single argument constructor. Obviously this comes with a run-time cost, but it is relatively small for such a powerful feature.The
AliasOf
attribute could be a hint for tooling.I'm a strong believer that types should be the guidance to the programmer rather than variable names, because variable names don't persist from the input of a function to the output, whereas types do.
(by the way I'm aware of the limitation of
new List<NonEmpty<A>, A>()
when the type is astruct
, I want to raise that as a separate issue)The text was updated successfully, but these errors were encountered: