Skip to content
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

Feature Request: "Refined Value Type" #2369

Closed
KlausEvenEnevoldsen opened this issue Mar 27, 2019 · 21 comments
Closed

Feature Request: "Refined Value Type" #2369

KlausEvenEnevoldsen opened this issue Mar 27, 2019 · 21 comments

Comments

@KlausEvenEnevoldsen
Copy link

When I write entity classes that I use with Entity Framework, I would like to do something like this:

public class Artist
{
   public ArtistId Id { get; set; }
   public ArtistName Name { get; set; }
}

ArtistId is an Int32 with a little bit more. It is a Refined Value Type that I can use anywhere in my code. I can use it to make sure that I pass the right ID in my services. If I pass an Int32 I would get an error when I try to compile the code.

I image an refined Value Type could be defined something like this:

public ValueType Int32 ArtistId;

And it would be great if the Refined Value Type could be decorated with attributes like this:

[PrimaryKey]
public ValueType Int32 ArtistId;

[MaxLength(200)]
public ValueType String ArtistName;

I know this is a big change. :-)

It would be wonderful to be able to write services with strong verification of the types used.

@theunrepentantgeek
Copy link

#39
#1695

@KlausEvenEnevoldsen
Copy link
Author

#39
#1695

I see the similarities, but this suggestion is mainly syntactic sugar and handled mostly by the compiler. :-)

@HaloFour
Copy link
Contributor

@KlausEvenEnevoldsen

but this suggestion is mainly syntactic sugar and handled mostly by the compiler. :-)

In what way? What does a "ValueType" field compile to? How do you use it? What makes it different from either of the other proposals, which are also just syntax sugar?

@KlausEvenEnevoldsen
Copy link
Author

In what way? What does a "ValueType" field compile to? How do you use it?

@HaloFour

This would be an Int32:

public ValueType Int32 ArtistId;

It would be comparable to an Int32:

ArtistId artistId = 42;

if (artistId == 42)
   // true

And it would "inherit" all static methods of an Int32 (as it is an Int32):

ArtistId x = ArtistId.Parse("42");

What makes it different from either of the other proposals, which are also just syntax sugar?

I see this suggestion as a more simple implementation. :-)

@HaloFour
Copy link
Contributor

@KlausEvenEnevoldsen

I don't get it. What's the point if ArtistId is just an Int32? What does that buy you? Normally you use a wrapper struct when you're trying to enforce a degree of type safety between values that are the same type.

Can you give an example as to what your code would compile to?

@KlausEvenEnevoldsen
Copy link
Author

@KlausEvenEnevoldsen
I don't get it. What's the point if ArtistId is just an Int32? What does that buy you? Normally you use a wrapper struct when you're trying to enforce a degree of type safety between values that are the same type.
Can you give an example as to what your code would compile to?

I am not a language designer, so I cannot give you that example, but I can show you how I would use it. :-)

[PrimaryKey]
public ValueType Int32 ArtistId;

[MaxLength(200)]
public ValueType String ArtistName;

public class Artist
{
   public ArtistId Id { get; set; }
   public ArtistName Name { get; set; }
}

public class SomeService
{
   public Artist FindArtistWithId(ArtistId id) { … }
}

public class SomeController : Controller
{
   public IActionResult MyAction(ArtistId id, [FromServices]SomeService service)
   {
      // would be ok
      var artist = service.FindArtistWithId(id);

      // would not be ok
     var artist2 = service.FindArtistWithId(42);

      // would be ok
      var artist3 = service.FindArtistWithId(42 as ArtistId);
   }
}

@HaloFour
Copy link
Contributor

@KlausEvenEnevoldsen

I am not a language designer, so I cannot give you that example,

Without those details it's kind of difficult to ascertain exactly what you're looking to accomplish with this proposal.

      // would not be ok
      var artist2 = service.FindArtistWithId(42);

If it's not ok to pass an Int32 to a method that expects an ArtistId then how do you get an ArtistId? And if ArtistId isn't a separate type then how is that separation encoded or enforced?

@KlausEvenEnevoldsen
Copy link
Author

  // would not be ok
  var artist2 = service.FindArtistWithId(42);

If it's not ok to pass an Int32 to a method that expects an ArtistId then how do you get an ArtistId? And if ArtistId isn't a separate type then how is that separation encoded or enforced?

Something like this:

     // would be ok
     var artist3 = service.FindArtistWithId(42 as ArtistId);

@HaloFour
Copy link
Contributor

So then ArtistId is a type? You said it was an Int32. How would as work with that? And as doesn't work with value types at all.

@KlausEvenEnevoldsen
Copy link
Author

So then ArtistId is a type? You said it was an Int32. How would as work with that? And as doesn't work with value types at all.

Or, ArtistId is an Int32 and the as keyword is altered and can transform Int32 into a ArtistId. I am trying to not invent new keywords.

I'll leave the implementation details to people more intelligent than me :-)

@scalablecory
Copy link

The C++ community has long had a concept of "strong typedef" that accomplishes this.

Type safety is exactly the point of it and it can be super useful to help the author create code free of errors by forcing some form of cast to get at the actual type.

I'd give this a weak +1, weak only because a wrapper struct can accomplish the same thing and is more flexible re: error checking that a value is within limits.

@HaloFour
Copy link
Contributor

@KlausEvenEnevoldsen

I am trying to not invent new keywords.

I'll leave the implementation details to people more intelligent than me :-)

The syntax is much less important than what you want that syntax to accomplish. I'm trying to unpack exactly what this proposal is attempting to accomplish which is difficult to do from a blob of invented syntax. The language team isn't very likely to pick up a proposal that doesn't give them something actionable.

I would put that sample code for working with the type in your proposal as that is definitely a big part of it.

@scalablecory

Indeed, this feels like something between aliases and case classes. I'm all for the concept, but I'm not sure how this specific syntax gets us there. For example, these things are declared as fields/properties, but they're treated as types. That seems to really limit where and how you can use them, not to mention the issues that would arise if you have the same name declared in two separate types. This proposal seems to want the alias to be erased and replaced with the underlying type, which opens a huge can of worms when it comes to metadata representation. But we don't even have a full proposal to discuss.

@AartBluestoke
Copy link
Contributor

@HaloFour wherever we have functions that take an Int or string, visual studio automatically shows the variable name, to help you avoid you putting the "integer-that-is-a-person's-age" into the "integer-that-is-a-userID" slot.
It may be possible to make this compile time type checking only where it is an error to stick an UserAge into a UserID slot, even though both slots take.

Which is easier to use:

processUser(int,int) // just trust the user to put the right type of int into the right slot
processUser(UserID,UserAge)

The only current mechanism is to trust the user to put the right type of int into the current slot.

If we were to do this with classes and propose something that looked like :
processUser(Object userPrimaryKey,Object userParameters) // just trust the user to put the right type of Object in the right slot
this would (rightfully) be handled with close-as-stupid. At the moment we face the same thing when proposing functions that accept value types, with no mechanism fall into the "pit of success".

These types would be implicit-cast Covarient, and explicit cast Contravairent with their underlying type. They could even compile transparently into their underlying valuetype, with just an annotation on functions accepting them as parameters.

What they are not is assignable to sibling types of the same value type, giving us compile time type safety over specific uses of integers, string, and boolean.

@HaloFour
Copy link
Contributor

@AartBluestoke

I'm aware of the concept. I'm not opposed to a solution here.

I personally advocate for case classes or single case union types. As proper types not only do you get better support from the type system and the rest of the ecosystem, but you have the opportunity to add custom validation, equality, comparison operators, etc. The records proposal seems to get us there with as little required boilerplate as:

public class UserID(int Value);

If they can be value types then all the better as the CLR is good at optimizing them away.

@KlausEvenEnevoldsen
Copy link
Author

@HaloFour wherever we have functions that take an Int or string, visual studio automatically shows the variable name, to help you avoid you putting the "integer-that-is-a-person's-age" into the "integer-that-is-a-userID" slot.
It may be possible to make this compile time type checking only where it is an error to stick an UserAge into a UserID slot, even though both slots take.
Which is easier to use:
processUser(int,int) // just trust the user to put the right type of int into the right slot
processUser(UserID,UserAge)

The only current mechanism is to trust the user to put the right type of int into the current slot.
If we were to do this with classes and propose something that looked like :
processUser(Object userPrimaryKey,Object userParameters) // just trust the user to put the right type of Object in the right slot
this would (rightfully) be handled with close-as-stupid. At the moment we face the same thing when proposing functions that accept value types, with no mechanism fall into the "pit of success".
These types would be implicit-cast Covarient, and explicit cast Contravairent with their underlying type. They could even compile transparently into their underlying valuetype, with just an annotation on functions accepting them as parameters.
What they are not is assignable to sibling types of the same value type, giving us compile time type safety over specific uses of integers, string, and boolean.

Yes, thank you for explaining my intentions, when I am not able to make them clear myself :-)

@KlausEvenEnevoldsen
Copy link
Author

The C++ community has long had a concept of "strong typedef" that accomplishes this.
Type safety is exactly the point of it and it can be super useful to help the author create code free of errors by forcing some form of cast to get at the actual type.
I'd give this a weak +1, weak only because a wrapper struct can accomplish the same thing and is more flexible re: error checking that a value is within limits.

To me the important thing is to obtain the functionality, the syntax is less important.

@alaa13212
Copy link

This sound very similar to inline classes of Kotlin
https://typealias.com/guides/introduction-to-inline-classes/

@juliusfriedman
Copy link

juliusfriedman commented Mar 31, 2019

And C# this would just be

using Int32 = Whatver

It's called a the using directive..

https://docs.microsoft.com/en-us/dotnet/csharp/language-reference/keywords/using-directive

I think we need to refine this proposal 🤗

@alaa13212
Copy link

Problem with using directive is that it is per file
It is true that this directive

using Minute = System.Int32;

Will allow code like this

Minute duration = 5;

Or like this

/// <summary>
/// Send email to target after delay
/// </summary>
/// <param name="email">Model for email to be sent</param>
/// <param name="delay">Delay before it should be sent</param>
public static void SendEmail(Email email, Minute delay = 0)
{
    // TODO 
}

But my library users will see this

public static void SendEmail(Email email, int delay = 0)

Kotlin inline classes are visible to method users
It also support methods, without the need to extend int only for Minute specific functionality
Finally, they are inlined at compile-time so no performance or memory impact

@theunrepentantgeek
Copy link

Kotlin inline classes are visible to method users
It also support methods, without the need to extend int only for Minute specific functionality
Finally, they are inlined at compile-time so no performance or memory impact

Unless I'm missing something important (always a possibility), a proper semantic type gives you everything you want - and can already be implemented in C# without any language changes.

If you check out the discussion of #1300 (Proposal: Add support for typedef (like in C++)), you'll see that this has been heavily discussed before.

I did some benchmarking that not only shows no memory allocation but no meaningful peformance difference between a semantic type and a primitive one.

@YairHalberstadt
Copy link
Contributor

Closing as due to a github bug I can't move this to a discussion, and this issue doesn't have much support. Feel free to create a new discussion and reference this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

8 participants