All streams
Search
Write a publication
Pull to refresh
51
0.2
Valentin Nechayev @netch80

Программист (backend/сети)

Send message
> Так вот, разработчики IDE должны разбираться в языке едва ли не больше чем разработчики компилятора.

Справедливости ради, это касается только фронтэнда компиляции — парсинга. Глубины оптимизации, JIT… — это уже не его части.
Kotlin вполне соответствует этому — бэкэнды разные, но эта ниша занята собственно исполняющей VM в JRE, в Javascript машине… И поэтому я сомневаюсь в собственной native реализации — наверняка спихнут на LLVM :)
> Ну как пример — OS/360. Там порядка миллиона строк кода и тысяча человеко-лет. При этом она вполне выполнялась на машинах с 200 тысяч операций в секунду и 256 килобайт памяти. Вы считаете, что это небольшая программа?

Для своего времени (1964) это была очень большая машина. На небольших (с памятью типа 64K) OS/360 могла вообще не идти (были всякие DOS/360 и тому подобные).

> Там только компиляторов пяток был.

Угу. Только вот, например, Fortran G не оптимизировал, а Fortran OE имел столько багов, что непонятно было, как его результат можно вообще использовать. Баги они были и в этой системе.

> Реальные показатели этого проекта «плотность будет равна 0.464 ошибки на 1000 строк кода». Это означает, что в FAR остались баги типа «если залезть на шкаф и высунуть в окно телескоп то в коне дома за 3 километра мы увидим голую задницу», А если на шкаф не залезать — не увидим.

Один баг может создать критическую неработу, а 10000 — не влиять ни на что. При чём тут эти сравнения со шкафом?

> Если вам интересны параметры нашей системы, то там 135 тысяч строк и 10 человеко-лет.

Тут рядом сказали уже, но поддержу — это очень небольшая система, тем более на такое время работы. Даже если считать, что этот объём за половину того времени — то за это время можно было буквально вылизать код.
> Это в принципе неверно, потому что большинство ошибок — алгоритмические.

С сотрудниками той квалификации, которую Вы описываете тут и в одном из предыдущих постингов, где мы общались — возможно, так и есть.
Но это никак не соответствует основной массе программистов. Могу сказать за себя — я периодически делаю такие ошибки, которые выявляются статическими анализаторами. И если бы получить честный опрос всех программистов — таких было бы, думаю, 99%.
Я крайне рад, что Вы исключение.
Потому что все функции pthread API сделаны так, чтобы не пришлось ходить к нему же для получения errno (errno в многонитевом режиме это что-то вроде (*__errno_ptr()), где функция errno_ptr находит thread-local storage (TLS) данной нити и возвращает адрес errno в нём). Коды ошибки универсально возвращаются из функции, 0 означает отсутствие ошибки, другой код — конкретный номер ошибки.
И этот код надо проверять — несоздание нити в случае переполнения чего-нибудь это вполне обычная ситуация. Поэтому он возвращается напрямую, а идентификатор созданной нити — по указателю.
> Байты процессора не обязательно должны совпадать с байтами винчестера

Гм, логично.

> Значит скандал поменьше, с приставками, а не суффиксами, допустим?

Как видим, таки да. Хотя для этого потребовался суд.
> что подтверждение доставки при использовании TCP всё-таки имеет какой-то больший размер, чем просто несколько байт контрольной суммы.

Мнэээ… обычно это IP-пакет общим размером 52 байта, иногда 40 (для стеков без передачи timestamp option). Не «несколько байт контрольной суммы», но и не килобайты. Как ложится на WiFi — пусть лучше соотв. спецы расскажут.
Ну, тут надо уточнить, что за «канал» Вы поднимали на UDP. OpenVPN?
> Есть морские мили и сухопутные, и они отличаются. Важен контекст. В контексте вычислительной техники двоичные приставки (1024) уместнее десятичных (1000).

Вы почему-то думаете тут только об оперативной памяти и тому подобном; но, например, в случае каналов связи есть прямое и грубое столкновение двух традиций от того, что у них параллельно развилось, что «кило» это таки 1000. Например, передача звука в классическом ISDN это 64000 бит/с, а не 65536 бит/с, потому что частота дискретизации — ровно 8 кГц (а не 8192 Гц). Если ещё и понимание частоты менять… тут совсем будет плохо. В интернет-провайдере, где я работал, было в договорах установлено, что килобит = 1024 бита, мегабит = 1000 килобит, гигабит = 1000 мегабит. Выглядит жутко, но это прямые последствия того, что это связь, а не память или тому подобное; и, пока специфицировано и подписано, не вызывало больше вопросов. Но принципиальная проблема остаётся: переиспользование старого «кило» и т.п. под новые реалии создаёт конфликты.

Инициатива IEC по отдельным бинарным префиксам правильна по идее, но плоха по реализации. Ki против K письменно — нет проблем. Но различить «киби» и «гиби» тяжело даже без шума.
Я как-то нарисовал себе предложение по исправлению, думаю, стоит агитировать пошире.

> Хотите играть по-крупному, я подскажу — формально понятие байта нигде не закреплено.

Оно таки закрепляется в каждом конкретном случае. Например,
Intel® 64 and IA-32 Architectures Software Developer’s Manual: «A byte is eight bits». z/Architecture Principles of operation: «An eight-bit unit is called
a byte, which is the basic building block of all information formats.» ISO вместо byte использует octet, и тоже определяет (X.680): «each octet being an ordered sequence of eight bits.» (Наверняка где-то есть и более прямое определение, но и так достаточно для показа.)
«The de-facto standard of eight bits is a convenient power of two permitting the values 0 through 255 for one byte. The international standard IEC 80000-13 codified this common meaning.» (Википедия, Byte.)

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

> Никто не мешает вам прям сейчас удвоить ценник, формально не обманывая покупателей.

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

Как уже сказал, считаю это только вопросом терминологии. Хотите называть это ошибкой — не возражаю.

> то стандарт фортрана допускал реализацию с копированием по значению-результату. То есть одно копирование — на входе в функцию. И, если надо — второе, на выходе.

Хм, интересно, такого не видел. Но вот требование копирования обратно тут 1) не соответствует более новым традициям, 2) и непонятно, как называть — «по ссылке» уже не совсем корректно. В любом случае, я рад, что в новых языках этого нет, а где есть — есть и тенденция явно обозначать такое (var в Pascal, но ещё лучше ref и out в C#, где надо его указывать и в списке вызова).
> перед переходом на TDD покрываем существующую кодовую базу тестами

Это никак не относится к тому, что я говорил — о проблемах работы с существующим кодом, уже покрытом тестами, согласно TDD.

> Вы при описании TDD забыли ещё один этап при разработке — рефакторинг. Замена пузырька или вставок на Бэтчера — это как раз и он есть: видимое поведение не изменяется, а код оптимизируется по какой-то метрике.

Это не к тому, что я описывал. Во-первых, Вы почему-то предположили, что какая-то сортировка уже была. Ну ладно, примем. Тогда, при буквальном следовании TDD данный вид рефакторинга просто невозможен — его не допустит догма.
Property-based это само по себе сложно. Мы пробовали (на PropEr). В большинстве случаев получалось, что описать необходимое поведение отдельной функции в тех терминах, что ему нужно, во много раз сложнее, чем написать саму функцию. ;(
А вот что в нём хорошо — поиск маргинальных случаев. Но можно и не дождаться.
> Что у него будет в TDD? Да ровно то же, что и в коде. В тестах он тоже решит, что «не меньше — это больше».

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

И Ваше правило

> Никогда не тестировать свой собственный код.

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

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

> Тесты (что железа, что софта) бывают двух видов. Одни — чтобы доказать, что ошибок нет. Другие — чтобы найти ошибки. TDD дает лишь тесты первого рода.

Полностью согласен, но я бы переформулировал. Слово «доказать» некорректно — разве что «подтвердить», «подкрепить уверенность». Доказательство возникает только в результате верификации, пусть и ручной (глазной? не знаю, как лучше сказать), тесты же обеспечивают выполнение условий этой верификации (рядом уже писал то же более сжатой формулировкой).

> оценка работы программеров — по тому, чтобы багов было меньше и самой малой значимости.

И они в этих условиях сами себя не тестируют? Что-то я сильно сомневаюсь. Может, тестируют, но подпольно? :)

> Но массовое использование TDD, где надо и где не надо — это карго-культ.

+100.
При обсуждении TDD надо отделить мухи от котлет и принципы, лежащие в его основе, от их конкретной комбинации. У меня лично претензии именно к конкретной комбинации (ну и, естественно, к доведению её до статуса религии).

Первое: тут уже упоминалось, но повторю: формально TDD никак не требует собственно писать по ТЗ, оно требует только удовлетворения теста. Поэтому, если мы напишем умножение в виде

multiply(0, 0) -> 0;
multiply(2, 0) -> 0;
multiply(2, 2) -> 4.

и никто не проверит, что работа вообще не делается, будет полное формальное соответствие, которое никому не нужно.

Для объяснения этого вообще нужно вернуться к тому, зачем же тест нужен. Тут лучшая формулировка, что я слышал, дана коллегой landerhigh с RSDN: «Тесты — не для поиска багов, а для написания верифицируемого в контролируемых условиях кода.» Под верификацией имеется в виду любая верификация, начиная с банального просмотра глазами. Код должен обеспечивать своим содержимым выполнение ТЗ так, чтобы это могли проверить средства верификации (сам автор, коллеги-ревьюеры, автоматические средства...), а тест — чтобы ловить то, что верификация не ловит: человек не заметил опечатку или крайний случай; вместо символа ';' шутник подставил ';' (U+037E); не выловлен какой-то крайний случай; и тому подобное.

Далее, в классическом TDD объявляется, что
1) пишутся тесты до кода («test first»);
2) тесты должны быть проверены на то, что они не проходят (тупо — запустили, увидели отказ и только после этого имеем право двигаться дальше);
3) пишется код для их удовлетворения.

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

Чтобы допустить в принципе ситуацию, когда код меняется, надо допустить, если использовать правило «test first», что новые тесты соответствуют TDD, но старые — нет, часть тестов может работать и до новой разработки, и после. А это значит, что они уже нарушают этот принцип — их нельзя проверить на корректность той проверкой, что «тест до написания упал => он, вроде, правильный».

Поэтому, или вы доверяете корректности уже существующих тестов согласно их коду, но тогда правило «сначала тест должен упасть» вообще теряет смысл, или вы вводите другие методы контроля корректности тестов (в моём случае это инверсии тестов, когда проверяется нарушение выходных данных при заведомых отклонениях входных данных). А в этом случае уже пофиг, были написаны тесты до или после: это вопрос административного контроля качества покрытия тестами, но не собственно их выполнения.

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

Пригодный к длительному сопровождению (а не к одиночному «хренак, отдали заказчику и забыли») тестовый комплект должен быть таким, чтобы корректность самого тестирования можно было проверить автоматизированно в любой момент без ручного вмешательства. Например, мы вводили инверсии тестов — аналог «мутационного тестирования» именно для кода тестов, в явно продуманных местах. Общая идея: пусть тест ожидает, что если на входе A, на выходе B. Модифицируем код посылки на вход так, чтобы посылалось C, но так, что верхний уровень контроля теста ничего про это не знает. Тест должен упасть (а вокруг него строится другой, который проверяет факт падения вложенного теста). Ошибка инверсного теста у нас срабатывала очень редко, но каждый такой случай означал серьёзные скрытые проблемы в коде.

И с такими средствами исходный принцип TDD про «сначала тест должен упасть» теряет свой методический смысл: что прямой тест работает, а инверсный падает — проверяется в любой момент независимо от того, был ли написан тест раньше кода, или нет.

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

Вы вначале решите, что должен пройти тест для (1,2) и (2,1). OK, сравнили два элемента, прошли. Теперь добавляем третий… четвёртый… двадцатый… во что превратилась функция? В лучшем случае вы получите сортировку вставками (если кодер очень умён), а скорее всего это будет «пузырёк». При совсем тупом кодере это вообще не будет читаемо даже после ста грамм. И Вы никак не получите ни метод Хоара, ни тем более метод Бэтчера. Потому что для них надо сначала реализовать алгоритм, хотя бы в уме, со всеми циклами, ветвлениями и тому подобным, а уже затем тестировать.
Можно сказать тут, конечно, что алгоритм в уме и алгоритм в коде — разные. Но когда вы заранее знаете, какое именно ветвление вы напишете и почему — вы уже действуете не под тест, а под алгоритм. Идея предварительного разделения на два подмассива, как у Хоара, а тем более математически подобранная сортировка подпоследовательностей, как у Бэтчера — тестом не решится.

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

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

В каком же случае TDD может идеально работать, и даже быть полезным, в его каноническом виде? Это
1) написание только нового кода, или расширение на безусловно новые требования;
2) полное отсутствие необходимости R&D, весь проект ясен с самого начала (сюда входит и вариант изменения ТЗ на ходу — просто таких начал становится несколько);
3) большое количество низкоквалифицированных сотрудников и аналогичного управления ими, которое способно сэкономить на тестах, но для которого угроза административного наказания за нарушение инструкций важнее проблемы собственно качества выходного кода.

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

Резюмирую: я никак не против отдельных функциональных тестов всех уровней (от юнит- и до интеграционных верхнего уровня). Они должны быть. Более того, они должны быть и для нормального заказчика (который обязан не верить, пока не покажут работающее), и для собственного контроля. И разработчик должен сам писать тесты на все подозрительные и крайние случаи, и контроль должен это поощрять и требовать. Но требование 100% покрытия и «test first» — это то, что превращает разумный подход в религию.

И ещё один PS — речь не про тесты для прямой проверки ТЗ (я их отношу к BDD, а не TDD).

Чуть сумбурно получилось, но, надеюсь, понятно.
> Потому что return — это таки сахар поверх goto с гарантиями

return только частный не самый интересный для обсуждения случай. Интереснее с break. До сих пор масса языков позволяет им выйти только с самого нижнего уровня. Пример Java, где можно даже выйти из тела именованного if, до сих пор остаётся приятным исключением.

> Поэтому, например, писать руками код с goto в 2017 году на каких-нибудь даже плюсах я особых причин не вижу.

На плюсах — тот же выход из множества вложенных циклов (когда неудобно порождать отдельную функцию; хотя с появлением лямбд стало значительно проще делать их эффективно). На C — объединение веток очистки за собой является стандартнейшим и полезнейшим приёмом (альтернатива чудовищно хрупка).
> Изменение ТЗ — это исправление ошибок.

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

> на случай атомной войны планировалась автономность до года

Р-радикально. :) Впрочем, тогда и не такие взлёты мысли были. И устройство было упрощено до предела. Не думаю, что кто-то из современных конструкторов сейчас возьмётся повторять этот подвиг в варианте с хоть каким-то расчётом на более современные средства, а не уровня древней Спарты.

> Ну тоже примерно так. Ну может 5-7, а не 3, но примерно так…

Ook.

> Во втором варианте проще вставить дополнительные операции между вызовом процедуры и использованием её результата.

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

> По ссылке все-таки, не по указателю. Указателей на данные там вообще не было.

Это я переупростил для ясности. С точки зрения C++-like терминологии это таки ссылка, даже если внутри чистейший указатель.
Тритовые — тем, что будут умножать и делить на 2, а не на 3.
А ещё есть вопрос операций типа count leading (trailing) zeros (sign zeros, etc.). Аналог сохранится, но кроме него надо будет ещё другие варианты продумывать.
Для этого нужен следующий шаг — увеличение количества миллибит в бите. Увеличение всего на 2.4% (1000 -> 1024) позволит повысить плотность упаковки картинки на 4-8%. Скорость на каналах, понятно, повысится во столько же раз.
Так Вы тогда и спецификацию Си прочитайте. В C11 (final draft) — пункт 6.2.6.2. Достаточно объёмная цитата, форматирование я уложил в плоское:

=== cut here ===
For unsigned integer types other than unsigned char, the bits of the object
representation shall be divided into two groups: value bits and padding bits (there need not be any of the latter). If there are N value bits, each bit shall represent a different power of 2 between 1 and 2, so that objects of that type shall be capable of representing values from 0 to 2**N − 1 using a pure binary representation; this shall be known as the value representation. The values of any padding bits are unspecified.

For signed integer types, the bits of the object representation shall be divided into three groups: value bits, padding bits, and the sign bit. There need not be any padding bits; signed char shall not have any padding bits. There shall be exactly one sign bit. Each bit that is a value bit shall have the same value as the same bit in the object representation of the corresponding unsigned type (if there are M value bits in the signed
type and N in the unsigned type, then M ≤ N ). If the sign bit is zero, it shall not affect the resulting value. If the sign bit is one, the value shall be modified in one of the following ways:
— the corresponding value with sign bit 0 is negated (sign and magnitude);
— the sign bit has the value −(2**M ) (two’s complement);
— the sign bit has the value −(2**(M − 1)) (ones’ complement).
Which of these applies is implementation-defined, as is whether the value with sign bit 1 and all value bits zero (for the first two), or with sign bit and all value bits 1 (for ones’ complement), is a trap representation or a normal value. In the case of sign and magnitude and ones’ complement, if this representation is a normal value it is called a negative zero.
=== end cut ===

То есть свобода по сравнению с Java, да, есть — нежёсткие размеры, возможность signed быть короче соответствующих им unsigned, и выбор метода представления отрицательных чисел не только дополнительным кодом. Но двоичность — в полный рост, слово bit(s) во всех определениях, и никакой троичной альтернативы нет.

> Понятно что можно запустить виртуальную машину на чём угодно, но вот скорость у этого будет…

И то же самое для Си. Чтобы получить язык, удобно переносимый на троичную логику, надо что-то ближе к Ada, где основные рабочие типы задаются диапазонами. И особенно надо тщательно продумать, что будет с некоторыми операциями вроде битовых сдвигов, count_leading_zeros…
А отдельной подсистемы DOS — нету, она считается «Win16 console».

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


в консольной DOS-программе под Windows NT можно использовать то, что было в WIn16, но отсутствовало в DOS. Например — обращаться к сетевым файлам.

Ну там очень много чего можно было использовать через спец. интерфейсы. Например, то, что запускалось через DOS4G[W] и тому подобные экстендеры, под Win95 запускалось нативно (а даже если программа просила экстендер, тот просто "встраивал" свои действия в Windows). Для сети Win95 эмулировала SMB интерфейсы стиля Lantastic и аналогов. Но если это не выглядело Win бинарником, а был просто досовским .exe — то прямой путь к Windows-интерфейсам был недоступен (можно было требовать её dllʼки, но там требовалась особая осторожность).
Вообще, этот слой там чуть ли не высшее произведение инженерной осторожности за всю историю осестроения — заслуживает отдельного рассказа (увы, я знаю оттуда дай бог чтобы 2%, и не возьмусь).

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

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


100% не предусмотришь, но 95% — вполне.

В 95% мы вполне вкладывались. Оставшихся 5% хватало, чтобы последствия обсуждать ещё много лет. :) И Туполеву наверняка не ставили задачи в виде "самолёт должен летать год без посадки".


И я об этом же. Если система к движению в какую-то сторону не готова — это ошибка аналитика. Или команда программистов вообще не видит дальше спринта (а надо смотреть на 5-10 лет вперед).

Реально получалось видеть с достаточным качеством на год, в общих чертах — на три.


double V = GetV());
printf("V=%.1f\n",V);
Но второй вариант проще и в отладке и в модификации.

Ну, опустим, что они неравнозначны (вот в варианте

printf("V=%.1f\n", (double) GetV())
есть действительно полное соответствие). Если отладка это распечатать V и/или не спутать пошаговый проход GetV() с printf() — да, согласен.
Но с модификацией связи не вижу. Наоборот, первый кажется проще для модификации — за счёт отсутствия лишних сущностей. Ту же переменную V надо проследить, чтобы её значение нигде дальше не использовалось. Вот если бы был оператор типа unvar (del в Python), чтобы поставить после printf и после этого чтобы V была неизвестна — тогда не надо было бы смотреть вниз.


Указателей в алголе-60 и фортране-IV вроде не было (или не было в тех реализациях, с которыми я работал).

В Фортране IV всё передавалось по указателю. Передача по значению появилась позже. От этого возникал ряд неприятных эффектов. Но я понял идею этого пункта, спасибо.

Information

Rating
2,695-th
Location
Киев, Киевская обл., Украина
Date of birth
Registered
Activity