Дмитpий Hecтepук

Блог о программировании — C#, F#, C++, архитектура, и многое другое

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

19 комментариев

Когда-то давно я писал на тему того, как с помощью монадичного подхода можно делать цепочные проверки на 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 и класс с методами расширения. Или можно – как вы думаете?

Advertisements

Written by Dmitri

24 ноября 2010 в 1:40

Опубликовано в .NET, C#

комментариев 19

Subscribe to comments with RSS.

  1. слабо, слабо(

    Бека

    24 ноября 2010 at 5:58

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

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

    Андрей

    24 ноября 2010 at 7:09

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

      Dmitri

      24 ноября 2010 at 9:41

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

        AlexIdsa

        24 ноября 2010 at 10:13

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

        Андрей

        24 ноября 2010 at 11:08

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

          AlexIdsa

          24 ноября 2010 at 11:45

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

          Dmitri

          24 ноября 2010 at 13:10

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

    Alexandr Zhuravlev

    24 ноября 2010 at 12:32

    • А мы не при каждом, а только при первом создаём :)

      Андрей

      24 ноября 2010 at 12:34

    • Не при каждом, только при первом.

      Dmitri

      24 ноября 2010 at 13:09

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

    Denys Kholod

    24 ноября 2010 at 12:37

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

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

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

    Игорь

    24 ноября 2010 at 14:37

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

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

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

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

    Игорь

    24 ноября 2010 at 14:38

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

      Dmitri

      24 ноября 2010 at 14:41

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

        Игорь

        24 ноября 2010 at 14:47

      • ой-ой, параметр bool забыл, но не Func

        Игорь

        24 ноября 2010 at 14:48

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

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

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

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

    Andrey

    30 ноября 2010 at 17:46

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

    Odinserj

    30 ноября 2010 at 23:39

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

      Dmitri

      2 декабря 2010 at 14:56


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

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

%d такие блоггеры, как: