Pull to refresh
193
0.6
Alexander Pevzner @apevzner

Программист на все руки

Send message

Повтор сигнатуры функции, впрочем, слабо тянет на «кучу» копипаста — но это всё ещё копипаст и всё ещё два файла вместо одной строчки

Ну вот да, На кучу как-то не тянет.

Два файла - согласен. Но я не уверен, что лучше, несколько файлов или длинная сопля #ifdef-ов. Особенно когда вариантов больше, чем два.

В реальном мире, в котором программисты любого уровня гениальности постоянно совершают ошибки — не кажется; RAII выглядит разумным компромиссом между «естественностью» и надёжностью/безопасностью

А чем new/delete лучше open/close?

Как конкретно написать?

umask_linux.go:

func Umask(m int) {
    syscall.Umask(m)
}

umask_windows.go:

func Umask(m int) {
}

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

А Вам не кажется, что управлять временем жизни файла как-то естественнее в терминах операций с файлом, чем в терминах создания/удаления переменной?

Кроме того, закрытие файла в Go имеет полезный side-effect: оно разблокирует все заблокированные операции ввода-вывода, связанные с этим файлом (который может быть и сетевым сокетом, не обязательно именно дисковым файлом), сохраняя при этом сам объект корректным для обращений.

Давайте рассмотрим реальный пример из моей практики — Umask. В юниксах его надо вызвать, в винде его не существует и соответственно можно не делать ничего. Какой максимально компактный кроссплатформенный код для этого можно написать?

Напишите функцию с прототипом, как у Umask, которая для линуха вызтвает syscall.Umask, а для венды не делает ничего.

И снова — одним лишь C++ мир не ограничивается, есть как минимум один язык, в котором без захвата мьютекса вы в принципе не сможете получить доступ к данным

Ну, ОК. Я ссылаюсь на C++ не потому, что это эталон языка, а потому, что ссылка на C++ будет понятна большинству участников дискуссии.

Это хуже. Компилятор не может знать, какие файлы нужно открывать (поэтому это приходится прописывать вручную в коде) — зато он прекрасно знает, какие файлы нужно закрывать

Ну, нет. Закрытие файла - это действие с глобальными side-effect-ами. Например, оно может позволить другой программе файл открыть, если используется mandatory locking, обеспечит запись недописанных данных на диск и т.п.

Откуда бы компилятору знать, когда файл уместно закрывать? Эту логику почти всегда приходится как-то явно выражать, и явный open/close - не самый плохой вариант.

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

Например, в Go три системы типов: отдельно для констант, отдельно для конкретных типов и отдельно для интерфейсов. Это достаточно сложно объяснить языком спецификации с математической точностью. А вот пользоваться - удобно, приятно и соответствует интуитивным ожиданиям.

Всё ещё неудобно: какой-нибудь один платформозависимый вызов, который можно было бы запихнуть в один if, приходится размазывать как минимум на два файла с кучей копипаста

Зачем копипаста? Сделайте функцию (или тип), которые делают то, что вам надо, платформенно-зависимым способом с платформенно-независимым интерфейсом. Только унесите туда лишь необходимый функциональный минимум. И копипасты у вас практически не будет.

Что, разумеется, тоже показывает Go далеко не с лучшей стороны

Дело вкуса. Не вижу смысла об этом спорить.

Который очень удобно забывать прописывать? Лучше уж RAII

А если вы со своим RAII забудете мьютекс не отпустить, а захватить?

Вы хотите try-finally

Я не хочу try-finally

И это плохо. Программист не должен тратить своё драгоценное время на делание того, что может сделать сам компилятор (опять же RAII)

Это настолько же плохо, насколько плоха необходимость открывать файлы перед использованием. Возможно и плохо, но обусловлено практическими соображениями.

Да, архитектурная проблема в языке, который в принципе позволяет смешивать несмешиваемое

Объявить их строками разных типов, и Go не позволит вам их смешивать.

Вы так говорите, как будто это примеры чего-то высококачественного)

Да, это примеры чегото высококачественного.

Стоп, что? Почему err ещё раз используется в foo2()? Может, я не вижу каких-то деталей? Даже если изменить оператор на :=, мы не поймём, почему err находится в области действия в течение (потенциально) всей остальной части функции. Почему? Может, она считывается позже?

Потому, что перед первым if-ом она была объявлена как "более глобальная". В C++ будет абсолютно то же самое.

Если сократить эту область, код будет намного проще читать. Вот только синтаксис Go этого не позволяет.

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

«А твой nil какого цвета?», — ошибка на два миллиарда долларов.

Да, увы. Нетипизованный nil не равен типизованному.

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

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

У Go одна из лучших в индустрии поддержка кросс-платформенности. Во-первых, многие вещи, которые в C/C++ потребуют условной компиляции, в Go будут работать совершенно единообразно на любой платформе, а все сложности скрыты под капотом стандартной библиотеки и снаружи не видны. Во-вторых, в Go ну прям ОЧЕНЬ хорошая поддержка кросс-компиляции.

Фактически, Вы можете предъявить только то, что в Go build target оформляется, как комментарий специального вида, и не имеет прямой поддержки в синтаксисе языка.

Но во-первых, это придирка, а во-вторых, Вы забыли про то, что можно управлять условной компиляцией с помощью имени файла (foo_linux.go - файл только для Linux, foo_windows.go - файл предназначен только для венды и т.п.)

append без определения владения

append сделан в Go очень по-Сишному. Любому человеку с опытом программирования на Си он очень понятен и интуитивен.

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

Да, реализация append в Go может быть не очень интуитивно понятна, скажем, человеку, привыкшему к Питону. Но в реальной жизни это редко доставляет проблемы.

defer — это тупость

Отличный механизм. Если Вам надо отложить какое-то действие на конец функции, у вас есть для этого готовый синтаксис. И использование его гораздо более прямолинейно, чем RAII, который полагается на неявный результат работы деструктора объекта, выходящего из области видимости.

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

Это аналог сишного atexit, но на более локальном уровне. И такого механизма ОЧЕНЬ не хватает в C.

Очень тупо. Одни ресурсы необходимо уничтожать с помощью defer, другие нет. Поди разберись.

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

Но вы же не требуете, чтобы обращение к файлам всегда шло по имени, а не по дескриптору (write("hello.txt", "hello, world!"))

Стандартная библиотека «глотает» исключения, так что выхода нет...

Да, http глотает паники вместо того, чтобы разматывать их. Я считаю это одной из немногих ошибок проектирования стандартной библиотеки.

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

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

Иногда формат данных не UTF-8
А может, при разработке языка просто нужно тщательнее продумывать его структуру? Может, надо изначально делать правильно, а не создавать простые механизмы явно кривыми?

А как правильно? Завести 100500 типов для строк?

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

Согласен, в 90% случаев всё работает прекрасно. Но в остальных нет.

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

При создании этого языка уже были известны более удачные примеры. И что в итоге? А в итоге мы вынуждены работать с кривыми кодовыми базами Go.

Этот язык создали не юные пионеры, а люди, которые создали UNIX, C, namespaces, ...

Наверное, они подумали над своими решениями.

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

Я понял, больше не буду никого благодарить — всем от этого только плохо становится …

Плохо не от благодарности, а от отсутствие чувства меры.

Это как сахар в кофе. 2 ложки - норм, 10 - пить невозможно.

Ну, ждал от кого-нибудь такого ответа.

Поверьте, это ОЧЕНЬ удобно в большом проекте, когда разные сущности, имеющие отношение к одной и той же вещи, названы консистентно между собой.

Да, это хороший вариант.

А не знаете, у него есть автор, или "музыка народная, слова тоже мои"?

Для процессора код — это абсолютно детерминированный поток инструкций, операций с ячейками памяти

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

Недавний шум на тему Meltdown and Spectre - это отзвуки этой проблемы, но сама по себе проблема фундаментальна для современных вычислительных устройств.

Вы пишете, что «ребята не писали умных статей, они это просто сделали». И это правда. Но кто были эти люди?

Ну, я примерно это и сказал. И хотел обратить ваше внимание, что вот, такой опыт существует и успешно используется.

Стиль gofmt не был выбран случайно: это кодификация интуиции гениев.

Стиль gofmt не является чем-то волшебным и время от времени меняется.

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

Более того, при сравнении gofmt и rustfmt

Я думаю, что rustfmt родился под прямым влиянием gofmt.

Как, например, Plan9, исследовательская операционная система от тех же людей. Сама по себе не так уж и широко используется, но procfs, sysfs и Linux namespaces - прямое продолжение заложенных в ней идей.

Я отвечал практическим примером на ваши же слова:

Ведь главный вопрос — как создать такой стандарт, который не будет отторгаться командой, а станет ее «второй натурой»? Простые линтеры проверяют синтаксис, но не семантику.

Моя цель была - ознакомить вас с весьма удачным примером a-priory work в той области, которой вы собираетесь заниматься.

Это не я придумал. Это стандартная практика в мире Go. Любой редактор, который понимает Go на уровне подсветки синтаксиса, заодно принудительно его форматирует.

Gofmt's style is no one's favorite, yet gofmt is everyone's favorite.

Это в том смысле, что никому не нравится сам по себе стиль gofmt, но всем нравится иметь единый стиль.

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

Эти ребята не писали на эту тему умных статей, они это просто сделали (но надо понимать, что это не просто ребята, а те самые ребята, которые придумали UNIX, C и заложили основу контейнеров и много чего еще).

И получается, что нет нужды придумывать некий идеальный стиль, который понравится всем. Ценность возможностi использовать всемирно-общий стиль оправдывает неудобства индивидуальной подстройки под него каждого разработчика.

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

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

А что, нельзя было прогнать линтер локально и до более долгосрочных этапов сборки?

Но вообще все ИМХО, главное чтобы был описанный стандарт, принятый в команде и все его придерживались.

А еще лучше - инструментарий, который мягко но настойчиво навязывает этот стандарт.

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

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

В хорошо написанной программе присутствует система наименования вещей, и она консистентна. Удобно, когда однородным объектам даются однородные имена. И, скажем, если я вижу класс по имени DeviceLocator, я вправе ожидать, что описан он будет в файле по имени devicelocator.xxx, а не discoveryservice.xxx - иначе как мне искать его реализацию среди нескольких сотен файлов?

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

Всё это не упрощает выбор имен для сущностей.

В вашей статье есть один существенный недостаток: вы рассуждаете о наименовании понятий ("ошибка Гейзенберга"), но сказать пытаетесь о наименовании переменных и функций.

Никто не назовёт свой класс godObject или свою функцию SpaghettiCode (хотя с радостью используют эти слова, чтобы охарактеризовать, но не назвать, какое-либо место в программе).

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

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

Собственно, корпоративный стиль управления старается человека от этого бремени самостоятельного мышления избавить, сведя его деятельность к следованию процессу а оценку успешности - к выполнению KPI. Это предсказуемо и позволяет избавиться от такой ненадёжной штуки, как способности (да и вообще, личности) конкретного человека. Отсюда и берётся упомянутое выше лечение анализов, а не людей.

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

Будет обидно, если под давлением менеджеров и ИИ мы этот уровень потеряем или маргинализируем. Потому что в итоге мы одичаем и вернёмся в пещеры (где не будет ни ИИ ни корпораций, так что история пойдёт по кругу).

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

Вы не с той стороны заходите.

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

А то получится в итоге, "что выросло, то выросло".

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

Если предполагается какой-то из вариантов отложенной доставки событий, каковы гарантии в плане порядка их доставки?

Итоговая модель, которая у вас получится, понятна ли она программистам, насколько сложно её описать, удобно ли ей пользоваться?

Только зная ответы на эти вопросы можно решать, как внутри себя должна быть устроена реализация.

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

Information

Rating
1,953-rd
Location
Москва, Москва и Московская обл., Россия
Registered
Activity

Specialization

System Software Engineer, Software Architect
Lead