Strongly-typed integers
Asked Answered
C

7

5

As a thought experiment on a hobby project, I've been thinking of a way to ensure that this sort of subtle bug/typo doesn’t happen:

public void MyMethod(int useCaseId)
{
    // Do something with the useCaseId
}

public void SomeOtherMethod()
{
    int userId = 12;
    int useCaseId = 15;
    MyMethod(userId); // Ooops! Used the wrong value!
}

This bug would be hard to find because there’s no compile-time error, and you wouldn’t necessarily even get an exception at run-time. You’d just get "unexpected results".

To resolve this in a simple way, I’ve experimented with using empty enum definitions. Effectively making a user id a data type (without going quite as far as a class or a struct):

public enum UseCaseId { // Empty… }

public enum UserId { // Empty… }

public void MyMethod(UseCaseId useCaseId)
{
   // Do something with the useCaseId
}

public void SomeOtherMethod()
{
   UserId userId = (UserId)12;
   UseCaseId useCaseId = (UseCaseId)15;
   MyMethod(userId); // Compile error!!
}

What d’you think?

Crosseye answered 10/8, 2010 at 11:46 Comment(11)
Why? You've added a significant amount of complexity in order to what, prevent typos? I think it's a bit overkill to code around someone using the wrong variable.Korea
After reading your question second time, I realize, it seems you're trying to put restriction on arg-name. This is an over-kill. Why one would ever want to do this ? Isn't the type supposed to do the job ?Pepi
It's a thought experiment. I was thinking of applying it specifically to the primary key values in my model. I'm not proposing we all start writing code like this, I just had the idea and wondered what people thought.Crosseye
And on top of it, your MyMethod() is going to deal with the value of the argument not the name of the argument, so wats the point in enforcing validation on the name of the argument. And yes you could always tweak by passing different values to MyMethod() still keeping the name of the arg as UseCaseId.. I'd recommend my answer if you really wish to validate the argument value without adding another type into your project yet keeping it simple.Pepi
@thiscuriousgeek The type doesn't do the job, because UserID and UseCaseID are the same type (int). The point of this experiment is to see if there's a lightweight way to prevent accidentally passing the wrong value to a method. Say for example your ASP.NET MVC controller action takes the UserID property of a ViewModel and passes it to UseCaseService.CloseUseCase(int useCaseID). The service class in this example would close the wrong use case.Crosseye
If I were you, then I'd not design my method as UseCaseService.CloseUseCase(int useCaseID), I'd call it UseCaseService.CloseUseCase(UseCase useCaseObj) which will solve this problem.Pepi
@thiscuriousgeek Yes good point. For my contrived example that's probably what I'd end up doing. I was thinking of times when you have the PK value, and don't want to load the object from your data store just so you can pass it to something that only needed the ID anyway.Crosseye
@Korea "I think it's a bit overkill to code around someone using the wrong variable." Umm, I think that's kind of the point of static typing (though I suppose you have a good point when working in a programming language that makes defining types a pain in the starbangsnailpound).Marquis
Incidentally, I hear the D programming language has "strong typedefs", that is, two types with the same underlying representation can be treated as entirely distinct.Bombe
In a lot of languages you don't need to wrap your type in an enum. Just define two int types and they will be treated as two different types. This is true for go, Typescript, D etc. Personally I'd just define type UsecaseId: int; type UserId: intHie
Maybe. Does that prevent type coercion etc though?Crosseye
P
6

If you're going to the trouble of creating new types to hold UserId and UseCaseId, you could almost as easily make them into simple classes and use an implicit conversion operator from int to give you the syntax you want:

public class UserId
{
    public int Id { get; private set; }

    public UserId(int id)
    {
       id_ = id;
    }

    public static implicit operator UserId(int id)
    {
        return new UserId(id);
    }

    public static void Main()
    {
        UserId id1 = new UserId(1);
        UserId id2 = 2;
    }
}

That way, you get the type safety without having to litter your code with casts.

Palmitate answered 10/8, 2010 at 11:56 Comment(15)
+1. This solution makes more sense to me via usage than the OP code. While the OP code is very short, it will be confusing for a user of the class taking those parameters.Quintinquintina
I think the implicit conversion defeats the idea of letting the compiler check the code.Scarlet
I don't recommend a whole new type for just validating the arguments. In your solution, If it could take 2, it could take any value. Are we addressing the right problem here ?Pepi
@Niall C: You could make this a "TypedInteger" base class, and each time you want a new int type, make an empty derived class. If you felt like it, you could event make it a template, and provide this for any class...Quintinquintina
@jdv, Niall C: Yes I agree. If you can make the implicit conversion go the other way, I think you'll have a winner.Quintinquintina
@this: It prevents you from passing a UserId to a method expecting a UseCaseId, which is what the OP wanted.Palmitate
@this: I say, name it ExplicitlyTyped<T>, and change the operator to public static implicit operator T(ExplicitlyTyped<T> value). Make UserId and UseCaseId derived classes, with only a constructor (to forward to the base constructor). This will solve the implicit conversion calling problem (the compiler accepting MyMethod(5)) , and the cross-compatible values problem.Quintinquintina
That's too much of engineering. There's no need for all this. Check my comments on the question to see why.Pepi
Oops, I thought the @this was a special marker for everyone on the question... :) Anyhow, another advantage of taking the class hit is that it would leave you a hole to shoehorn more code into later. While I wouldn't say that everyone should use these everywhere, I can see where it would be useful. It may be over engineering for some problems, but it may well be a clever, useful, and painless solution in other cases.Quintinquintina
@Merylyn: I agree with you. I only commented within the context of the question and the discussions.Pepi
It probably makes sense to do the implicit cast back to int. But either way, I'd recommend not doing both.Cowbane
Since the language in question seems to be C#, why not use a value type? The semantics seem a better fit.Dig
@Merlyn: I think I've done similar, with a ConstrainedValue<T> type.Cowbane
@kyoryu: Is that what you named a class that you created, or is there a ConstrainedValue<T> type that I should check out? I love finding .NET framework classes I didn't know about :)Quintinquintina
@Merlyn: That was just a class I created. I'm considering writing some classes like that (possibly one for each primitive type) and CodePlexing them.Cowbane
M
2

If it were Haskell and I wanted to do this, I might do it like:

data UserId    = UserId    Int
data UseCaseId = UseCaseId Int

This way, functions will accept a UserId instead of an Int, and creating a UserId is always explicit, something like:

doSomething (UserId 12) (UseCaseId 15)

This is similar to Niall C.'s solution of creating a type to wrap around an Int. However, it'd be nice if it didn't take 10 lines to implement per type.

Marquis answered 12/8, 2010 at 2:43 Comment(0)
U
2

I have wanted to do something similar for a while and your post prompted me to try the following:

 public class Id<T> {
    private readonly int _Id;

    private Id(int id) {
        _Id = id;
    }

    public static implicit operator int(Id<T> id) {
        return id._Id;
    }

    public static implicit operator Id<T>(int id) {
        return new Id<T>(id);
    }
}

which I can use as follows

public void MyMethod(Id<UseCase> useCaseId)
{
   // Do something with the useCaseId
}

public void SomeOtherMethod()
{
   Id<User> userId = 12;
   Id<UseCase> useCaseId = 15;
   MyMethod(userId); // Compile error!!
}

I think passing this type of Id object is better than passing the entire domain object because it makes the contract more explicit between the caller and the callee. If you pass just the id, you know that the callee is not accessing any other property on the object.

Unfavorable answered 24/9, 2011 at 0:57 Comment(0)
E
1

I personally think it is unnecessary to be honest.
It is down to the developer to implement the logic properly and you can not rely on compile time errors for such bugs.

Eugene answered 10/8, 2010 at 11:49 Comment(2)
Even when the compiler can check this part of the logic, and even when this is sort of the point of strong typing? Almost -1 .Marquis
... which makes sense if you're using a dynamic language. If you've got the headache out of a statically-typed language, why not let it catch errors for you?Cowbane
I
1

Late to the game, but FWIW ... this project on codeplex has defined several "strongly typed" scalars like Angle, Azimuth, Distance, Latitude, Longitude, Radian, etc. Actually, each of these is a struct with a single scalar member and several methods/properties/constructors to manipulate it "correctly". Not really much different than making each of these a class, aside from the fact that these are value types rather than reference types. While I have not used the framework, I can see the value of possibly making these types first class citizens.

Don't know whether it is ultimately a good idea or not, but it does seem useful to be able to write code like this (similar to your original example) with type safety (and value safety) ensured:

Angle a = new Angle(45); //45 degrees
SomeObject o = new SomeObject();
o.Rotate(a); //Ensure that only Angle can be sent to Rotate

or

Angle a = (Angle)45.0;

or

Radians r = Math.PI/2.0;
Angle a = (Angle)r;

It seems like this pattern would be most useful if your domain has a lot of scalar "types" with value semantics and potentially many instances of these types. Modeling each as a struct with a single scalar gives a very compact representation (compared making each a full blown class). While it might be somewhat of a pain to implement this level of abstraction (rather than just using "naked" scalars to represent domain values) and discreteness, the resultant API seems like it would be much easier to use "correctly".

Ingram answered 13/12, 2010 at 18:51 Comment(0)
P
0

I'd rather prefer to validate the argument in MyMethod and raise appropriate exception in case of error-condition.

public void MyMethod(int useCaseId)
{
    if(!IsValidUseCaseId(useCaseId))
    {
         throw new ArgumentException("Invalid useCaseId.");
    }
    // Do something with the useCaseId
}

public bool IsValidUseCaseId(int useCaseId)
{
    //perform validation here, and return True or False based on your condition.
}

public void SomeOtherMethod()
{
    int userId = 12;
    int useCaseId = 15;
    MyMethod(userId); // Ooops! Used the wrong value!
}
Pepi answered 10/8, 2010 at 11:56 Comment(2)
Of course, however how to validate that a UserID was passed in instead of a UseCaseID, when they're both int? I.e. what would your IsValidUseCaseId() look like?Crosseye
Why not use a type, and catch the error at compile-time rather than runtime? Isn't that one of the major benefits of strongly-typed languages?Cowbane
T
0

Very late to the party, but I think you're right to strongly type such concepts instead of using primitives.

You might find this project useful:

https://github.com/SteveDunn/Vogen

Teresita answered 27/3 at 21:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.