Разбор текстовых запросов в Sprache
Я пытаюсь написать код для соответствия строк на основе шаблона:
образец: "собака и (кошка или коза)"
тестовая строка: "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 не вызывает принудительное выполнение лямбда-выражения?) или как заставить его выполняться первым.
Пример кода приведен ниже (я удалил все операторы, но для краткости "и", повторное введение их из шаблона Кори просто. Тривиально. Для компиляции кода необходимо добавить 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 ответ
Есть несколько проблем с вашим кодом, но основная проблема, вызывающая рассматриваемое исключение, заключается в 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));