Ещё один 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 пакет

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

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 3

      +6
      Надо мелкомягких допинать, чтобы встроенную в компилятор поддержку сделали. А то отложили на неопределённый срок.
        0
        Интересная штука.

        На bitbucket-e у вас в примерах в документации закралась ошибка, как мне кажется: в Option<int> скармливаются строки. Соответствующий юнит-тест при этом корректен.
          0
          Исправил, спасибо!

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

        Самое читаемое