cezarypiatek / CSharpExtensions

A set of annotations and analyzers that add additional constraints to your codebase
MIT License
122 stars 12 forks source link

Support for Union type? #54

Open jhf opened 2 years ago

jhf commented 2 years ago

I have a project where we are simulating union types, by having a class where one, and only one, attribute can and must be set.

For instance (contrived example):

public record AgentUnion {
  public Monster? Monster {get; set;}
  public Player? Player {get; set;}
}
public record Monster {
  public int ArtificialIntelligenceLevel {get; set;}
}
public record Player {
  public List<string> BackpackContents {get; set;}
}

This allows a heterogenous list of either player or monster. It would have been great to have an annotation like [OneOf] or [Union] that requires the user to provide one, and only one, of those attributes.

And, yes, there is no good way of handling each one of these cases in C#, with a compiler check, and I'm not sure what that would look like.

I'm using:

if (agent.Monster != null){...
} else if (agent.Player != null){...
} else throw new Exception();
cezarypiatek commented 2 years ago

Have you considered using type inheritance for that?

 public abstract record BaseAgent;

 public record MonsterAgent: BaseAgent 
 { 
      public Monster Monster { get; set; }
 }

 public record PlayerAgent : BaseAgent
 {
     public Player Player { get; set; }
 }

    class SomeType
    {
        public BaseAgent Agent { get; set; }
    }

then you can write something like that:

void DoSomethingFor(SomeType t)
{
           switch (t.Agent)
            {
                case MonsterAgent monsterAgent:
                    break;
                case PlayerAgent playerAgent:
                    break;
                default:
                    throw new ArgumentOutOfRangeException();
            }
}
jhf commented 2 years ago

Yes, for C# only that would be nice, however I'm exposing this as types to other programming languages, that don't support abstract types, but does support union/sum types, thats why I'm using composition that can always be translated to a simple dto structure.

cezarypiatek commented 2 years ago

Sorry for the late response, I've been thinking a lot about this problem.

I could try to implement an analyzer that handles the following syntax

public record AgentUnion {
  [FieldUnion(Group="Group1")]
  public Monster? Monster {get; set;}

  [FieldUnion(Group="Group1")]
  public Player? Player {get; set;}
}
public record Monster {
  public int ArtificialIntelligenceLevel {get; set;}
}
public record Player {
  public List<string> BackpackContents {get; set;}
}

.... but I'm afraid that this might introduce too much magic. @jhf what do you think about it? What should I report /check when properties are marked with this attribute?

jhf commented 2 years ago

@cezarypiatek Sorry for the late reply as well. While the mechanism you describe is the ultimate in flexibility, I think a more direct approach would be simpler. In particular, the simplest, and for me most frequently required usage, would be with an anonymous Group where inventing a name would be strange. In that case I would have loved to just have a class/record attribute FieldUnion that would require all attributes to be optional and one would always be required.

cezarypiatek commented 2 years ago

@jhf I might want to take a look at this project https://github.com/StefH/AnyOf

jhf commented 2 years ago

@cezarypiatek I have been using OneOf, but I see that AnyOf has support for json as well!

jhf commented 2 years ago

In the case of AnyOf then the type would be quite straightforward:

...
  public AnyOf<Monster,Player> Agent {get; set;}
...

I'm currently using ServiceStack.OrmLite so I would need that to understand the AnyOf as well.

The OneOf has the advantage that it checks for completeness at compile time.