dotnet / csharplang

The official repo for the design of the C# programming language
11.33k stars 1.02k forks source link

Proposal: Type aliases / abbreviations / newtype #410

Open louthy opened 7 years ago

louthy commented 7 years ago

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.

    // Implicit alias
    public implicit alias IntMap<T> = Dictionary<int, T>;

    // Usage
    var x = new IntMap<string>();

    // Explicit alias
    public alias Month = int
        where value >= 1 && value =< 12;

This is similar to the using MyAlias = MyType, but is parametric, generates a new type, and works across assemblies.

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:

    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 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 constraint would be injected into the constructor of the generated type alias (which would throw an ArgumentOutOfRange exception if it returns false). 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:

    public interface Pred<A>
    {
        bool IsTrue(A value);
    }

    public struct NonEmpty<A> : Pred<IReadOnlyList<A>>
    {
        public bool IsTrue(IReadOnlyList<A> list) => 
            list.Count > 0;
    }

    public struct Any<A> : Pred<IReadOnlyList<A>>
    {
        public bool IsTrue(IReadOnlyList<A> list) => 
            true;
    }

    public struct List<PRED, A> : IReadOnlyList<A>
        where PRED : struct, Pred<IReadOnlyList<A>>
    {
        A[] items;

        public List(IEnumerable<A> items)
        {
            // Initialise the list
            this.items = items.ToArray();

            // Runs the predicate validation
            if(!default(PRED).IsTrue(this)) throw new ArgumentOutOfRangeException(nameof(items));
        }

        // ... Implementation of the IReadOnlyList interface
    }

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:

    public int Product(List<NonEmpty<int>, int> list)
    {
        var result = list.First();
        foreach(var item in list.Skip(1))
        {
            result = result * item;
        }
        return result;
    }

That means Product can't ever get a type that represents an empty list. e.g.

    var x = new List<Any<int>, int>(new int [0]);
    var y = new List<NonEmpty<int>, int>(new int [0]);   // Compiles, but throws at run-time
    var z = new List<NonEmpty<int>, int>(new [] { 1, 2, 3, 4  });

    var res1 = Product(x);        // Invalid type, won't compile
    var res2 = Product(y);        // Won't ever get here because y can't be constructed
    var res3 = Product(z);        // Works

Clearly passing around List<NonEmpty<int>, int> is very annoying, and so a type-alias of:

    public alias NonEmptyList<A> = List<NonEmpty<A>, A>

Would be great, and would make Product much more declarative and easy to parse:

    public int Product(NonEmptyList<int> list)
    {
        var result = list.First();
        foreach(var item in list.Skip(1))
        {
            result = result * item;
        }
        return result;
    }

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:

    public alias NonEmptyList<A> = List<A>
        where value.Count > 0;

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:

    public alias UserId = int where value > 0
    public alias FirstName = string where value.Length > 0
    public alias Surname = string where value.Length > 0

We could then have methods like:

     public User AddUser(UserId id, FirstName first, Surname last) => ...

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).

    public DateTime CreateDate(int year, int month, int day) => ...

Can anyone say they've never fallen over on something like this ^^^.

    public alias Year = int
    public alias Month = int
    public alias Day = int

    public DateTime CreateDate(Year year, Month month, Day day) => ...

I think calling new to instantiate an aliased type is fine, but I would prefer this:

    CreateDate(Year(2017), Month(4), Day(5));

To this:

    CreateDate(new Year(2017), new Month(4), new Day(5));

We should allow explicit conversions though:

    CreateDate((Year)2017, (Month)4, (Day)5);

I expect under-the-hood for the compiler to create a new type for this, which should be a lightweight struct:

    public struct NewType<A>
    {
        public readonly A Value;
        public NewType(A value) => Value = constraintExpr
             ? value
             : throw new ArgumentOutOfRangeException(nameof(value));
        public static explicit operator NewType<A>(A value) => new NewType<A>(value);
        public static implicit operator A(NewType<A> value) => value.Value;
    }

So Year would be:

    [AliasOf(typeof(int))]
    public struct Year
    {
        public readonly int Value;
        public Year(int value) => Value = constraintExpr
             ? value
             : throw new ArgumentOutOfRangeException(nameof(value));
        public static explicit operator Year(int value) => new Year(value);
        public static implicit operator int(Year value) => value.Value;
    }

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 a struct, I want to raise that as a separate issue)

sirgru commented 7 years ago

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 using with generics is not possible though,

Edit: I didn't see mentioned the using statement in the original post. Maybe that was edited later?

louthy commented 7 years ago

@sirgru That only works within a single compilation unit (a single source file). This should work intra and inter assembly.

sirgru commented 7 years ago

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>;
louthy commented 7 years ago

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 using x = y are using it to avoid naming clashes, not as a more declarative type system.

jnm2 commented 7 years ago

This proposal adds on #259, improving it in a way that would be extremely helpful for me.

Whatever we do here, I hope we don't block a future CLR improvement where I can declare a runtime type alias, enabling me to change the name of a type without breaking source or binary compatibility.

scottdorman commented 7 years ago

@jnm2 What does this add to #259? I see these as being the same. What am I missing?

fanoI commented 7 years ago

My preoccupations is they resembles too much C typedef, then when I'll use another library I find an IntMap<T> and how I should know that is a Dictionary with another name?

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 :-( It will be different if "Day" while being an int (with all operators working!) will have the added value of the validation (only a positive number between 1 and 31).

sirgru commented 7 years ago

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: 1) The DateTime(year: 2017, month: 4, day: 6); will always avoid the perceived error of mixing the arguments. 2) Throwing exceptions is the preferred signaling method that something unexpected is happening, with arguments and otherwise. As proposed, the exception is still thrown but the throwing point is in the predicate and not at the natural spot of checking for validity. This could bring less clarity when debugging. 3) In C#, this does not guard us from doing anything wrong, as mentioned:

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.

louthy commented 7 years ago

My preoccupations is they resembles too much C typedef, then when I'll use another library I find an IntMap and how I should know that is a Dictionary with another name?

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.

Calling and int "Year" seems yet more a typedef without any real value something as C "size_t" that in reality is simply an int :-(

It isn't simply an int, it's a Year that happens to be backed by an int. It stops programmers accidentally providing a Day where the Year should be, and forces them to be explicit when providing an int.

It will be different if "Day" while being an int (with all operators working!) will have the added value of the validation (only a positive number between 1 and 31).

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 NewType feature I talk about in that comment. It's also great for Func signatures to describe exactly what the expected parameters are.

    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)

To me, this is 10x clearer because:

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 int input variable get accidentally assigned to the wrong thing mid-method and because there's no compiler errors the value has propagated and caused issues later?

so, the only purpose of carrying these properties within the type is to carry the documentation with the definition.

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.

The exception will be thrown at the call site either way, and no further compile time safety is gained.

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.

louthy commented 7 years ago

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 where expression is injected into the constructor for the new type, and would throw an ArgumentOutOfRange error if the expression returns false.

yaakov-h commented 7 years ago

That sounds an awful lot like the contracts proposal(s).

sirgru commented 7 years ago

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.

louthy commented 7 years ago

@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 null reference checking. But I'm thinking more about types that have a context (like Month), but once they're stored as an int that information is lost; the name of a variable can't enforce the rules of the type, no matter how obvious you think it is at the point of use. A good example is the common error in javascript with zero indexed months. That's an example of where an out-by-one error could propagate.

Another example that gets away from what might seem like the trivial Year, Month, Day examples is in the project I run day to day (which is a multimillion line code-base, and therefore cognitively challenging to keep in the mind constantly), we'll use aliases for Id types for ORM classes. So for example PersonId is aliased to an int, CompanyId also. That means that the common issue of int IDs flying about doesn't end up with the wrong ID in the wrong foreign-key field. This can be especially problematic if you have a function that takes multiple IDs, and a later refactor causes the order of arguments to change.

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).

jnm2 commented 7 years ago

@scottdorman #259 is explicitly asking for aliases internal to an assembly; my only need is for aliases that act externally.

DerpMcDerp commented 7 years ago

One major problem with this proposal is that sometimes type predicates are dependent on others, e.g. Day should be dependent on Month and Year in the example given in this proposal.

A more common example is Index:

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 Count rather than just being non-negative.

ufcpp commented 7 years ago

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.

jnm2 commented 7 years ago

@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.

louthy commented 7 years ago

@DerpMcDerp

One major problem with this proposal is that sometimes type predicates are dependent on others, e.g. Day should be dependent on Month and Year in the example given in this proposal.

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.

jmagaram commented 7 years ago

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 IntMap<T> but you give it a Dictionary<int, T>

(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 Month = int where value >= 1 && value <= 12. This would be really useful for quickly defining specialized types with custom validation. I'm not sure where you'd draw the line between the quick syntax and having the flexibility of defining additional methods on the new type. If you had alias Farenheit = double you might want a ConvertToCelcius instance method. Maybe you'd want a custom constructor that normalizes a string before storing it. Some of these things could be defined with extension methods and static methods defined elsewhere, but it would be more convenient to define them as part of the alias, like...

public type Email restricts string { 
    static Regex _pattern = new Regex(@"^([\w\.\-]+)@([\w\-]+)((\.(\w){2,3})+)$", RegexOptions.Compiled);

    public Email(string input)
    {
        if (!_pattern.IsMatch(input ?? string.Empty)) throw new ArgumentOutOfRangeException();
        value = input.Trim().ToLower();
    }

    public string UserName => { .. return portion before the domain name... }

    public static (bool isValid, Email result) TryParse(string input) => { .. }
}
ghost commented 7 years ago

Hopefully it will help to solve this spaghetti: Task<Output<ICollection<IUser>>>

because currently available using syntax will look like: using Something = System.Threading.Tasks.Task<Root.Namespace1.Output<Root.Namespace2.ICollection<Root.Namespace3.Membership.IUser>>>;

Ugly...

Type aliases are very required...

schotime commented 7 years ago

+100 for type alias'. Especially for string/int type types.

ghost commented 7 years ago

If somebody means global type aliases.. then it is important to allow them to be nested in class to narrow scope.

gulshan commented 7 years ago

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.

orthoxerox commented 6 years ago

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 Email.

svick commented 6 years ago

@orthoxerox With that approach, if I upgrade to a new compiler, my code suddenly stops compiling? I think that's not considered acceptable.

orthoxerox commented 6 years ago

@svick that's what will happen if you upgrade to the compiler version supporting nullable reference types, too.

svick commented 6 years ago

@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 https://github.com/dotnet/csharplang/issues/790:

A number of the features described would lead to breaking of existing code in the form of new warnings. The simplest "solution" to this is to simply put all the warnings under a big switch. […]

orthoxerox commented 6 years ago

@svick you got me confused there for a moment and I gave a hasty incorrect reply. There won't be a compiler error in most cases, "original type to newtype" and "newtype to original type" conversions should be implicit. The only error would be a conversion from newtype X to newtype Y, which is only possible to get when upgrading your compiler when you either use an external assembly with newtypes incorrectly (so it's a real bug that you've caught) or when you use two external assemblies and pass a newtype from one of them into another expecting a different newtype (now that's a potential issue).

I'll think about the second pitfall some more.

svick commented 6 years ago

@orthoxerox Ok, now that I understand what you meant, that does sound fairly reasonable.

JohnnyMaxK commented 6 years ago

@louthy IMO, we must have a more declarative way to create primitive types with constraints to be in phase with the swagger specification data types: https://swagger.io/docs/specification/data-models/data-types/ In our side we had to develop our own primitive abstract class to manage this kind of constraints in our custom code generation.

Please see below the abstract class we have created to fulfill our needs.


    public interface IPrimitiveType : IComparable
    {
        object Value { get; }
        Type GenericType { get; }
    }

    public interface IPrimitiveType<T> : IPrimitiveType, IEquatable<T>, IComparable<T>
    {
        new T Value { get; }
    }

    [DataContract]
    [Serializable]
    [TypeDescriptionProvider(typeof(PrimitiveTypeTypeDescriptionProvider))]
    [JsonConverter(typeof(PrimitiveTypeJsonConverter))]
    public abstract class PrimitiveType<TMySelf, T> : IPrimitiveType<T>, IFormattable, IValidatableObject
        where TMySelf : PrimitiveType<TMySelf, T>
        where T : IComparable
    {
        static PrimitiveType()
        {
            var valueType = typeof(T);
            if (!valueType.IsSimpleType())
            {
                throw new ArgumentException("The T parameter must be a simple type.");
            }

            if (valueType != typeof(string) && valueType != typeof(bool) && !typeof(IFormattable).IsAssignableFrom(typeof(T)))
            {
                throw new InvalidOperationException($"Cannot create PrimitiveType<{typeof(T).Name}> because it's not implementing {nameof(IFormattable)}.");
            }
        }

        /// <summary>
        /// Override this constructor with [JsonConstructor] attribute
        /// to allow deserialisation of PrimitiveTypes with values
        /// that are no longer possible throught normal constructor validation
        /// </summary>
        protected PrimitiveType() { }

        protected PrimitiveType(T value)
        {
            if (value == null) throw new ArgumentNullException(nameof(value));
            if ((value is Guid || value is DateTime) && value.IsNullOrDefault()) throw new ArgumentNullException(nameof(value));

            Value = value;

            var validationResults = Validate(null).ToArray();

            if (validationResults.Length == 0) return;
            if (validationResults.Length == 1) throw new ValidationException(validationResults[0].ErrorMessage);
            else throw new AggregateException(validationResults.Select(r => new ValidationException(r.ErrorMessage)));
        }

        public virtual IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            yield break;
        }

        /// <summary>
        /// Return the validation context display name or name of the type.
        /// </summary>
        protected string SayMyName(ValidationContext validationContext) => validationContext.SayMyName(this);

        [NotNull]
        [DataMember]
        public T Value { get; private set; }

        #region IPrimitiveType Members

        object IPrimitiveType.Value => Value;

        [SuppressMessage("Microsoft.Design", "CA1033:InterfaceMethodsShouldBeCallableByChildTypes", Justification = "F.Y.")]
        Type IPrimitiveType.GenericType => typeof(T);

        #endregion

        public override bool Equals(object other)
        {
            if (other == null)
            {
                return false;
            }
            if (other is T)
            {
                return this.Value.Equals(other);
            }
            if (!(other is TMySelf))
            {
                return false;
            }
            if (this.Value == null)
            {
                return (other == null);
            }
            return this.Value.Equals(((TMySelf)other).Value);
        }

        public override int GetHashCode()
        {
            return this.Value.GetHashCode();
        }

        public override string ToString() => Value.ToString();

        public string ToString(string format, IFormatProvider formatProvider)
        {
            return (Value as IFormattable)?.ToString(format, formatProvider) ?? ToString();
        }

        public int CompareTo(object obj)
        {
            var implementation = obj as TMySelf;
            if (implementation != null)
            {
                return this.Value.CompareTo(implementation.Value);
            }
            else
            {
                return this.Value.CompareTo(obj);
            }
        }

        public virtual bool Equals(T other)
        {
            return this.Equals((object)other);
        }

        public virtual int CompareTo(T other)
        {
            return this.Value.CompareTo(other);
        }

        public static bool operator !=(PrimitiveType<TMySelf, T> a, PrimitiveType<TMySelf, T> b)
        {
            return !(a == b);
        }
        public static bool operator ==(PrimitiveType<TMySelf, T> a, PrimitiveType<TMySelf, T> b)
        {
            // If both are null, or both are same instance, return true.
            if (ReferenceEquals(a, b))
            {
                return true;
            }

            // If one is null, but not both, return false.
            if (((object)a == null) || ((object)b == null))
            {
                return false;
            }

            return a.Equals(b);
        }

        public static bool operator !=(PrimitiveType<TMySelf, T> a, T b)
        {
            return !(a == b);
        }
        public static bool operator ==(PrimitiveType<TMySelf, T> a, T b)
        {
            // If both are null, or both are same instance, return true.
            if (ReferenceEquals(a, b))
            {
                return true;
            }
            if (a == null
                 && (b is Guid && ((Guid)(object)b) == default(Guid)))
            {
                return true;
            }

            // If one is null, but not both, return false.
            if (((object)a == null) || ((object)b == null))
            {
                return false;
            }

            return a.Equals(b);
        }

        #region DO NOT USE IMPLICT OR YOU ARE GOING TO A WORLD OF PAIN!

        public static explicit operator T(PrimitiveType<TMySelf, T> PrimitiveType)
        {
            return PrimitiveType.Value;
        }

        public static explicit operator PrimitiveType<TMySelf, T>(T primitive)
        {
            if (primitive == null || (primitive is Guid && ((Guid)(object)primitive) == default(Guid)))
            {
                return null;
            }

            var ths = (TMySelf)Activator.CreateInstance(typeof(TMySelf), primitive);
            return ths;
        }

        #endregion
    }

We use it like this:


  [Serializable]
    [DataContract]
    public class UserId : PrimitveType<UserId, Guid>
    {
        public UserId(Guid value) : base(value)
        {
        }
    }

 [DataContract]
    [Serializable]
    public class Percent : PrimitveType<Percent, decimal>
    {
        public Percent(decimal value) : base(value)
        {
        }

        public override IEnumerable<ValidationResult> Validate(ValidationContext validationContext)
        {
            if (Value < 0 || Value > 100)
                yield return new ValidationResult($"{SayMyName(validationContext)} is not a valid percentage.");
        }
    }

As you can see we had some trouble in the json serialization, we apply the validation after the deserialization is done to avoid error management during the deserialization and response the list of errors in case of bad request.

Of course this code is incomplete, if you need more info don't hesitate to ask me.

chrisrollins commented 5 years ago

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.

HaloFour commented 5 years ago

@chrisrollins

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.

What do you mean by this? Delegates can certainly be pointed to methods as long as the signatures match. Func<...> and Action<...> are just standard delegates.

Can you post a short repro?

chrisrollins commented 5 years ago

@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.

pongba commented 5 years ago

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:

  1. composability
  2. scope

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..

theunrepentantgeek commented 5 years ago

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 Foo and a typedef Bar that represents that type elsewhere:

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 WriteName() is expecting a Foo, and will expect that the type information retrieved via GetType() will be for a Foo. So it would be reasonable for that call to GetType() to always return the type information for Foo.

But ... then we have a really odd situation, where bar.GetType() returns one set of type information in one place, and a wholly different set of type information somewhere else.

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.

pongba commented 5 years ago

@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)?

theunrepentantgeek commented 5 years ago

@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 Reader and Writer to support the serialization of a class:

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 CustomerReader and CustomerWriter already existed elsewhere, they'd be used instead, with very surprising results.

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.

pongba commented 5 years ago

@theunrepentantgeek Thanks! That cleared things up quite a bit! :)

VBAndCs commented 4 years ago

This will be more powerful if also allows generic aliases #3309.

svdHero commented 1 year ago

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. In C#, however, it is a pain, because one has to build wrapper types which (from a domain point of view) just don't make sense. Since we have to name both the wrapper and the wrappee, we end up with having two named things in code while only one of them exists in the business domain. That is awful domain modelling whose sole reason is a technical one, namely the lack of proper global type aliases in C#.

And no, the using directive does not solve the problem, because our domain model is used by multiple assemblies and the ubiquitous language should be used in the whole codebase.

This feature proposal should really be pushed by anyone who is serious about DDD.

orosbogdan commented 4 months ago

Any updates on this, it'd would help with strongly typed ids a lot.

CyrusNajmabadi commented 4 months ago

@orosbogdan Explicit extensions seems to subsume these use cases.

KennethHoff commented 4 months ago

@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?

CyrusNajmabadi commented 4 months ago

Whatever strategy we'd go with would likely be the same as what we'd do for any strongly-typed-id scenario. :)

KennethHoff commented 4 months ago

Whatever strategy we'd go with would likely be the same as what we'd do for any strongly-typed-id scenario. :)

.. fair enough 😅