How can I duplicate the F# discriminated union type in C#?

后端 未结 6 941
抹茶落季
抹茶落季 2020-12-09 18:10

I\'ve created a new class called Actor which processes messages passed to it. The problem I am running into is figuring out what is the most elegant way to pass related but

6条回答
  •  挽巷
    挽巷 (楼主)
    2020-12-09 18:39

    Union types and pattern matching map pretty directly to the visitor pattern, I've posted about this a few times before:

    • What task is best done in a functional programming style?
    • https://stackoverflow.com/questions/1883246/none-pure-functional-code-smells/1884256#1884256

    So if you want to pass messages with lots of different types, you're stuck implementing the visitor pattern.

    (Warning, untested code ahead, but should give you an idea of how its done)

    Let's say we have something like this:

    type msg =
        | Add of int
        | Sub of int
        | Query of ReplyChannel
    
    
    let rec counts = function
        | [] -> (0, 0, 0)
        | Add(_)::xs -> let (a, b, c) = counts xs in (a + 1, b, c)
        | Sub(_)::xs -> let (a, b, c) = counts xs in (a, b + 1, c)
        | Query(_)::xs -> let (a, b, c) = counts xs in (a, b, c + 1)
    

    You end up with this bulky C# code:

    interface IMsgVisitor
    {
        T Visit(Add msg);
        T Visit(Sub msg);
        T Visit(Query msg);
    }
    
    abstract class Msg
    {
        public abstract T Accept(IMsgVistor visitor)
    }
    
    class Add : Msg
    {
        public readonly int Value;
        public Add(int value) { this.Value = value; }
        public override T Accept(IMsgVisitor visitor) { return visitor.Visit(this); }
    }
    
    class Sub : Msg
    {
        public readonly int Value;
        public Add(int value) { this.Value = value; }
        public override T Accept(IMsgVisitor visitor) { return visitor.Visit(this); }
    }
    
    class Query : Msg
    {
        public readonly ReplyChannel Value;
        public Add(ReplyChannel value) { this.Value = value; }
        public override T Accept(IMsgVisitor visitor) { return visitor.Visit(this); }
    }
    

    Now whenever you want to do something with the message, you need to implement a visitor:

    class MsgTypeCounter : IMsgVisitor
    {
        public readonly Tuple State;    
    
        public MsgTypeCounter(Tuple state) { this.State = state; }
    
        public MsgTypeCounter Visit(Add msg)
        {
            Console.WriteLine("got Add of " + msg.Value);
            return new MsgTypeCounter(Tuple.Create(1 + State.Item1, State.Item2, State.Item3));
        }
    
        public MsgTypeCounter Visit(Sub msg)
        {
            Console.WriteLine("got Sub of " + msg.Value);
            return new MsgTypeCounter(Tuple.Create(State.Item1, 1 + State.Item2, State.Item3));
        }
    
        public MsgTypeCounter Visit(Query msg)
        {
            Console.WriteLine("got Query of " + msg.Value);
            return new MsgTypeCounter(Tuple.Create(State.Item1, 1 + State.Item2, State.Item3));
        }
    }
    

    Then finally you can use it like this:

    var msgs = new Msg[] { new Add(1), new Add(3), new Sub(4), new ReplyChannel(null) };
    var counts = msgs.Aggregate(new MsgTypeVisitor(Tuple.Create(0, 0, 0)),
        (acc, x) => x.Accept(acc)).State;
    

    Yes, its as obtuse as it seems, but that's how you pass multiple messages a class in a type-safe manner, and that's also why we don't implement unions in C# ;)

提交回复
热议问题