8

私は、小さな数学的スクリプト エンジン (または、必要に応じて DSL) に取り組んできました。楽しみのためにそれを作る、それは深刻なことではありません。いずれにせよ、私が欲しい機能の 1 つは、タイプ セーフな方法で結果を取得する機能です。問題は、返すことができる 5 つの異なる型があることです。

Number、bool、Fun、FunN、および NamedValue。Fun と FunN の抽象基底クラスである AnyFun もあります。Fun と FunN の違いは、Fun は引数を 1 つしか取りませんが、FunN は複数の引数を取ることです。別の型を保証するのに1つの引数で十分に一般的であると考えました(間違っている可能性があります)。

現時点では、これを実現するために Result というラッパー型と Matcher というクラスを使用しています (F# や Haskell などの言語のパターン マッチングに着想を得ています)。使ってみると基本的にはこんな感じ。

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

これが私の現在の実装です。硬いですけどね。新しいタイプを追加するのはかなり面倒です。

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

もっと種類を増やしていきたいということです。関数が数値とブール値の両方を返せるようにし、Fun を Fun< T > (T は戻り値の型) に変更したいと考えています。これは実際に主な問題がある場所です。私は AnyFun、Fun、FunN を持っています。この変更を導入した後、AnyFun、Fun< Number >、Fun< bool >、FunN< Number >、FunN< bool > に対処する必要があります。それでも、一致しない関数に対して AnyFun を一致させたいと思います。このような:

engine.Eval(src).Match()
  .Case((Fun<Number> result) => Console.WriteLine("I am special!!!"))
  .Case((AnyFun result) => Console.WriteLine("I am a generic function"))
  .Do();

新しい型の追加をより適切に処理する、より優れた実装に関する提案はありますか? または、タイプセーフな方法で結果を取得する方法について他の提案はありますか? また、すべての戻り値の型に共通の基本クラスを用意する必要がありますか (および bool の新しい型を追加する必要がありますか)。

ところで、パフォーマンスは問題ではありません。

気をつけて、カー

編集:

フィードバックを読んだ後、代わりにこのマッチャー クラスを作成しました。

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

短いですが、ケースの順序が重要です。たとえば、この場合、Fun オプションは決して実行されません。

.Case((AnyFun result) => Console.WriteLine("AAANNNNNNNYYYYYYYYYYYYY!!!!"))
.Case((Fun result) => Console.WriteLine("I am alone"))

しかし、場所を変えるとそうなるでしょう。

.Case((Fun result) => Console.WriteLine("I am alone"))
.Case((AnyFun result) => Console.WriteLine("AAANNNNNNNYYYYYYYYYYYYY!!!!"))

それを改善することは可能ですか?私のコードに他の問題はありますか?

編集2:

解決しました:D.

4

2 に答える 2

0

DSL の結果を常に同じように扱いたい場合

結果を常に同じ方法で処理したい場合 (特定のタイプの DSL オブジェクトを常に同じ方法で変換/適応させたい場合など)、このようなアダプター デリゲートを配置する 1 つ以上の辞書を使用することをお勧めします。

アプリケーションをどのように拡張する予定なのか正確にはわかりませんが、戻り値の型ごとに 1 つの個別の辞書を用意し、それらすべてにゼロまたは 1 つの入力パラメーターを持たせるのは良い考えだと思います。(複数のパラメーターを使用する代わりに、返したい DSL パラメーターを 1 つのオブジェクトにラップするだけです)。

例:

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

DSL の結果を別の方法で扱いたい場合

コード内で DSL の結果を別の方法で処理したい場合は、ここにある TypeSwith を使用することをお勧めします。TypeSwitch は、複数の if/else ステートメントとキャストを使用するより単純な方法です。このアプローチでは、使用するロジックを指定できるため、辞書に入れるロジックに制限されません。(必要に応じて、TypeSwitch を簡単に変更して拡張メソッドにすることができます)。

例:

public class SomeClass
{
    public void SomeDlsMethod()
    {
        TypeSwitch.Do(someDslObject,
            TypeSwitch.Case<DslObjectA>(someDslObjectA => ...),
            TypeSwitch.Case<DslObjectB>(someDslObjectB => ...),
            TypeSwitch.Default(() => ...)
        );
    }
}
于 2013-07-16T18:55:03.123 に答える