I worked on a small math scripting engine (or DSL if you want). Do it for pleasure, its nothing serious. In any case, one of the functions I want is the ability to get results from it with a safe type. The problem is that there are 5 different types that it can return.
Number, bool, Fun, FunN and NamedValue. There is also AnyFun, which is an abstract base class for Fun and FunN. The difference between Fun and FunN is that Fun takes only one argument, and FunN takes more than one argument. Clearly, this was common enough with a single argument to guarantee a separate type (maybe not).
I am currently using a shell type called Result and the Matcher class to accomplish this (inspired by pattern matching in languages ββsuch as F # and Haskell). It basically looks when you use it.
engine.Eval(src).Match() .Case((Number result) => Console.WriteLine("I am a number")) .Case((bool result) => Console.WriteLine("I am a bool")) .Case((Fun result) => Console.WriteLine("I am a function with one argument")) .Case((AnyFun result) => Console.WriteLine("I am any function thats not Fun")) .Do();
This is my current implementation. This is tough. Adding new types is pretty tedious.
public class Result { public object Val { get; private set; } private Callback<Matcher> _finishMatch { get; private set; } public Result(Number val) { Val = val; _finishMatch = (m) => m.OnNum(val); } public Result(bool val) { Val = val; _finishMatch = (m) => m.OnBool(val); } ... more constructors for the other result types ... public Matcher Match() { return new Matcher(this); } // Used to match a result public class Matcher { internal Callback<Number> OnNum { get; private set; } internal Callback<bool> OnBool { get; private set; } internal Callback<NamedValue> OnNamed { get; private set; } internal Callback<AnyFun> OnAnyFun { get; private set; } internal Callback<Fun> OnFun { get; private set; } internal Callback<FunN> OnFunN { get; private set; } internal Callback<object> OnElse { get; private set; } private Result _result; public Matcher(Result r) { OnElse = (ignored) => { throw new Exception("Must add a new exception for this... but there was no case for this :P"); }; OnNum = (val) => OnElse(val); OnBool = (val) => OnElse(val); OnNamed = (val) => OnElse(val); OnAnyFun = (val) => OnElse(val); OnFun = (val) => OnAnyFun(val); OnFunN = (val) => OnAnyFun(val); _result = r; } public Matcher Case(Callback<Number> fn) { OnNum = fn; return this; } public Matcher Case(Callback<bool> fn) { OnBool = fn; return this; } ... Case methods for the rest of the return types ... public void Do() { _result._finishMatch(this); } } }
The thing is, I want to add more types. I want to make it so that functions can return both numbers and bools, and change Fun to Fun <T>, where T is the return type. This is actually the main problem. I have AnyFun, Fun, FunN, and after introducing this change I will have to deal with AnyFun, Fun <Number>, Fun <bool>, FunN <Number>, FunN <bool>. And even then, I would like it to match AnyFun for any function that doesn't match ourselves. Like this:
engine.Eval(src).Match() .Case((Fun<Number> result) => Console.WriteLine("I am special!!!")) .Case((AnyFun result) => Console.WriteLine("I am a generic function")) .Do();
Does anyone have any suggestions for a better implementation that better handle new types? Or are there any other suggestions on how to get the result in a safe way? Also, should I have a common base class for all return types (and add a new type for bool)?
Performance is not a problem, by the way.
Take care kerr
EDIT:
After reading the feedback, I created this combiner class instead.
public class Matcher { private Action _onCase; private Result _result; public Matcher(Result r) { _onCase = null; _result = r; } public Matcher Case<T>(Callback<T> fn) { if (_result.Val is T && _onCase == null) { _onCase = () => fn((T)_result.Val); } return this; } public void Else(Callback<object> fn) { if (_onCase != null) _onCase(); else fn(_result.Val); } public void Do() { if (_onCase == null) throw new Exception("Must add a new exception for this... but there was no case for this :P"); _onCase(); } }
Its shorter, but the order of things matters. For example, in this case, the Fun parameter will never be run.
.Case((AnyFun result) => Console.WriteLine("AAANNNNNNNYYYYYYYYYYYYY!!!!")) .Case((Fun result) => Console.WriteLine("I am alone"))
But this will happen if you switch places.
.Case((Fun result) => Console.WriteLine("I am alone")) .Case((AnyFun result) => Console.WriteLine("AAANNNNNNNYYYYYYYYYYYYY!!!!"))
Can this be improved? Are there any other problems with my code?
EDIT 2:
Decided: D.