От переводчика

Неожиданный поворот в поддержке String Templates в JDK 23. Команда Java решила отказаться от функциональности, которая есть в большинстве современных языках программирования. Почему так произошло? Кажется, из-за слишком большой гибкости, которую заложили на ранних этапах разработки, а также, нежелания просто сделать “синтаксический сахар” для строковой интерполяции. А чего же хотели разработчики на самом деле? Нам кажется, что все-таки - последнего. Сообщество Spring АйО представляет перевод почтовой переписки Гэвина Бирмана и Брайана Гоеца, в которой решилась судьба String Templates.

Гэвин Бирман gavin.bierman at oracle.com

Пт 5 апреля 14:01:54 UTC 2024 

Спасибо за подробную обратную связь на письмо Брайана. Я думаю, будет справедливо отметить, что все еще присутствует широкий диапазон мнений о том, какую в точности форму должна принять эта функциональность.

Пришло время для нас решить, будем ли мы предоставлять эту функциональность в JDK 23. Учитывая, что есть поддержка изменений дизайна API, но отсутствует единство мнений в отношении того, как этот дизайн должен выглядеть, разумными представляются следующие действия: (а) НЕ поставлять текущий дизайн как превью функциональность в JDK 23, (б) потратить достаточное количество времени на продолжение процесса дизайна. Мы все согласны, что наш любимый язык заслуживает того, чтобы потратить столько времени, сколько нужно, на оттачивание нашего дизайна до совершенства! Превью функциональности предназначены как раз для этого - для опробования зрелых дизайнов, прежде чем мы подтвердим их как окончательные. Иногда мы можем изменить своё мнение.
 
Таким образом, для дальнейшей ясности: в JDK 23 не будет String Templates, даже с --enable-preview. 
 
Для тех из вас, кто экспериментирует со String Templates в JDK 22 - пожалуйста продолжайте, и поделитесь своим опытом с нами. Это лучшая форма обратной связи! (Нам не нужны напоминания о том, что делают другие языки - мы уже провели подробное исследование. Но мы ничего не знаем о вашем приложении; попробуйте и, возможно, обнаружите что-то новое. Экспериментируйте и присылайте нам обратную связь - хорошую или плохую.)
 
Спасибо, 
Гэвин 
 

Брайан Гоец brian.goetz at oracle.com

Пт 8 марта 18:35:43 UTC 2024

Время проверить, как обстоят дела со String Templates. Мы прошли два раунда превью и получили некоторую обратную связь.
 
Напоминаю, что основная цель сбора обратной связи — это узнать о дизайне или реализации то, чего мы еще не знаем. Это могут быть отчеты о багах, отчеты об индивидуальном опыте использования, ревью кода, тщательный анализ, новаторские альтернативы и т.д. А лучшая обратная связь обычно поступает от тех, кто использует функциональность “в состоянии злости” - пытаясь реально написать код с ее помощью. (“Некоторые люди предпочли бы другой синтаксис” или “некоторые люди предпочли бы, чтобы мы сфокусировались только на интерполяции строки” - все это точно попадает в категорию “вещей, которые мы уже знаем”) 
 
В процессе использования этой функциональности в проекте `jextract` мы узнали о многих вещах, неизвестных нам прежде, и они убедили нас поменять наш подход к реализации этой функциональности. В частности, роль процессоров «превышает» предлагаемую ими ценность и, после дальнейшего исследования, мы теперь верим в то, что существует возможность достижения целей данной функциональности без использования явной абстракции “процессора”! Это очень существенный шаг вперед.
 
Из JEP 459: 

  • Упростить написание программ на Java, облегчив процесс выражения строк, которые включают значения, вычисленные во время выполнения. 

  • Повысить читаемость выражений, которые смешивают в себе текст и другие выражения, независимо от того, умещается ли текст в одну строку кода (как со строковыми литералами) или распространяется на несколько строк кода (как с текстовыми блоками). 

  • Улучшить безопасность программ на Java, которые составляют строки из предоставленных пользователем значений и передают их в другие системы (например, составление запросов к базам данных) посредством поддержки валидации и трансформации как темплейта, так и значений, включенных внутрь его выражений. 

  • Сохранить гибкость, позволив библиотекам Java задавать синтаксис форматирования, использованный в строковых темплейтах.

  • Упростить использование API, которые принимают строки, написанные на языках, отличных от Java (например, SQL, XML и JSON). 

  • Разрешить создание нестроковых значений, вычисленных из текстовых литералов и вложенных выражений, не проходя при этом через промежуточное строковое представление.

Не является целями :

  • Не является целью вводить синтаксический сахар для оператора конкатенации строк в Java (+)^ поскольку это позволило бы обходить валидацию, которая входит в список целей. 

  • Не является целью отказываться от поддержки или полностью удалять классы StringBuilder и StringBuffer, которые традиционно использовались для комплексных или программных композиций строк.

Еще одна вещь, которая не изменилась — это наши взгляды на синтаксис для встраивания выражений. В то время как многие люди высказались в духе, “почему бы не сделать то, что делают Kotlin/Scala”, этот вопрос был более чем полностью изучен на стадии первоначального дизайна. (По факту, в то время как разногласия по синтаксису зачастую абсолютно субъективны, в данном случае все было довольно ясно - $-синтаксис объективно хуже, и был бы вдвойне плохо, если бы его внедрили в существующий язык, в котором уже существуют строковые литералы. Всё это было рассмотрено ранее, так что я не буду пересказывать это здесь).
 
Теперь давайте поговорим о том, что с нашей точки зрения должно измениться: роль процессоров и тип StringTemplate.
 
Процессоры изначально представлялись как абстракция преобразования темплейтов к их финальной форме (будь это строка или что-то другое). Однако, Java уже содержит устоявшиеся средства абстракции поведения: методы. (По факту, применение процессора может рассматриваться просто как новый синтакс для вызова метода). Наш опыт использования данной функциональности привлек внимание к следующему вопросу: когда мы конвертируем SQL запрос как темплейт в форму, требуемую базой данных (такую как PreparedStatement), почему мы должны писать: 
 
DB.”… template …” 
 
если мы можем использовать стандартную библиотеку Java: 
 
Query q = Query.of(“…template…”) ? 
 
В самом деле, одна из худших вещей, связанных с наличием процессоров в языке, состоит в том, что разработчики API поставлены в трудную ситуацию: они не знают, написать ли им процессор или обычный API и зачастую вынуждены делать выбор до того, как последствия этого выбора станут окончательно понятны. (Вдобавок к этому, процессоры поднимают аналогичные вопросы и на стороне использования). Но реальная критика здесь связана с тем, что захват и обработка темплейта объединены вместе, тогда как они должны быть отдельными, последовательно вызываемыми функциями. 
 
Это замотивировало нас пересмотреть некоторые причины, по которым процессоры были так важны на начальном этапе проектирования. И оказалось, что на этот выбор повлияли - возможно, чрезмерным образом - ранние экспериментами с реализацией. (Одной из фоновых целей дизайна было позволить дорогостоящим операциям наподобие String::format стать (намного) дешевле. Без глубокого погружения в производительность, String::format может быть более, чем на порядок, хуже, чем эквивалентная операция конкатенации, и это в свою очередь иногда мотивирует разработчиков использовать худшие языковые конструкции для форматирования. Процессор FMT привел эту стоимость обратно к показателям, сравнимым с эквивалентной конкатенацией). Эти ранние эксперименты сформировали предубеждение, что необходимо знать процессор в момент захвата темплейта, но после перепроверки мы осознали, что есть и другие методы достижения желаемой производительности, не требующие знания о процессорах в момент захвата темплейта. Это, в свою очередь, позволило нам заново посмотреть на ту точку в процессе дизайна, через которую мы прошли ранее, когда строковые темплейты были лишь “новым видом литерала”, а работа, выполняемая процессорами, могла вместо этого быть выполнена обычными API. 
 
В этот момент родился более простой дизайн и реализация, которые отвечали целям по семантике, правильности и производительности: литералы темплейтов (“Hello \{name}”) — это просто литеральная форма StringTemplate: 

StringTemplate st = “Hello \{name}”;

String и StringTemplate остаются несвязанными типами. (Мы исследовали несколько способов преобразования их один в другой, но они создавали больше проблем, чем решали). Обработка строковых темплейтов, включая интерполяцию, осуществляется обычными API, которые имеют дело со StringTemplate, в чем им помогают некоторые умные трюки реализации, гарантирующие хорошую производительность. 
 
Для API, где интерполяция известна как безопасная в данном домене, например PrintWriter, API могут делать выбор от имени домена, предоставляя перегрузку, чтобы воплотить этот дизайн:

void println(String) { … }

void println(StringTemplate) { 
    // … interpolate and delegate to println(String) … 
}

Результат состоит в том, что для API с безопасной интерполяцией, вроде println, мы можем использовать темплейт напрямую, не жертвуя безопасностью: 

System.out.println(“Hello \{name}”);

В этом примере строковый темплейт является объектом класса StringTemplate, а не String (отсутствует неявная интерполяция), и выбирается перегрузка println для StringTemplate, которая в свою очередь выбирает, как обрабатывать шаблон. Так мы остаемся верны принципу, что интерполяция достаточно опасна, чтобы быть явным выбором в коде, но мы все еще позволяем библиотекам делать такой выбор, если этого этого хотят.

Аналогично, процессор FMT заменяется перегрузкой String::format, которая интерпретирует шаблоны с встроенными спецификаторами формата (например, «%d»):

String format(String formatString, Object… parameters) { 
    // … same as today … 
}

String format(StringTemplate template) {
    // … equivalent of FMT …
} 

И пользователи могут вызывать это так:

String s = String.format(“Hello %12s\{name}”);

Здесь API String::format выбрал интерпретировать строковые шаблоны согласно правилам, ранее указанным в процессоре FMT (не обычная интерполяция), но этот выбор заложен в семантике библиотеки, поэтому на месте использования не требуется дальнейшего явного выбора. Пользователь уже выбрал передать его в String::format; и этого достаточно для выбора обработки.
 
Там, где API не осуществляют выбора относительно того, что означает развертывание темплейта, пользователи продолжают иметь возможность явно обрабатывать их перед передачей, используя API, которые это делают (например, String::format или обычная интерполяция).
 
Результат всего этого следующий: 

  • Необходимость использования "лишнего кода" на месте вызова (ранее это было имя процессора; теперь это статические или экземплярные методы для обработки шаблона) полностью исчезает при работе с библиотеками, которые уже поддерживают темплейты.

  • Даже с библиотеками, которые требуют использования "лишнего кода" на месте вызова, это не становится более навязчивым, чем раньше, и может быть уменьшено со временем по мере адаптации API.

  • StringTemplate — это просто еще один тип, который API могут поддерживать, если хотят. Процессор "DB" становится обычным factory методом, который принимает строковый темплейт или обычный builder API. 

  • API теперь имеют больше контроля над временем обработки и назначением темплейта, так как мы больше не требуем выбирать процессор на этапе создания темплейта.

  • Становится проще абстрагировать обработку темплейта (например, комбинировать или манипулировать темплейтами именно как темплейтами, до их обработки). 

  • Интерполяция остается неявным выбором, но библиотеки, знакомые с ST, могут делать выбор от имени пользователя. 

  • Влияние особенностей языка и интерфейса API становится намного меньше, что хорошо. Core JDK APIs (например, println, format, exception constructors) обновляются для возможности работы со строковыми темплейтами.

Остаётся ответить на вопрос, который, вероятно, сейчас задаст себе каждый: «как же тогда делать интерполяцию?». Ответ: “через обычные библиотечные методы”. Это может быть статический метод (String.join(StringTemplate)) или instance метод (template.join()), который должен быть реализован (но давайте, пожалуйста, отложим это на потом). 
 
Это набросок направления, поэтому не стесняйтесь задавать вопросы или оставлять комментарии. Мы обсудим детали по мере продвижения.

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм - Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.

Ждем всех, присоединяйтесь

Комментарии (15)


  1. noavarice
    02.07.2024 08:37
    +3

    Ситуация со string templates напоминает анекдот, где унитаз на потолок приделали. Плохо сделали, Вася, надо переделать


  1. gsaw
    02.07.2024 08:37
    +4

    $-синтаксис объективно хуже

    По мне лучше как то явно обозначить темплейт, чем определять это по \{} в строке. Тогда имхо и ${} можно было бы использовать не ломая совместимость с существующим кодом. Что то вроде _"Hello ${name}", для получения результата STR._"Hello ${name}". Я при переключении с java на typescript до сих пор часто ошибаюсь на стрелочных функциях. Потом буду еще на этих темплейтах ошибаться. Причем тут даже ошибку и компайлер не поймает. Если вместо \{} напишу ${}. Если я правильно понял это

    System.out.println(“Hello \{name}”);

    В этом примере строковый темплейт является объектом класса StringTemplate, а не String (отсутствует неявная интерполяция), и выбирается перегрузка println для StringTemplate



    1. ris58h
      02.07.2024 08:37

      Причем тут даже ошибку и компайлер не поймает. Если вместо {} напишу ${}

      В TS поймает?


      1. gsaw
        02.07.2024 08:37
        +2

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


  1. ImagineTables
    02.07.2024 08:37
    +1

    И пользователи могут вызывать это так:
    String s = String.format(“Hello %12s{name}”);

    Объясните, пожалуйста, в чём профит. Я вижу, что в шаблоне строки появляется нездоровая привязка к контексту (name), и теперь её нельзя куда-нибудь вынести (в базу, ресурсы и т.п.). В то время как традиционное форматирование — это точка, где сводятся абстрактный шаблон и конкретный контекст.


    1. ris58h
      02.07.2024 08:37

      в чём профит

      https://openjdk.org/jeps/459

      Goals

      • Simplify the writing of Java programs by making it easy to express strings that include values computed at run time.

      • Enhance the readability of expressions that mix text and expressions, whether the text fits on a single source line (as with string literals) or spans several source lines (as with text blocks).

      • Improve the security of Java programs that compose strings from user-provided values and pass them to other systems (e.g., building queries for databases) by supporting validation and transformation of both the template and the values of its embedded expressions.

      • Retain flexibility by allowing Java libraries to define the formatting syntax used in string templates.

      • Simplify the use of APIs that accept strings written in non-Java languages (e.g., SQL, XML, and JSON).

      • Enable the creation of non-string values computed from literal text and embedded expressions without having to transit through an intermediate string representation.

      Это же и в статье есть.


      1. ImagineTables
        02.07.2024 08:37

        Вопрос, как бы, был не совсем об этом. Привязка к контексту (к именам переменных) это такой здоровый минус, что я не представляю, как его можно чем-то перевесить. Такие строки навсегда обречены оставаться в коде, и их нельзя никуда выносить, ни в ресурсы, ни в конфиги, ни в базу. Потому что тогда сразу начнётся рассинхрон — просто поменяли имя в коде, и привет. А держать все литералы в коде — это, может быть, приемлемо для хелло-ворлдов, но не для промышленных программ, где все тексты потенциально локализуемые, их пишут и правят копирайтеры, а не кодеры, без необходимости пересборки, и т.п. Вопрос, то есть, был в том, как кто-то с этим мирится.

        Что касается конкретно этой цитаты, в отдельных местах это какое-то натягивание совы на глобус.

        Improve the security of Java programs that compose strings from user-provided values and pass them to other systems (e.g., building queries for databases) by supporting validation and transformation of both the template and the values of its embedded expressions.

        Как же мы всю жизнь боролись без интерполяций с SQL-инъекциями? Ах да, у нас же для этого был функционал query-builder'а, который и занимался безопасностью с учётом предметной области. А теперь что? Снова выносить санитайзинг на уровень прикладного программиста?


        1. Fuud
          02.07.2024 08:37

          А держать все литералы в коде — это, может быть, приемлемо для хелло-ворлдов, но не для промышленных программ, где все тексты потенциально локализуемые, их пишут и правят копирайтеры, а не кодеры

          Все и не требуется. Старый format никто удалять не пытается. Так что где хранить темплейты и привязывать ли их к контексту - на выбор разработчика. Более того, предыдущий вариант с процессорами делал возможность использовать темплейт как ключ, а локализованную строку забирать откуда хочешь по любым правилам.

          Ах да, у нас же для этого был функционал query-builder'а, который и занимался безопасностью с учётом предметной области.

          Так они и хотят (ради этого и был сыр-бор с процессорами), чтобы можно было написать query-builder, но так, чтобы читался попроще.


          1. ImagineTables
            02.07.2024 08:37

            где хранить темплейты и привязывать ли их к контексту - на выбор разработчика

            А можно просто пример кода, где от интерполяции больше пользы, чем вреда? Заранее спасибо. Устроит на любом языке, т.к. пихают её всюду.


            1. Fuud
              02.07.2024 08:37
              +1

              Повсюду пихают примитивный вариант - тот, что заменяет конкатенацию строк. Ну и там от нее не заметно вреда, одна польза - читается проще. Если в коде складываются строки, скорее всего, замена будет выглядеть лучше.

              log.info("User "+user.getUsername()+" initiated operation: "+operation)
                vs
              log.info("User ${user.getUsername()} initiated operation $operation")


              1. ImagineTables
                02.07.2024 08:37

                Спасибо.

                Во-первых, про конкатенацию речи вообще не идёт. За неё надо сразу гнать из профессии. Надо сравнивать форматирование и интерполяцию.

                А, во-вторых, это как раз то, про что я написал. Тут предполагается, что логи нелокализуемые, ненастраиваемые из конфигов, ненастраиваемые VERBOSE'ом и т.д. Для курсовой, для пет-проекта, для прототипа это нормальные предположения, для промышленного софта — не очень.

                Пока что я вижу, что интерполяция — способ понижения порога вхождения в язык, в ущерб хорошим практикам. Но всегда готов пересмотреть свои взгляды, если будет более другой пример.


                1. Fuud
                  02.07.2024 08:37

                  Локализуемые логи - это что-то реально существующее? Я такого не встречал.

                  Почему verbose не настраивается? Настраивай, как хочешь. Если не хочешь, чтобы строки высчитывались каждый раз, используй лямбды. Что-то вроде этого:

                  log.info(() -> "User ${user.getUsername()} initiated operation $operation")

                  Но разве это хуже вот этого:

                  log.info("User {} initiated operation {}", user.getUsername(), operation)


                  1. ImagineTables
                    02.07.2024 08:37

                    Локализуемые логи - это что-то реально существующее? Я такого не встречал.

                    Если продукт B2B (какая-нибудь СУБД, СКД, система управления производством или просто софтина, идущая в комплекте с промоборудованием) и поставляется в разные страны, локализация логов не будет особо отличаться от локализации остального, например, дашборда. С её помощью можно сегментировать рынки, чтобы купившие локальную версию со скидкой не могли её внедрить за пределами страны, чтобы впарить курсы, сертификацию и т.п. У продажников это называется «(к)анальная дисциплина».

                    Почему verbose не настраивается?

                    Мне кажется, это не очень правильно, когда в прикладном коде вообще есть какие-то признаки поддержки verbose. Например, надо что-то менять, когда мы добавляем verbose, или, наоборот, удаляем его. Думать об этом должен автор логгера, когда такое бизнес-требование появится (может, оно и не появится). А в прикладном коде надо просто отдать ему событие логгирования и все значения, которые имеют для него смысл. Заодно не будем проблем, когда (и если) понадобится конфигурирование, локализация и пр. Ну и вообще — гораздо ведь лучше, когда данные отделены от кода.


  1. tsypanov
    02.07.2024 08:37

    в данном случае все было довольно ясно - $-синтаксис объективно хуже

    Совершенно непонятно, чем он хуже. Ну и в целом обороты вроде

    Мы исследовали несколько способов преобразования их один в другой, но они создавали больше проблем, чем решали

    Оставляют впечатление недосказанности


    1. Fuud
      02.07.2024 08:37

      Как я понял, главная проблема с $-синтаксисом, что уже есть куча строк, где используются такие темплейты. Могут возникнуть неожиданные коллизии. Например, с темплейтами в Спринге.