Enter a safe way to return values ​​from a scripting language in C #

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.

+8
c #
source share
2 answers

Your helper can handle unlimited types by doing something like this:

 public class Matcher { private readonly Result result; // pass this in private readonly List<Func<Result, bool>> cases = new ...(); public Matcher Case<T>(Action<T> action) { cases.add(result => { if(typeof(T).IsAssignableFrom(result.Value.GetType())) { action((T)(result.Value)); return true; } return false; } return this; } public void Do() { for each(var @case in cases) { if(@case(result)) return; } } } 

I think you really don't need a list, unless Result has Value until a later time. I do not quite understand your object model, but if the type of result is known, then do not use the list and just do a type test right away.

+2
source share

If you always want to process the DSL result in the same way

If you always want to handle the result in the same way (for example, if you always want to convert / adapt a specific type of DSL object in the same way), I suggest using one or more dictionaries in which you put adapter delegates like this .

I do not know exactly how you plan to extend your application, but it seems to me that in your case it is a good idea to have one separate dictionary for each type of return and let all of them have zero or one input parameters. (Instead of using multiple parameters, simply wrap the DSL parameters that you want to return to a single object).

Example:

 public class SomeClass { public IDictionary<Type, Action<object>> RegistryVoid { get; set; } public IDictionary<Type, Func<object, int>> RegistryInt { get; set; } public void SomeDlsMethod() { ... // Example when you need to convert your DSL data object to int: int value = RegistryInt[someDslObject.GetType()](someDslObject); } } 

If you want to process the DSL result differently

If you want to view the DSL result differently in your code, I suggest using the TypeSwith found here . TypeSwitch is simply an easier way than using multiple if / else statements and castings. With this approach, you can specify the logic in which you use it, so you are not limited to the logic entered into the dictionaries. (TypeSwitch can easily be changed to become an extension method if you want to.)

Example:

 public class SomeClass { public void SomeDlsMethod() { TypeSwitch.Do(someDslObject, TypeSwitch.Case<DslObjectA>(someDslObjectA => ...), TypeSwitch.Case<DslObjectB>(someDslObjectB => ...), TypeSwitch.Default(() => ...) ); } } 
0
source share

All Articles