Pull to refresh

Comments 35

PinnedPinned comments

По пунктам:

декремент - да, не нужен (и во многих новых языках его нет)

обмен значений - a, b = b, a. В таком реализован много где, да и читается легко.

нулевой указатель - ситуация не так проста. С одной стороны да, это "ошибка на миллиард долларов". А с другой - нужен способ выразить что в ячейке "еще" или "уже" нет никакого значения. Если просто убрать null, то вместо него будут использовать пустой объект или еще какую-то непредсказуемую заглушку и станет только хуже (с null, по крайней мере, значение можно сравнить). Современный подход к решению - nullable типы у которых надо явно запросить перед использованием - null там или не null.

результат последней операции. Скорее вреден. Каждый раз в чужом коде этот оператор будет вызывать маленький шок - что это, надо найти откуда это значение пришло. Особенно в сложном коде (а в простом им и пользоваться не будут). И очень легко сломает код, например если для отладки воткнуть в цикл отладочный вывод который и станет последним оператором. И хорошо если тип отличается и компилятор сразу ругнется. А если нет - счастливой отладки.

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

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

Большое спасибо за развернутый комментарий!

обмен значений - a, b = b, a. В таком реализован много где, да и читается легко.

Точно, я на самом деле забыл про эту конструкцию. Спасибо, что напомнили.

нулевой указатель - ситуация не так проста.

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

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

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

Результат выполнения последней операции - это как? Вот с точки зрения архитектуры?

Это регистр флагов ))

CMP EAX, EBX
JZ EQUAL

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

Да, я про это сразу написал (машина Тьюринга). Вот только работать с таким кодом будет очень тяжело.

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

*val++ - это от ориентированности С на систему адресации PDP-11. Просто ++ и -- видимо вошли в ЯП уже автоматом в нагрузку.

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

Добро пожаловать в Go. :)

"оператор присвоения значения" =

"оператор создания переменной" :=

По пунктам:

декремент - да, не нужен (и во многих новых языках его нет)

обмен значений - a, b = b, a. В таком реализован много где, да и читается легко.

нулевой указатель - ситуация не так проста. С одной стороны да, это "ошибка на миллиард долларов". А с другой - нужен способ выразить что в ячейке "еще" или "уже" нет никакого значения. Если просто убрать null, то вместо него будут использовать пустой объект или еще какую-то непредсказуемую заглушку и станет только хуже (с null, по крайней мере, значение можно сравнить). Современный подход к решению - nullable типы у которых надо явно запросить перед использованием - null там или не null.

результат последней операции. Скорее вреден. Каждый раз в чужом коде этот оператор будет вызывать маленький шок - что это, надо найти откуда это значение пришло. Особенно в сложном коде (а в простом им и пользоваться не будут). И очень легко сломает код, например если для отладки воткнуть в цикл отладочный вывод который и станет последним оператором. И хорошо если тип отличается и компилятор сразу ругнется. А если нет - счастливой отладки.

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

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

Большое спасибо за развернутый комментарий!

обмен значений - a, b = b, a. В таком реализован много где, да и читается легко.

Точно, я на самом деле забыл про эту конструкцию. Спасибо, что напомнили.

нулевой указатель - ситуация не так проста.

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

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

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

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

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

Нет не проигнорировал. Это вы не поняли что означает "компилятор не контролирует работу с ссылками". Это вовсе не "nullable типы у которых надо явно запросить перед использованием - null там или не null", а полный контроль компилятора над ссылками без какой либо адресной арифметики на уровне синтаксиса языка, в том числе и с управлением владения объектами как в Rust Управление памятью и разделяемыми ресурсами без ошибок

Так и ззнал, что раст всплывет... Где в том абзаце про адресную арифметику? По секрету: там про пустой объект и заглушку.

Я не являюсь апологетом Rust и не пишу на нем, но мне нравится его концепция владения объектами.

А адресная арифметика, это не только арифметические операции с адресами, но и операции сравнения. Поэтому, сравнение адреса с null, это тоже часть адресной арифметики.

Есть subleq, например – язык программирования из одной команды и собственно название этой команды

Полагаю, что тема близка к UTP:

Вообще есть такая тема, как Unifying Theories of Programming (UTP), где пытаются собрать все абстракции, которые нужны для описания любых программ. В принципе это близко к Лексикону программирования.

https://forum.oberoncore.ru/viewtopic.php?p=104352

Если еще "глубже копнуть", то должно быть триединство: код (скрипт) - алгебраическая запись - схема. Т.е. программа должна иметь возможность трансляции в какое-либо мат. исчисление: CCS\ pi-исчисление, только более совершенную алгебру процессов.

"Левое" движение к этому (код.скрипт - алгебраическая запись): функциональное программирование http://intsys.msu.ru/staff/mironov/thfunprog.pdf

"Правое" движение (алгебраическая запись - схема): BPMN (pi-исчисление), s-BPM (CCS) http://workflowpatterns.com/documentation/documents/pi-hype.pdf

Все уже придумано до нас. Минимальный набор реализован в языке Оберон. Все остальное - ненужная реализация фантазий и хотелок "я хотел бы вот эту возможность".

Прочитал статью.

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

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

Результат выполнения последней операции - это как? Вот с точки зрения архитектуры?
Я выполнил ++, какой должен быть результат? Я положил значение в стек - какой должен быть результат?
Если я завершил цикл, в котором что-то считал, результат должен быть то что я считал, или счетчик цикла? А если цикл не for а while? а если вышел через break, должен ли результат быть отличным от выхода через завершение цикла по done?

И какие затраты будут, если на КАЖДУЮ ОПЕРАЦИЮ нужно хранить результата?

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

Спасибо за развернутый комментарий!

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

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

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

Результат выполнения последней операции - это как? Вот с точки зрения архитектуры?

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

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

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

Почему?

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

Ничего не понял. Компилятор не выполняет код, он его компилирует. Зачем вам переменная, которая есть только во время компиляции.

Или вам отладчик нужен? Так они есть.

Скорость работы программы бывает важна только в том случае, если программа работает правильно. А язык программирования должен помогать писать правильно работающие программы. И в подавляющем числу случаев программисту ненужен не только доступ к низкоуровневому коду, но ему будет все равно, как это работает на нижнем уровне.

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

Почему?

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

Ничего не понял. Компилятор не выполняет код, он его компилирует. Зачем вам переменная, которая есть только во время компиляции.
Или вам отладчик нужен? Так они есть.

Генерация машинного кода компилятором, это самое последние действие, которое он делает при компиляции исходного текст программа. А до этого происходит очень много предварительных этапов обработки исходных текстов. Чтобы лучше понять, почитайте теорию.

Скорость работы программы бывает важна только в том случае, если программа работает правильно.

Скорость работы важна тогда, когда код должен выполняться быстро и эффективно. Сжатие, шифрование, рендеринг.
Правильно и быстро это вообще не рядом.

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

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

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

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

Почему?

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

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

Генерация машинного кода компилятором, это самое последние действие, которое он делает при компиляции исходного текст программа. А до этого происходит очень много предварительных этапов обработки исходных текстов. Чтобы лучше понять, почитайте теорию.

Вы ушли от вопроса непонятной фразой полной воды. Зачем вам в языке программирования иметь переменную с результатом выполнения предыдущей операции. каждой предыдущей операции

Результат выполнения последней операции - это как? Вот с точки зрения архитектуры?

Это регистр флагов ))

CMP EAX, EBX
JZ EQUAL

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

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

CMP EAX, EBX
MOV eax,eax
JZ EQUAL

На результат какой команды среагирует JZ, если MOV не меняет флаг четности?

Кроме этого, при наличии адресной арифметики (явной или не явной), сразу становится необходимым использование специального зарезервированного значения под названием "нулевой указатель"

А как из первого вытекает второе?

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

Так а арифметика указателей тут причём? Указатель может быть невалидным и при этом не быть null

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

операция сравнения, это адресная арифметика.

Пожалейте сову. Object в Java тоже можно сравнивать на (не)равенство. Это не значит, что на Object есть арифметика.

И что вас тут рассмешило, ваше незнание?

Адресная арифметика (address arithmetic) — это способ вычисления адреса какого-либо объекта при помощи арифметических операций над указателями, а также использование указателей в операциях сравнения.

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

В классических managed-языках, типа java, считается что нет адресной арифметики, но при этом есть сравнение ссылок на равенство.

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

А в Java и на самом деле адресной арифметики нет. Так как единственный используемый в языке указатель this, это скорее не "указатель на объект", а "объект, где в качестве уникального идентификатора использован указатель".

Вопрос конечно терминологический, и поэтому каждый останется при своём мнении, но я к адресной арифметике отношу

  1. Добавление константы к указателю

  2. Вычитание указателей как операция расстояния между ними

  3. Сравение указателей на "больше-меньше"

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

Так я уже написал, что указатель this в Java, это не адрес, а идентификатор объекта, в качестве которого выбран адрес, то есть в Java не просто "конвенционально считается", а там действительно "нет адресной арифметики".

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

Можно синтаксически запретить неинициализированные указатели. Например, как ссылки в C++. Или объявить, что чтение неинициализированного указателя это UB. А какое там значение - null, или любой другой мусор - для спецификации языка не имеет значения.

И чем вам инкремент не угодил...

Вот у меня есть такой метод в классе:

    uint8_t read_byte()
    {
        return buffer[buffer_pos++];
    }

Если отказаться от инкремента, тогда придётся этот метод переписать так:

    uint8_t read_byte()
    {
        uint8_t result = buffer[buffer_pos];
        buffer_pos += 1;
        return result;
    }

Неужели, по вашему, стало понятнее?

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

Sign up to leave a comment.

Articles