Разбор текстовых запросов в Sprache - PullRequest
0 голосов
/ 16 января 2019

Я пытаюсь написать код для соответствия строк на основе шаблона:

шаблон: "собака и (кошка или коза)"

тестовая строка: "doggoat" результат: true

тестовая строка: "dogfrog" результат: false

Я пытаюсь написать синтаксический анализатор, используя Sprache, с большей частью логики превосходного ответа Кори на подобную проблему. Я почти у цели, но я получаю исключение при запуске кода:

'Бинарный оператор AndAlso также не определен для типов System.Func 2 [System.String, System.Boolean]' и '' System.Func`2 [System.String, System.Boolean] '.'

Я понимаю, что это означает, что мне нужно объединить лямбды в узлах дерева выражений с логическими операторами, которые я пытался использовать ExpressionVisitor, основываясь на ответе на другой вопрос здесь . Тем не менее, программа аварийно завершает работу до запуска ExpressionVisitor - кажется, что сначала выполняется команда Parse, но я не совсем понимаю, почему (возможно, это потому, что инструкция Sprache.Parse.Select не вызывает принудительное выполнение лямбда-выражения?) или как заставить его выполнить первым.
Пример кода приведен ниже (для краткости я удалил все операторы, кроме 'и', повторное введение их из шаблона Corey тривиально. Для компиляции кода необходимо добавить Sprache из NuGet.

class Program
{
    static void Main(string[] args)
    {
        var patternString = "dog and cat";

        var strTest = "dog cat";
        var strTest2 = "dog frog";

        var conditionTest = ConditionParser.ParseCondition(patternString);

        var fnTest = conditionTest.Compile();
        bool res1 = fnTest(strTest); //true
        bool res2 = fnTest(strTest2); //false
    }
}

public static class ConditionParser
{
    static ParameterExpression Param = Expression.Parameter(typeof(string), "_");

    public static Expression<Func<string, bool>> ParseCondition(string text)
    {
        return Lambda.Parse(text);
    }

    private static Parser<Expression<Func<string, bool>>> Lambda
    {
        get
        {
            var reduced = AndTerm.End().Select(delegate (Expression body)
            {
                var replacer = new ParameterReplacer(Param);
                return Expression.Lambda<Func<string, bool>>((BinaryExpression)replacer.Visit(body), Param);
            });

            return reduced;
        }
    }

    static Parser<Expression> AndTerm =>
        Parse.ChainOperator(OpAnd, StringMatch, Expression.MakeBinary);
    // Other operators (or, not etc.) can be chained here, between AndTerm and StringMatch

    static Parser<ExpressionType> OpAnd = MakeOperator("and", ExpressionType.AndAlso);

    private static Parser<Expression> StringMatch =>
        Parse.Letter.AtLeastOnce()
        .Text().Token()
        .Select(value => StringContains(value));

    static Expression StringContains(string subString)
    {
        MethodInfo contains = typeof(string).GetMethod("Contains");

        var call = Expression.Call(
            Expression.Constant(subString),
            contains,
            Param
        );

        var ret = Expression.Lambda<Func<string, bool>>(call, Param);
        return ret;
    }

    // Helper: define an operator parser
    static Parser<ExpressionType> MakeOperator(string token, ExpressionType type)
        => Parse.IgnoreCase(token).Token().Return(type);
}

internal class ParameterReplacer : ExpressionVisitor
{
    private readonly ParameterExpression _parameter;

    protected override Expression VisitParameter(ParameterExpression node)
    {
        return base.VisitParameter(_parameter);
    }

    internal ParameterReplacer(ParameterExpression parameter)
    {
        _parameter = parameter;
    }
}

1 Ответ

0 голосов
/ 16 января 2019

Есть несколько проблем с вашим кодом, но основной проблемой, вызывающей рассматриваемое исключение, является метод StringContains, который возвращает лямбда-выражение. И Expression.AndAlso (а также большинство Expression методов) основаны на простых не лямбда-выражениях (или теле лямбда-выражений). Основная идея кода синтаксического анализа состоит в том, чтобы идентифицировать и объединить простые выражения и создать одно лямбда-выражение из полученного выражения.

Чтобы исправить исходную проблему, метод StringContains должен возвращать непосредственно выражение MethodCall, а не лямбда-выражение.

Вторая проблема в том же методе StringContains заключается в том, что он обращает аргументы к string.Contains. Он в основном делает token.Contains(parameter), в то время как в соответствии с ожидаемыми результатами он должен делать обратное.

Весь метод (с использованием другой удобной Expression.Call перегрузки) может быть уменьшен до

static Expression StringContains(string subString) =>
    Expression.Call(Param, "Contains", Type.EmptyTypes, Expression.Constant(subString));

Теперь все должно работать как положено.

Однако, поскольку класс ConditionParser использует один экземпляр ParameterExpression, который затем используется для построения лямбда-выражения, нет необходимости в ParameterReplacer, поэтому метод (свойство) Lambda может быть уменьшено до

private static Parser<Expression<Func<string, bool>>> Lambda =>
    AndTerm.End().Select(body => Expression.Lambda<Func<string, bool>>(body, Param));
...