CSharpExtensions icon indicating copy to clipboard operation
CSharpExtensions copied to clipboard

Support for Union type?

Open jhf opened this issue 4 years ago • 7 comments

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();

jhf avatar Nov 24 '21 13:11 jhf

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();
            }
}

cezarypiatek avatar Nov 24 '21 18:11 cezarypiatek

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.

jhf avatar Nov 24 '21 20:11 jhf

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?

cezarypiatek avatar Dec 07 '21 20:12 cezarypiatek

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

jhf avatar May 18 '22 10:05 jhf

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

cezarypiatek avatar May 18 '22 19:05 cezarypiatek

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

jhf avatar May 19 '22 12:05 jhf

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.

jhf avatar May 19 '22 12:05 jhf