...

суббота, 28 июня 2014 г.

Ещё один Pattern Matching на C# — теперь с построением контекста

Полтора месяца назад я опубликовал статью, посвящённую реализации соспоставления с образцом на C#. В комментарии к статье gBear справедливо отметил отсутствие контекста в кейсах. В первой версии мэтчера я сознательно проигнорировал этот механизм, так как посчитал синтаксические возможности выражений в C# недостаточными для его реализации. Однако, некоторое время спустя я понял, что нужного эффекта можно достичь путём построения Expression вручную. Под катом — реализация полноценного pattern matching.



Изначально при реализации сопоставления с образцом мне хотелось сделать синтаксис case-выражения похожим на следующий:

s => string.IsNullOrEmpty(s) => 0




К сожалению, в C# это является синтаксически неверным: по сути

s => t => s * t




Представляет собой функцию в каррированой форме. Второй идеей для case выражения было использование тернарного оператора вроде следующего:

s => t ? a : b




Которое опять-таки невозможно по причине отсутствия в C# типа Unit (для использования в ветке else). Была идея типом для выражения b сделать Expression и передавать туда следующий case, но этому препятствует требование идентичности типов для выражений a и b.

В какой-то момент я свыкся с мыслью, что реализовать контекстную связанность мне не удастся и пользовался мэтчингом в том виде, в котором он есть.

Однажды в процессе отладки кода, вроде следующего



...
{s => s is string, s => ((string)s).Length}
...




я подумал, что вместо проверки типа is можно проводить преобразование as и проверять результат этого преобразования. Тут меня осенило — ведь это же и будет по сути построением контекста! Не откладывая надолго, я взялся за реализацию.

Во второй версии решено было отказаться совсем от реализации Matcher перебором лямбда-функций и использовать только деревья выражений (как в ExprMatcher). Метод Add пришлось сделать типизированым:



public void Add<TCtx>(Expression<Func<TIn, TCtx>> binder, Expression<Func<TCtx, TOut>> processor)
{
var bindResult = Expression.Variable(typeof (TCtx), "binded");
var caseExpr = Expression.Block(
new []{bindResult},
Expression.Assign(bindResult, Expression.Invoke(binder, Parameter)),
Expression.IfThen(
Expression.NotEqual(Expression.Convert(bindResult, typeof(object)), Expression.Constant(null)),
Expression.Return(RetPoint, Expression.Invoke(processor, bindResult))
));
_caseExpressionsList.Add(caseExpr);
}




Тип TCtx является «типом контекста» для кейса. В случае, если первое выражение вернуло не null экземпляр TCtx, выполняется второе выражение, причём аргументом для него является результат сопоставления.

Предыдущий синтаксис с предикатами решено было оставить, т.к. он иногда удобнее:

public void Add(Expression<Predicate<T>> condition, Expression<Action<T>> processor)
{
var caseExpr = Expression.Block(
new Expression[]{
Expression.IfThen(
Expression.Invoke(condition, Parameter),
Expression.Return(RetPoint, Expression.Invoke(processor, Parameter))
)});
_caseExpressionsList.Add(caseExpr);
}




Т.к. выражения для кейсов теперь строятся непостредственно при добавлении, код сборки полного выражения мэтчера существенно упростился:

private Func<TIn, TOut> CompileMatcher()
{
var finalExpressions = new Expression[]
{
Expression.Throw(Expression.Constant(new MatchException("Provided value was not matched with any case"))),
Expression.Label(RetPoint, Expression.Default(typeof(TOut)))
};

var matcherExpression = Expression.Block(_caseExpressionsList.Concat(finalExpressions));
return Expression.Lambda<Func<TIn, TOut>>(matcherExpression, Parameter).Compile();
}


Приведу небольшой пример функции, возвращающей значение аргумента, в случае если его тип — string или StringBuilder и строку «Unknown object» — в ином случае:



var match = new Matcher<object, string>
{
{s => s as string, s => s},
{sb => sb as StringBuilder, sb => sb.ToString()},
{o => true, (bool _) => "Unknown object"}
}.ToFunc();


В дальнейших планах — добавить в пакет набор готовых дискриминаторов для часто встречающихся кейсов.

На этом, пожалуй, всё. На всякий случай приведу ссылки на проект и nuget-пакет мэтчера:

Проект на bitbucket

Nuget пакет


Как и в прошлый раз, буду рад замечаниям/предложениям в комментариях.

Спасибо за внимание!


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at http://ift.tt/jcXqJW.


Комментариев нет:

Отправить комментарий