Каким должен быть язык программирования? Анализ и критика Описание языка Компилятор
Отечественные разработки Cтатьи на компьютерные темы Компьютерный юмор Новости и прочее

Заметки о выходе из функции без значения и зеркальности get и put

Выход из функции без значения

            Язык PL/1 имеет недостаток в слабом различии подпрограмм и функций (в терминах языка это процедуры и процедуры-функции). Процедура-функция должна возвращать значение через оператор RETURN(значение), а просто процедура может иметь выход через RETURN без параметра. При этом в процедуре необязательно всегда ставить явный RETURN, достаточно просто «добраться» до последнего оператора этой процедуры.

            Но такой подход приводит к тому, что и в процедуре-функции можно добраться до последнего оператора, не используя RETURN, и, тем самым, выйти без значения. Конечно, я вставил в компилятор проверку, что тело процедуры-функции содержит хотя бы один оператор RETURNS, и если это не так — в последней строке текста процедуры-функции выдается ошибка «БЕЗ ОТВЕТА». Но, разумеется, это неполная проверка, например, правильно работающая процедура функция F может оканчиваться так:
. . .
IF A>B THEN RETURN(A); ELSE RETURN(B);
END F;
 а неправильная — так:
. . .
IF A>B THEN RETURN(A); // ELSE RETURN(B);
END F;
            И тут пришло в голову использовать для проверки место в компиляторе, где выбрасываются команды, на которые невозможно передать управление. Реализовано это очень просто: метки, как написанные программистом, так и расставленные самим компилятором имеют такой же внутренний формат, как и обычные уже сгенерированные команды процессора. Когда оптимизатор анализирует связанный список сформированных команд и находит команды безусловного перехода или возврата, он проверяет, что следующая «команда» — обязательно внешняя или внутренняя метка. Если это не метка — все команды до ближайшей метки выбрасываются. Я чуть улучшил исходный вариант этого места в компиляторе разбором, кто поставил эти команды без метки: компилятор или программист? Если сам компилятор — то команды выбрасываются без сообщения, иначе лучше сообщить программисту о таком странном месте, возможно это ошибка.

            Так вот, при разборе процедуры-функции в конце её тела компилятор теперь принудительно ставит некоторую фиктивную команду. Эта команда не превращается в коды процессора. Её единственное назначение — быть отброшенной как команда, на которую невозможно передать управление. Если же последующие части оптимизатора обнаружили такую команду, значит формально на неё можно передать управление и, следовательно, формально можно добраться до конца процедуры-функции, избежав операторов RETURN. Тогда текст:
IF A>B THEN RETURN(A); // ELSE RETURN(B);
END F;
даст предупреждение:
ВОЗМОЖЕН ВЫХОД БЕЗ ЗНАЧЕНИЯ ИЗ ФУНКЦИИ F

О зеркальности get и put

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

            Лет 30 объяснял всем (новичкам снисходительно), что операторы get/put в PL/1 зеркальны друг другу и это было сделано специально при проектировании языка.

            Мы часто пользовались этой зеркальностью. Например, одна программа печатает переменные в текстовый файл, другая их читает. Список переменных через запятую хранится в отдельном файле и вставляется в обе программы директивой %include.

            Т.е. оператор вывода выглядит как-нибудь так:
put file(f) data(%include ‘c:\test\param.txt’);
Удобно менять этот список только в одном месте.

            Однако вчера вдруг выяснилось, что для форматируемого ввода-вывода зеркальности нет. Если s — это, например, строка из 3 символов, то put edit(s)(a); печатает 3 символа, а якобы зеркальный оператор get edit(s)(a); сработает по-другому. Сначала из файла прочитается текстовый элемент по формату «а» (вся строка), а затем первые три символа перепишутся в переменную s. Если далее идут ещё элементы, они потеряются.

            Возник вопрос: кто виноват в потере зеркальности? Ошибка реализации, ошибка проектирования или ошибка моего восприятия? Cклоняюсь, что это ошибка моего восприятия. При проектировании, похоже, никто и не заморачивался никакой зеркальностью. Это я вообразил себе, что при проектировании была такая цель. Правда, дальше начинается схоластика: а то, что никто не заморачивался зеркальностью — это ошибка проектирования или нет?

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

            Ну, и лишний раз подтвердилась мудрость: век живи — век учись. Считал себя знатоком всех тонкостей и через 30 (!) лет узнал, что неправильно понимал свой рабочий инструмент.

            P.S. У Чехова, кажется, был рассказик, про человека, который годами ходил по улице мимо лавки с вывеской "большой выбор сигов" и удивлялся, чем одна рыба сиг отличается от другой. Через 10 лет он, наконец, прочитал вывеску. Там было написано "большой выбор сигар".

Автор: Д.Ю.Караваев. 12.02.2019

Опубликовано: 2019.02.14, последняя правка: 2022.12.24    20:36

ОценитеОценки посетителей
   ██████████████████████████████████████████ 1 (100%)
   ▌ 0
   ▌ 0
   ▌ 0

Отзывы

     2019/02/14 00:06, Автор сайта          # 

Зеркальность, одинаковость подходов дают возможность упирать на логику, а не рассчитывать на память. Почему бы, к примеру, не прийти к единому правилу, как должны располагаться играющие разные роли аргументы функций? Допустим, справа — источники информации, слева — приёмники. Или наоборот, но главное — чтобы везде одинаково. Это убережёт от множества ошибок. Зачем тут разнобой?
  fputc( символ_откуда, файл_куда);             // --> слева направо
fgets( строка_куда, сколько, файл_откуда); // <-- справа налево
(См. «Слева направо или справа налево?»). Тема зеркальности правильная: не столь важно, какое движение, правостороннее или левостороннее, главное, что имеет место быть только одно из них.

О возврате и невозврате значений. Когда в языке есть синтаксически обозначенные 1) чистые функции, 2) недетеминированные без побочных эффектов, 3) с побочными эффектами, 4) недетеминированные с побочными эффектами, то это помогает разобраться с возвратом значений.

Если у нас функция, возвращающее значение, имеет 1-й или 2-й тип, то в них и слово «return» не нужно. Возвращаемым значением является последнее выражение:
    . . .	// тело функции 1-го или 2-го типа
0) // последняя строка функции, здесь подразумевается return 0
Если у нас процедура, то интересные моменты тоже возможны. Процедура — это подпрограмма, не возвращающая значений, смысл её существования — в производстве побочного эффекта.

Допустим, f(x) — функция производящая побочный эффект, а g(y) и h(z) — его не производящие.
    . . .	// тело процедуры (3-й или 4-й тип)
f(x) // производим побочный эффект
g(y) // не производим побочный эффект
h(z) // не производим побочный эффект
return)
Вызов функций без побочного эффекта между f(x) и «return» (а это функции g(y) и h(z)) не имеет никакого смысла. Наработанные этими функциями результаты не используются далее для производства побочного эффекта. Т.е. это либо ошибка, либо выдача предупреждения о «лишнем коде», либо молчаливая оптимизация, этот код просто игнорирующая.

     2019/02/16 14:52, kt          # 

Еще по поводу "зеркальности". Справедливости ради: в стандарте языка изначально не было оператора типа get list(s)(a); А операторы обмена строки с фиксированной длиной типа get list(s)(a(3));/ put list(s)(a(3)); остаются зеркальными друг другу. Так что, возможно, изначально зеркальность в проекте языка все-таки предполагалась.

     2023/09/20 00:00, alextretyak          # 

Возник вопрос: кто виноват в потере зеркальности?

Ну в C++ тоже никакой зеркальности нет:
std::cout << 12 << 34; // выведет 1234
std::string s = "a b";
std::cout << s; // выведет `a b`, но
std::cin >> s; // если ввести `a b`, то в `s` запишется только `a`
std::getline(std::cin, s); // эта операция зеркальна `std::cout << s << '\n';`
// (но только при условии, что `s` не содержит символа '\n')
Не вижу в этом ничего страшного. Главное понимать, как работают операции ввода/вывода в используемом языке программирования, и применять их соответственно.
Некое подобие зеркальности есть в Python: print(s) зеркальна s = input(). Но опять же, если s не содержит символа перевода строки.

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

Ну, определённая логика тут тоже есть: файловый указатель всегда передаётся последним аргументом.
А в функциях «fread» и «fwrite» вообще одинаковое количество аргументов, и то, что эти аргументы расположены в одном порядке, порой оказывается довольно удобно: код для сохранения данных в файл, состоящий из вызовов «fwrite», можно скопировать, а затем заменить «fwrite» на «fread», и получится код для чтения из файла. Этот код, конечно, потребует доработки, но переставлять аргумент FILE* по крайней мере будет не нужно.

Когда в языке есть синтаксически обозначенные 1) чистые функции, 2) недетеминированные без побочных эффектов, 3) с побочными эффектами, 4) недетеминированные с побочными эффектами

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

Если у нас функция, возвращающее значение, имеет 1-й или 2-й тип, то в них и слово «return» не нужно. Возвращаемым значением является последнее выражение

С тем, что слово «return» не нужно в функциях без побочных эффектов, согласиться можно. Вот только не будет ли запутывать тот факт, что синтаксическая запись возврата значения функцией будет отличаться в зависимости от типа этой функции (для функций 1-го и 2-го типа — без «return», а для 3-го и 4-го — с «return»)? Я считаю, что разработчикам языка программирования следует выбрать какой-то один правильный вариант, а не обязывать программиста думать каждый раз нужен тут «return» для возврата значения или не нужен.

Слово «return», пожалуй, действительно не нужно в функциональных языках. Но в императивных языках есть некоторые функции/методы, которые возвращают значение, которое может быть проигнорировано. Например, в Python есть (https://docs.python.org/3/library/stdtypes.html#mutable-sequence-types) метод pop(i) у массивов, который удаляет i-й элемент и возвращает его. Но зачастую требуется только удалить элемент массива и возвращаемое значение pop(i) чаще просто игнорируется. При этом вводить отдельный метод, который только удаляет элемент массива и ничего не возвращает, нежелательно: во-первых, это плодит сущности, и во-вторых, наиболее подходящее название для такого метода — remove() — уже занято в Python под другое действие. Теперь представим себе функцию, которая оканчивается строкой a.pop(i). Что хотел программист в данном случае? Просто удалить элемент из a, или удалить и вернуть его?

Хочу заметить, что в Rust и Haskell эта проблема не актуальна, так как в первом необходимо всегда явно указывать тип возвращаемого функцией значения, а второй является функциональным языком, в котором с массивами так не работают. А в языках, где факт возвращения значения функцией можно не указывать (в Python, в 11l) целесообразно всегда ставить «return» для возврата значения. Для Rust можно ещё встретить такой аргумент (https://www.reddit.com/r/rust/comments/14ony3g /why_does_rust_have_the_convention_to_omitt_the/):

It's kind of just syntactic sugar. But it does make closures easier to deal with:
nums.iter().map(|i|*i).collect()

Without implicit return, |i|*i becomes |i|{return *i;}, which imo seems unneeded.

Но он не актуален в 11l, т.к. лямбды в 11l могут состоять только из одного выражения и не требуют «return».

Вызов функций без побочного эффекта между f(x) и «return» (а это функции g(y) и h(z)) не имеет никакого смысла.

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

     2023/09/24 22:47, Автор сайта          # 

код для сохранения данных в файл, состоящий из вызовов «fwrite», можно скопировать, а затем заменить «fwrite» на «fread», и получится код для чтения из файла. Этот код, конечно, потребует доработки, но переставлять аргумент FILE* по крайней мере будет не нужно.

Компания PVS-Studio в своих рассказах о том, как ошибаются программисты, часто критикует копипастный подход: он является источником большого количества ошибок. Хотя само занятие программирование можно считать главным источником ошибок :) Бухгалтеры, водители, повара ошибаются не так часто, как программисты :)

при объявлении каждой новой функции программист должен явно обозначать, к какой категории/типу относится эта функция?

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

Хотя можно схитрить. Ключевым словом «функция» обозначать только чистые функции. Для функций с побочными эффектами позаимствовать старое доброе слово «процедура». Остаётся подобрать ключевое слово для недетерминированных функций (может, прямо так и назвать, только сделать сокращение, например «недет»?) и для функций, сочетающих недерерминированность и побочные эффекты («подпрограмма»?).

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

Ну так есть же языки, в которых пишут return, хотя он не нужен, потому что ничего не возвращается. Наверно, такой «return» без аргументов можно оставить в качестве синтаксического сахара.

Но дело даже не в «return», а в том, что к разным категориям функций применимы разные оптимизации.

При этом вводить отдельный метод, который только удаляет элемент массива и ничего не возвращает, нежелательно: во-первых, это плодит сущности, и во-вторых, наиболее подходящее название для такого метода — remove() — уже занято в Python под другое действие.

Всё-таки это не по фэншую — позвать девушку на танец и не танцевать. Сказать, что мне всего лишь надо было освободить место около стены, где Вы стояли. А что страшного, если будет дополнительная функция будет носить имя «delete» или «drop» (как в Форте)?

     2023/09/27 00:00, alextretyak          # 

Хотя можно схитрить. Ключевым словом «функция» обозначать только чистые функции. Для функций с побочными эффектами позаимствовать старое доброе слово «процедура».

Так уже сделано в языке Nim (https://github.com/nim-lang/Nim/issues/2716#issuecomment-383512055): ключевое слово func является псевдонимом proc {.noSideEffect.}. Но как отмечено там же: термин "побочные эффекты" не имеет чёткого определения ("Side effect" has no precise definition).

В целом я склоняюсь к тому, что спецификаторы функций должны определяться автоматически компилятором (речь идёт о таких спецификаторах как inline, pure, а также const у методов). А то иначе получится как в D: например у функции analyzeHandHelper из примера отсюда: https://www.rosettacode.org/wiki/Poker_hand_analyser#D аж 4 спецификатора/атрибута (pure nothrow @safe @nogc), не считая private, а у вышестоящей функции analyzeHand спецификаторы/атрибуты nothrow и @safe почему-то закомментированы (неужели эта функция не-safe и может бросить исключение?).

И такие спецификаторы как consteval и constexpr в C++ я также считаю излишними: любую функцию, аргументы которой вычислимы на этапе компиляции, компилятор должен пытаться выполнить на этапе компиляции.

     2023/09/27 15:43, veector          # 

В целом я склоняюсь к тому, что спецификаторы функций должны определяться автоматически компилятором (речь идёт о таких спецификаторах как inline, pure, а также const у методов).

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

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

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

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

PS. В C++ мне вообще ничего не нравится, предпочитаю чистый Си.

     2023/09/28 11:10, Автор сайта          # 

Да, есть два подхода к свойствам функций, оба имеют как преимущества, так и недостатки.

Вариант первый: компилятор должен определять свойства сам. Если работу способен выполнить компьютер, а не человек, то это должно приветствоваться. Да, это ускоряет, на первый взгляд, работу разработчика. Но если надо узнать свойства функции, то это требует определённых усилий. Если x и y — это адреса функций, могу ли я их поместить в один массив
 A = {a, b}
то есть имеют ли они одинаковые определения? Если разные, то их нельзя помещать в один массив. Но откуда узнать свойства функций, чтобы не заглядывать глубоко в определения далёких предков? Ведь по внешнему виду сразу не скажешь, с каким представителем имеешь дело. Можно, конечно, для борьбы с неизвестностью использовать функции типа is_pure(), которая вернёт логическое значение. Но это лишняя работа, несмотря на то, что изначальная цель — это повесить лишнюю работу на компьютер.

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

Но встаёт вопрос об принудительном задании неких свойств. Например, объявляем, что функция чиста, и тогда включение в неё нечистых функций противоречит этому свойству. Ошибка при компиляции убережёт от смешения свойств. Тогда возникает вопрос: а не лучше ли такие описания сделать обязательными во избежание неочевидных ошибок? Например, компилятор сам в состоянии разобраться, где начинаются и заканчиваются блоки в программе. Но зачем тогда в Rust сделали фигурные скобки обязательными? Ведь это заставляет программиста делать ту работу, которую может выполнить компилятор. Ответ очевиден: во избежание ляпсусов и очепяток, которые не бросаются в глаза.

Тогда логичен второй вариант: свойства функции в обязательном порядке объявляется явно. Это уменьшает лаконичность. Хотя частично смягчить проблему можно увеличением количества ключевых слов, как в приведённом выше примере со словами «функция» и «процедура».

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

Не любую, а только чистую. Иначе бы на этапе компиляции можно было бы выполнять такие функции:
 x = random()	// аргумент известен во время компиляции, он пустой
put (y) // аргумент известен во время компиляции, это y
Обсуждение в дальнейшем будет перенесена в вероятно более подходящую ветку, а то оно далеко отклонилось от темы статьи.

     2023/09/28 16:44, veector          # 

Вот, ещё больше убедился в том, что автоматика должна быть управляемой.

Не любую, а только чистую. Иначе бы на этапе компиляции можно было бы выполнять такие функции:

 x = random() // аргумент известен во время компиляции, он пустой 
Тут приведен обычный пример. Если у меня в программе x = random() используется для шифрования сообщений, то, конечно, вычислять random() надо каждый раз в процессе выполнения программы, т.е. так, как Вы и написали.

Но есть и другие варианты использования случайных чисел!

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

     2023/09/30 00:00, alextretyak          # 

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

Фундаментальная ошибка inline-функций в том, что по факту все оптимизирующие компиляторы C++ на спецификатор inline просто не смотрят: решение о встраивании или не встраивании функции принимается ими автоматически и не зависимо от наличия/отсутствия этого спецификатора.
Но если очень надо, то есть __forceinline/__attribute__((always_inline)) для принуждения встраивания функции и __declspec(noinline)/__attribute__((noinline)) для запрета встраивания. Последний можно указать у вспомогательной функции, которая только вызывает встраиваемую функцию:
__forceinline auto inlined_fn(<some arguments>)
{
...
}

template <class... Args> __declspec(noinline) auto not_inlined_fn(Args&&... a)
{
return inlined_fn(std::forward<Args>(a)...);
}
Соответственно, если хочется экономить размер кода, то нужно вызывать not_inlined_fn(), которая не будет встраиваться.

Не любую, а только чистую.

Я думал об этом. И написал так, как написал осознанно. Т.к. практическое программирование порой противоречит теоретическим идеальным концепциям вроде «чистоты функций»: например, может быть вполне себе чистая функция, выполняющая какие-то очень сложные вычисления, но у которой есть булевый аргумент debug, который программист добавил для возможности отладки работы этой функции. Т.е. если аргумент debug равен true, тогда функция что-то пишет в лог или выводит в консоль или изменяет какую-то глобальную переменную, и таким образом формально уже не является чистой функцией. Но я не вижу причин, которые бы помешали компилятору при вызове этой функции с debug установленным в false считать эту функцию чистой и вычислять её на этапе компиляции при условии, что все аргументы этой функции вычислимы на этапе компиляции.

Иначе бы на этапе компиляции можно было бы выполнять такие функции:

x = random()   // аргумент известен во время компиляции, он пустой
put (y) // аргумент известен во время компиляции, это y

Вот кстати, а почему бы и нет? Вызов random() без предварительного srand(time(0)) (или чего-то похожего) обычно не используют. Но если используют, то именно для того, чтобы получить детерминированный random — а это полезно например для рефакторинга/переписывания какого-нибудь алгоритма, использующего random, чтобы можно было удостовериться, что алгоритм после переписывания работает правильно. Например, есть алгоритм генерации шума, который использует функцию random. Как понять, что после оптимизации кода алгоритм выдаёт такой же результат? Только сравнив шум, полученный старой версией алгоритма и новой версией. Что возможно только при условии детерминированности random. (На практике я довольно часто использовал детерминированный random (при этом саму функцию я называл nonrandom :)(:) для проверки корректности автоматизированного перевода задач на Rosetta Code с языка Python на 11l, как например вот в этой задаче: https://www.rosettacode.org /wiki/Bioinformatics /Sequence_mutation#11l, а вот https://github.com/11l-lang /_11l_to_cpp/blob /ec27a3d873ee4589f458141b63ebb0ab5be7cfbe /tests/python_to_cpp /Rosetta%20Code/b.txt#L1070 — соответствующий код на Python.

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

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

В целом, тут ситуация чем-то напоминает внеочередное исполнение (out-of-order execution) машинных инструкций в современных процессорах: для повышения производительности процессор может переупорядочивать инструкции, но итоговый результат должен быть в точности таким же, как если бы инструкции выполнялись процессором строго по порядку одна за другой (цитата отсюда: https://ru.wikipedia.org/wiki/Внеочередное_исполнение : «процессор записывает результаты выполнения инструкций так, чтобы создавалась видимость нормального, очередного выполнения». Так и оптимизирующий компилятор может выполнять всё, что ему вздумается, на этапе компиляции (в том числе вычислять любые функции: как полностью, так и частично — если функции не полностью чистые. Главное, чтобы результат был таким же, как и при традиционном исполнении кода, т.е. таким же, как ожидает программист. Лишь с той разницей, что получается этот результат за меньшее время выполнения скомпилированной программы.

     2023/10/01 15:17, Автор сайта          # 

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

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

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

Так и значение аргумента debug тоже известно во время компиляции, при этом формально не важно, равен он false или true, функция всё равно может вычисляться на этапе компиляции.

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

Вот кстати. А почему бы и нет?

Да потому что если можно на этапе компиляции можно вызывать недетерминированную функцию random, то тогда можно вызвать и любую операцию ввода данных. Запустил компиляцию, а программа просит заполнить накладную или читает личную переписку в WhatsApp :)

для того, чтобы получить детерминированный random … чтобы можно было удостовериться, что алгоритм после переписывания работает правильно.

Но это опять отладка, то есть специальный случай.

компилятор не должен выводить y на этапе компиляции. Но он может предвычислить какие-то выражения внутри функции put, чтобы put выполнялась быстрее — например перевести y в десятичную систему счисления на этапе компиляции

Да, это частичные вычисления; они, само собой, делаются при компиляции — при условии, что функция перевода в десятичную систему счисления чиста, а аргумент известен во время компиляции. Главное, чтобы эту функцию не подвергали экспериментам, делая её то детерминированной, то нет :)

Добавить свой отзыв

Написать автору можно на электронную почту
mail(аt)compiler.su

Авторизация

Регистрация

Выслать пароль

Карта сайта


Содержание

Каким должен быть язык программирования?

Анализ и критика

Описание языка

Компилятор

Отечественные разработки

Cтатьи на компьютерные темы

●  О превращении кибернетики в шаманство

●  Про лебедей, раков и щук

●  О русском ассемблере

●  Арифметика синтаксиса-3

●  Концепция владения в Rust на примерах

●●  Концепция владения в Rust на примерах, часть 2

●●  Концепция владения в Rust на примерах, часть 3

●  Суть побочных эффектов в чисто функциональных языках

●  О неулучшаемой архитектуре процессоров

●  Двадцать тысяч строк кода, которые потрясут мир?

●  Почему владение/заимствование в Rust такое сложное?

●  Масштабируемые архитектуры программ

●  О создании языков

●●  Джоэл Спольски о функциональном программировании

●  Почему Хаскелл так мало используется в отрасли?

●  Программирование исчезнет. Будет дрессировка нейронных сетей

●  О глупости «программирования на естественном языке»

●  Десятка худших фич C#

●  Бесплатный софт в мышеловке

●  Исповедь правового нигилиста

●  ЕС ЭВМ — это измена, трусость и обман?

●  Русской операционной системой должна стать ReactOS

●  Почему обречён язык Форт

●  Программирование без программистов — это медицина без врачей

●  Электроника без электронщиков

●  Программисты-профессионалы и программирующие инженеры

●  Статьи Дмитрия Караваева

●●  Идеальный транслятор

●●  В защиту PL/1

●●  К вопросу о совершенствовании языка программирования

●●  Опыт самостоятельного развития средства программирования в РКК «Энергия»

●●  О реализации метода оптимизации при компиляции

●●  О реализации метода распределения регистров при компиляции

●●  О распределении памяти при выполнении теста Кнута

●●  Опыты со стеком или «чемпионат по выполнению теста Кнута»

●●  О размещении переменных в стеке

●●  Сколько проходов должно быть у транслятора?

●●  Чтение лексем

●●  Экстракоды при синтезе программ

●●  Об исключенных командах или за что «списали» инструкцию INTO?

●●  Типы в инженерных задачах

●●  Непрерывное компилирование

●●  Об одной реализации специализированных операторов ввода-вывода

●●  Особенности реализации структурной обработки исключений в Win64

●●  О русском языке в программировании

●●  Формула расчета точности для умножения

●●  Права доступа к переменным

●●  Заметки о выходе из функции без значения и зеркальности get и put

●●  Модификация исполняемого кода как способ реализации массивов с изменяемыми границами

●●  Ошибка при отсутствии выполняемых действий

●●  О PL/1 и почему в нём не зарезервированы ключевые слова

●●  Не поминайте всуе PL/1

●●  Скорость в попугаях

●●  Крах операции «Инкогнито»

●●  Предопределённый результат

●●  Поддержка профилирования кода программы на низком уровне

●●  К вопросу о парадигмах

●  Следующие 7000 языков программирования

●●  Что нового с 1966 года?

●●  Наблюдаемая эволюция языка программирования

●●  Ряд важных языков в 2017 году

●●  Слоны в комнате

●●  Следующие 7000 языков программирования: заключение

Компьютерный юмор

Новости и прочее




Последние отзывы

2024/04/18 11:14 ••• Ivan
Энтузиасты-разработчики компиляторов и их проекты

2024/04/18 04:47 ••• void
Признаки устаревшего языка

2024/04/11 00:08 ••• Автор сайта
Постфиксные инкремент и декремент

2024/04/09 23:50 ••• Автор сайта
Русский язык и программирование

2024/04/07 15:33 ••• MihalNik
Все языки эквивалентны. Но некоторые из них эквивалентнее других

2024/04/01 23:39 ••• Бурановский дедушка
Новости и прочее

2024/04/01 23:32 ••• Бурановский дедушка
Русской операционной системой должна стать ReactOS

2024/03/22 20:41 ••• void
Раскрутка компилятора

2024/03/20 19:54 ••• kt
О многократном резервировании функций

2024/03/20 13:13 ••• Неслучайный читатель
Надёжные программы из ненадёжных компонентов

2024/03/07 14:16 ••• Неслучайный читатель
«Двухмерный» синтаксис Python

2024/03/03 16:49 ••• Автор сайта
О неправомерном доступе к памяти через указатели

2024/02/28 18:59 ••• Вежливый Лис
Про лебедей, раков и щук