Монадичность в сборке строк

Когда-то давно я писал на тему того, как с помощью монадичного подхода можно делать цепочные проверки на null (монада Maybe). Сейчас я хочу показать аналогичный подход, в котором путем создания декоратора мы можем делать весьма интересные вещи со строковыми литералами.

Вы уже знаете что конкатенация большого количества строк не есть гуд, так? .Net рекоммендует нам использовать для этого StringBuilder, но тем не менее иногда мы все же опускаемся на землю и пользуемся операторами + или +=. Это нормально пока в дело не вступает какая-то сложная логика. Например:

void AppleReport(int count)
{
  Console.WriteLine("You have " + count + "apple" +
                    count != 1 ? "s" : string.Empty); // wtf?
}

Код выше корректен, только записан он как-то не по-человечески: например, в коде выше string.Empty не должен фигурировать вообще – не думаю что в спецификации к этому методу хоть где-то упоминается использование пустой строки.

Наличие или присутствие окончания “s” – это тоже по сути дела монадичное поведение, только не в той форме в которой мы привыкли. Первое приближение к решению – это написать, например, метод расширения AppendIf() для класса… StringBuilder:

public static StringBuilder AppendIf(this StringBuilder sb, string text, bool condition)
{
    if (condition)
        sb.Append(text);
    return sb;
}

Последующее использования сего конструкта достаточно предсказуемо:

void CountApples(int count)
{
    Console.WriteLine(
        new StringBuilder("You have ")
            .Append(count)
            .Append(" apple")
            .AppendIf("s", count != 1));
}

Нам даже не пришлось использовать ToString() в примере выше но, по-хорошему, этот вызов нужен в большинстве случаев. Да даже без ToString(), цепочка вызовов выше не отличается какой-то особой изящностью. Что нужно так это более хитрый способ составлять подобные цепочки.

Монадичность + Гибкий Интерфейс

Итак, представим что у нас больше нет StringBuilder, но есть некий декоратор поверх него. Например:

public class MyStringBuilder
{
    private StringBuilder sb;
    public MyStringBuilder(string text = "")
    {
        sb = new StringBuilder(text);
    }
    public static implicit operator string(MyStringBuilder msb)
    {
        return msb.sb.ToString();
    }
}

В примере выше я не показал собственно пробросы на агрегированный StringBuilder, зато показал инициализацию. Это важно! Мы сейчас этим конструктором воспользуемся в следующем примере.

Итак, допустим что у нас есть класс с методами расширения в которых фигурируют методы похожие на методы StringBuilder, только повешены они на строку (!!!). Например:

public static MyStringBuilder a(this string text, object moreText)
{
    var msb = new MyStringBuilder(text);
    return msb.a(moreText);
}

При этом, у самого MyStringBuilder есть метод a(), который реализован вот так:

public MyStringBuilder a(string text)
{
    sb.Append(text);
    return this;
}

В том же ключе можно, например, создать AppendIf() – опять же, чуть укоротив название метода:

public static MyStringBuilder ai(this string text, object moreText, bool condition)
{
    var msb = new MyStringBuilder(text);
    return msb.ai(moreText, condition);
}

Ну а теперь можно воспользоваться всем этим счастьем и начать собирать строки в более “монадичной” манере:

void CountApples(int count)
{
    Console.WriteLine("You have ".a(count).a(" apple").ai("s", count != 1));
}

По аналогии с этим подходом можно понастроить других полезных методов. Жаль лишь что нельзя совместить наш helper class и класс с методами расширения. Или можно – как вы думаете?

,

19 responses to “Монадичность в сборке строк”

  1. Новый класс — только для того, чтобы имена переименовать? Мне кажется, “а” и “ai” в production-коде использовать неправильно, ничего они не говорят читающему о своём предназначении. Не такое уж и длинное ведь слово Append. Можно And сделать, если уж так критично, хотя тут будут аллюзии с Boolean)

    А AppendIf, это да, то, что надо.

    • Новый класс потому, что не хочется делать string.Append(). А что касается наименований, поскольку все это – инфраструктурные элементы (internal), их можно именовать как душе угодно. У меня например почти во всех проектах fluent-метод поверх string.Format() называется ƒ() – и ничего, это никого не смущает.

      • То, что методы инфраструктурные, не значит, что их будет использовать один человек. Мне кажется, для того, чтобы увеличивать порог входа в команду нового программиста, должны быть более весомые аргументы, чем длина метода.

      • Ну я про то, что для fluent-синтаксиса достаточно сделать соответствующие методы-расширения для строки, и это круто. Что до названия, у меня давно уже сформировалась устойчивая аллергия на одно-трёх буквенные названия, т.к. они повышают тот самый порог вхождения, ну и код меньше читается как английский текст.

      • Кстати, да. В подкасте в качестве одного из преимуществ fluent приводили его читаемость как на английском. А тут вон оно как. Ай-ай-ай :)

      • В данном случае длинные методы все портят. Поэтому люди и пишут тот же string.Format как f() или что-то вроде того. Тут – то же самое.

  2. А то, что мы при каждом вызове a и ai создаем новый MyStringBuilder и новый StringBuilder – это не удар по производительности?

  3. Про пороги входа – бред. Суть не в названии методов. Пост полезный.

  4. Для полноты картины должен быть метод

    StringBuilder AppendIf(this StringBuilder builder, Action thenBuildAction, Action elseBuildAction = null)

    ну или соответствующий MyStringBuilder.ai

  5. ай-ай-ай… меня порезали (((

    Для полноты картины должен быть метод

    StringBuilder AppendIf(this StringBuilder builder, Action<StringBuilder> thenBuildAction, Action<StringBuilder> elseBuildAction = null)

    ну или соответствующий MyStringBuilder.ai

    • Ну да, это еще удобнее. Только вы забыли параметр с булевой переменной (или Func<bool>).

      • не забыл. он считается сразу всегда. о вот ветки – нет.
        Также как и в тернарном операторе.

  6. Красиво. Про названия методов спроить не буду (я сторонник читаемых названий). Но вот одно на мой взгляд упущение есть. Я бы переставил условие в начало т.е.

    Console.WriteLine(“You have “.a(count).a(” apple”).ai(count != 1, “s”));

    так читабельнее. Т.е. иначе при длинной строке условие будет не так заметно. а вот при такой записи оно отлично видно:

    Console.WriteLine(“You have ”
    .a(count)
    .a(” apple”)
    .ai(count != 1, “s”));

  7. Красиво, конечно (в qt, по-моему было что-то похожее).
    Но когда столкнемся с локализацией приложения, неприятно будет такие строки переводить.

    • Ну локализация и такое conditional построение вообще очень плохо вяжется – тут как раз легче использовать теги вроде {0} и string.Format() для их заполнения.

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

Design a site like this with WordPress.com
Get started