Давайте представим нереалистичный сценарий, где вы выбираете язык программирования для проекта, который в перспективе станет очень большим. Допустим, это будет набор сервисов в монорепозитории, над которыми работает более 100 человек. Чтобы сделать этот сценарий ещё менее реалистичным, предположим, что вы игнорируете типичные ограничения, например, не учитываете, сможете ли использовать сборщик мусора, и впишется ли поставленная задача в конкретный стек технологий.

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

Какое подмножество C++ или Kotlin вы используете? Что вы предпочтёте: project.toml или requirements.txt? Теперь у вашего языка есть возможность поэтапной типизации с помощью аннотаций типов. Хотите ей воспользоваться? Как вы реализуете конкурентность: с помощью многопоточности, Tokio или std::async?

Чем более экспрессивный язык, тем сложнее всё становится. И здесь на сцену выходит Go. И речь не только о gofmt, но и о его стандартной библиотеке и согласованности. В Kotlin вам приходится гадать, что лучше использовать для ошибок: исключения или объекты Result? В случае же Go вам всё ясно – ищем err. Да, это многословно, но зато предсказуемо.

Экспрессивные языки прекрасны, но часто создают путаницу. Вы можете использовать богатый и комплексный язык, поддерживающий миллион способов реализации одного и того же. Именно это я хочу вам показать. Как же сохранить всю эту мощь, но уменьшить беспорядок? Как избежать возникновения 500 поддиалектов? Но прежде, чем переходить к решениям, обсудим Scala.

▍ Проблема Scala


Для наглядного представления проблемы возьмём Scala. Кстати, один из моих любимых языков. Вот только есть в нём одна большая загвоздка – отсутствие идиоматичности. Он слишком гибок.
Я могу написать один файл, класс Calculator, начав с простого стиля Java:

 // Инструкции return, скобки и точки с запятыми
    def getResult(): Double = {
      return result;
    }
    
    def multiply(number: Double): Calculator = {
      if (number == 0) { 
        println("Multiplication skipped: number is 0"); 
      } else { 
        result = result * abs(number); 
      }
      return this;
    }

Теперь в том же классе того же файла я могу переключиться на псевдокод Python:

 // Много пустого места, никаких return и точек с запятыми
    def add(number: Double): Calculator = 
      result += abs(number)
      this

    def subtract(number: Double): Calculator =
      result -= abs(number)
      this

А теперь я могу вызвать всю эту конструкцию без скобок и точек в стиле предметно-ориентированного диалекта Ruby:

val calc = new Calculator add -5 subtract -3 multiply -2

Весь код.

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

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

На Hacker News в каждой теме Scala есть комментарий от человека, который унаследовал базу кода и не смог в ней разобраться, отчасти из-за того, что она написана в чужом стиле.

▍ Проблема Монреаля в C++



Любая крупная база кода становится «городом» с разными стилями

В C++20 было внесено много хороших идей, но значительная часть существующего кода предшествует этому стандарту. Поэтому возникает дрифт. Перед вами есть два пути: избегать использования нового стиля написания или получить базу кода, где стилей станет на один больше. Если пойти вторым путём, то в итоге вы столкнётесь с проблемой Монреаля. Работа с кодом «старого Монреаля» будет подобна работе с другим диалектом. Теперь вам потребуется знать несколько диалектов, а также понимать, когда и где каждый из них применять.

▍ Руководств по стилю недостаточно


Руководства по стилю, особенно с применением автоматизации, могут существенно упростить задачу в случае крупной базы кода. Я считаю, что это прекрасно, когда языки позволяют экспериментировать. Возможно, мы пока не знаем, имеет ли смысл везде в Python использовать типы, или как часто следует применять в Go дженерики, или что-то ещё.

Но для конкретного проекта можно устанавливать правила. Предположим, «В этом Python нужны типы» или «Используй дженерики в Go там, где возможно». Мы устанавливаем стандарты для тестирования библиотек и создаём инструменты. И мы также пытаемся применить эти правила с инструментами везде, где можем. Но я считаю, что на уровне сообщества языка можно действовать более эффективно.

И здесь будет недостаточно руководств по стилю для конкретных баз кода.

▍ Царь стиля


Когда в 2006 году вышел Scala 2.0, внутренние предметно-ориентированные диалекты были очень популярны, и на фоне лидирующего тогда Ruby on Rails он предлагал более текучий стиль написания. Но времена меняются, и сегодня этот стиль больше не является идиоматичным для Scala.

Но откуда вам это знать, если вы не вращаетесь в соответствующих кругах? Большие ORM*-фреймворки по-прежнему используют этот стиль в своих руководствах. Тонкости написания современного идиоматичного Scala таятся в умах лидеров сообщества. И это плохо.

*ORM (Object-Relational Mapping, объектно-реляционное отображение)

Нам нужен Царь стилей. Кто-то в сообществе языка, кто сможет сказать, что вот это идиоматичный Scala 2.1:

def ABSOrSeven(maybeNumber: Option[Int]): Int = {
  if (maybeNumber.isDefined) Math.abs(maybeNumber.get)
  else 7
}

Но в Scala 3.1 предпочтительнее такой вариант:

def ABSOrSeven(maybeNumber: Option[Int]): Int = {
  maybeNumber.map(Math.abs).getOrElse(7)
}

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

▍ Идиоматичный Python



Развивающийся стандарт может свести многомерное число диалектов в более-менее прямую линию

Python-разработчики любят свой питонический код, Дзен Пайтона и PEP8. Как раз о таком я и думаю, но в постепенно развивающемся виде и в большем масштабе. Я считаю, что разработчикам языков нужно не только их разрабатывать, но и руководить появляющимися в них стандартами программирования. Они должны общаться с нами и говорить: делай так, а не эдак. И это должен быть именно диалог, со спорами и инструментами, которые помогут нам следовать правилам.

И этот стандарт будет постепенно меняться, ведь так? Он будет развиваться параллельно с языком. Возможно, аннотации типов, когда они только появились в Python, считались экспериментальными. А когда все к ним привыкли и стали считать удачной идеей, Python должен занять твёрдую позицию и заявить, что для питонического кода они являются обязательными. По мере роста языка и разветвления его сообщества должна расширяться и область документации стилей.

Вот пример: Python должен выбрать свою линию в отношении пакетных менеджеров и виртуальных сред. Я считаю, что нужно предпочесть Poetry и project.toml, а другие набили себе татуировки requirements.txt-for-life. Но любое решение будет лучше, чем то, которое мы имеем сегодня.

Должен быть кто-то ответственный, кто встанет со словами: «Итак, слушайте все. Мы собираемся стандартизировать использование для пакетов Hatch. Если у кого-то есть вопросы к Hatch, выскажитесь. Мы это учтём. Но имейте в виду, начиная с Python 3.16 Hatch будет стандартом».

И это касается фреймворков для тестирования, стандартных библиотек и даже способов обработки конкурентности. Языковые сообщества любят экспериментировать и исследовать. Но после исследования нам нужно объединяться. И именно здесь должны подключаться создатели языка. Только они могут сделать это реальным.

▍ Экспрессивность


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

Если вы развиваете язык, значит, у вас есть мнение о том, как должен выглядеть его идеальный код. Скажите нам! Запишите. Поделитесь с сообществом. Например: «Использование макросов в C++20 не считается идиоматичным», «Использование if для проверки типов возвращаемого объекта в Kotlin 1.17 не идиоматично», «Не используйте в Scala явные инструкции return». И так далее.

В этом случае у нас в сознании всегда будет присутствовать целевой образ хорошего кода. И даже если этот образ изменится, любая адекватная база кода лишь находится в определённой точке этого пути к самому продуманному и великому стилю.

Иными словами, вы можете оказаться в мире, где весь идиоматичный код Java 20 такой же единообразный, как Go. Но для того, чтобы в этот мир попасть, потребуется однозначно решить, когда можно использовать Streams API, а когда нет. Я имею в виду, что вы можете никогда окончательно в него и не попасть, поскольку понятный однострочный обработчик потока у одного станет спагетти-кодом у другого. Но я считаю, что мы можем более единогласно сойтись на том, каким хотим видеть язык.

▍ Конец бутербродных войн




Всё это поднимает немало вопросов. Когда следует канонизировать популярную библиотеку? Какой объём руководств по стилю будет слишком большим и начнёт сдерживать развитие? Какую часть этих нововведений можно внедрить с помощью инструментов? У меня ответа нет. Нужно просто с чего-то начать, например с версии Python PEP8, и постепенно продвигаться.

Если что-либо является нормой для сообщества, записать это. Если сообщество спорит о том, как правильно есть бутерброд – маслом вниз или вверх – нужно просто подбросить монету, принять решение и двигаться дальше. Сообществу это пойдёт на пользу.

Покажи нам путь, о Царь стиля!

▍ Сноска


1. Это лишь простой пример. Если бы мы были царями стиля, то да, правилом бы было «Не использовать явные return или точки с запятыми». А также «Не сопоставляйте с образцом (pattern matching), если можете использовать fold или map» и «Не используйте fold, если можете использовать getOrElse», и «Не выполняйте ручную рекурсию», и «Не используйте акторов без действительно веской причины», и «Не пишите кастомные операторы. Просто не пишите», и так далее. Многие возможности оказываются ценными только в конкретных обстоятельствах. ↩︎

Telegram-канал со скидками, розыгрышами призов и новостями IT ?

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


  1. Dair_Targ
    05.04.2024 15:54
    +9

    Но я считаю, что мы можем более согласованно сойтись на том, каким хотим видеть язык

    Не можем, и это - одна из причин популярности php, python, c++, java и прочих подобных. Пока одна группа людей будет тратить время на выяснение того, какой подход – самый лучший, вторая будет писать больше прикладного кода так, как получается, и склеивать используя более гибкий язык.

    В итоге у первой будет красивый hello world, а у второй - таки работающий продукт.


  1. summerwind
    05.04.2024 15:54
    +7

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

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

    Так же как и города во всем мире не обязаны быть как под копирку. Главное, чтобы на одной и той же улице рядом со строениями 18-го века не стояли небоскребы :)

    Взять упомянутый язык Scala. В каких-то командах привыкли использовать его в "better Java" стиле, какие-то команды любят и умеют писать на Scala в функциональном стиле.

    Было бы очень странно и бессмысленно всем в мире диктовать "используй от всей Scala только ООП подмножество" или "ты обязан писать на Scala исключительно в функциональном стиле".


    1. sva89
      05.04.2024 15:54

      Насчет строений не согласен.

      Hidden text

      Ну норм же смотрится: St Mary Axe - Google Maps


    1. Spyman
      05.04.2024 15:54
      +1

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


      1. 0x131315
        05.04.2024 15:54

        Даже в пределах одной версии языка на большом моно-проекте в разных частях присутствует разный стиль, т.к. за разные части большого проекта отвечают разные люди, весь проект кому-то одному физически нет возможности охватить, это слишком большой объем информации для человеческого мозга. Разные части еще и написаны в разное время и разными людьми, многих из которых уже нет на проекте. А неизбежная текучка приводит к постепенной утрате экспертизы, и через несколько лет уже не остается тех, кто знает код достаточно хорошо - начинают появляться велосипеды и дублирующая функциональность: люди не знают что уже что-то где-то в недрах кода решено, и пишут свои реализации, и эти реализации множатся. Старый код постепенно начинает отмирать - появляются и растут фрагменты кода, которые больше ни за что не отвечают, не работают, не вызываются, но их просто некому удалить, уже никто уже не может сказать можно ли этот фрагмент трогать, и не поломает ли это что-то, и желающих рисковать нет. Затрудняется поддержка: команда больше не контролирует кодовую базу, кодовая база начинает жить своей жизнью, пухнуть в размерах и сложности. Растет количество ошибок: команда не контролирует код, боится его трогать, никто больше не понимает как он вообще работает, из каких частей состоит - проект превращается в огромный черный ящик. Поддержка такого кода становится настоящей болью для команды, и все меньше и меньше людей хотят в это ввязываться, и деньги тут уже второстепенный фактор, нервы дороже. А еще за эту боль никто не доплачивает: можно уйти на туже или большую зарплату в небольшой молодой проект и забыть про легаси-монстров с огромными монорепозиториями и вечным хаосом, болью и страданиями

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


        1. Spyman
          05.04.2024 15:54

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

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

          Плюс разбиение на части общающиеся через интерфейсы это обычно где-то между дорого и безумно дорого и не всегда производительно, хотя в целом подход хороший)


  1. QValder
    05.04.2024 15:54
    +4

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

    И не будет их читать. Чтобы это работало нужна реализация на уровне компилятора, чтобы можно было указывать в опциях, операторы каких стилей он будет считать ошибками. Тогда руководитель разработки сможет задавать единый стиль, в котором работает команда. Но создание такого компилятора скажем для Scala с учетом размытости границ между стилями очень сложная и неоднозначная задача.


  1. Mox
    05.04.2024 15:54
    +3

    Есть ruby где gem, bundler и все идиоматично, но Python победил.


  1. Anton_Menshov
    05.04.2024 15:54
    +3

    Живу в Монреале и очень его люблю :) Часто программирую на C++ и, в зависимости от проекта, требований, конкретной компании - использую разные подмножества языка. То что, легче - не всегда лучше, и очень часто, "жить в городе с разными-разными районами" классно. Особенно, если во всем городе хотя бы единый стандарт электрической сети :)


  1. mynameco
    05.04.2024 15:54

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


  1. SAPetrovich77
    05.04.2024 15:54
    +4

    Не понимаю как это решит проблему кодовой базы огромного проекта который живёт и развивается на протяжении более 5 лет. Тут либо с выходом например новых фич в языке все на них переписывать, либо на годы похоронить себя (и проект) в парадигме возможностей языка 5..10ти летней давности.


    1. alan008
      05.04.2024 15:54

      Да никто ничего не переписывает. Работает - не трогай. Новые фичи в продукте можно реализовать и в рамках старой "изначально выбранной парадигмы", если всё впишется в неё.


      1. Spyman
        05.04.2024 15:54

        Новые фичи потому и новые, что раньше их небыло. Вот нельзя было раньше в Dart вызывать конструктор без new, а сейчас можно. И у нас либо половина кода так, половина эдак, либо мы не используем новую фичу. А через 5 лет когда вы будете нанимать нового разраба и покажете ему ваш кодстайл - где конструкторы с new, точка с запятой в конце каждой строки, неиспользования лябд и прочие радости - он просто скажет - пойду ка я на другой проект. Сейчас схожая проблема у андроид-проектов которые не перешли на котлин а продолжили писать новый функционал на яве (это конечно новый язык, но сравнить оригинальный c++ и современный - тоже считай новый язык).


      1. ReaderReader
        05.04.2024 15:54

        Не совсем понятно, что имеется в виду под "в рамках старой "изначально выбранной парадигмы", если всё впишется в неё ". Вот, например, раньше в С++ не было default в классах, а потом он появился. Можно его можно использовать, если код написан до того, как эта фича стала поддерживаться? Лямбд раньше не было, а потом появились. Можно их использовать, если весь код усеян std::bind1st, std::std::bind2nd ? Аналогично с auto и вообще со всеми остальными фичами, реализованными в C++ после выхода самой первой версии языка.


    1. ReaderReader
      05.04.2024 15:54

      Именно так. Я даже больше скажу. 5 лет - не так уж и много. У нас в кампании есть С / С++ проект, который стартовал в 1995 году. Разумеется, с тех пор там многое переписано, но есть и немало кода из 90-х годов, т.к. он полностью рабочий и его ни разу не требовалось менять. При этом новые части, разумеется, пишутся с использованием фич из самой свежей версии С++. И что в таком случае предлагает делать автор? Переписать почти весь проект, чтобы он соответствовал последнему стандарту С++ ? Переписать не потому, что там баги, не потому что там спагетти, которое невозможно поддерживать, и т.п., а просто, чтобы все было на С++ 20 ? А с выходом С++ 23 снова все переписать, чтобы была возможность использовать фичи из нового стандарта? И делать так с каждым релизом С++? Внутренний голос говорит мне, что это, ну очень мягко говоря, не самая хорошая идея :)


  1. Kahelman
    05.04.2024 15:54
    +1

    Кто нибудь постановку задачи читал? У нас набор сервисов, над которыми работают 100 человек, грубо 10 человек на сервис = 10 сервисов.

    Зачем нам сервисная архитекутра если мы требуем один язык и один стандарт?

    Пусть каждая команда сама решает. Главное чтобы протокол взамодействия был нормально задокументирован.


    1. 0x131315
      05.04.2024 15:54

      В микросервисной архитектуре все так, да: каждая команда сама решает, без оглядки на остальных, главное чтобы протокол взаимодействия сервисов был документирован и соблюдался. Описанное в посте все же наверное больше проявляется именно в огромных монорепах: в одной части все на коллекциях, в другой на циклах, в третьей вообще рекурсия, а в четвертой автор упоролся в побитовые операции, потому что привык так писать для МК - несколько человек писали в разных стилях, как им было удобно, они свои задачи решили и забыли, а все последующие, стараясь соблюдать существующий стиль, продолжают писать именно в том стиле, который видят в конкретном месте кода, просто потому что проще сделать так, чем связываться с бюрократией и что-то менять. За редкими исключениями: у некоторых все же рано или поздно не выдерживают нервы, и они переписывают какие-то реализации в своем стиле, и таких переписываний туда-сюда за время жизни монорепы может быть несколько штук для одного и того же участка кода.

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