Сейчас все понимают, что использовать оператор GOTO это не просто плохая, а ужасная практика. Дебаты по поводу его использования закончились в 80-х годах XX века и его исключили из большинства современных языков программирования. Но, как и положено настоящему злу, он сумел замаскироваться и воскреснуть в XXI веке под видом исключений.
Исключения, с одной стороны, являются достаточно простой концепцией в современных языках программирования. С другой же стороны, их часто используют неправильно. Есть простое и хорошо известное правило – исключения только для обработки поломок. И именно слишком вольная интерпретация понятия «поломка» приводит ко всем проблемам использования GOTO.
Теоретический пример
Разница между поломками и негативными бизнес-сценариями хорошо видна на окне входа в систему с очень простым сценарием использования:
- Пользователь вводит логин/пароль.
- Пользователь нажимает кнопку «Войти в систему».
- Клиентское приложение отправляет запрос на сервер.
- Сервер успешно проверяет логин/пароль (под успехом считает наличие соответствующей пары).
- Сервер отсылает клиенту информацию, что аутентификация прошла успешно и ссылку на страницу перехода.
- Клиент осуществляет переход на указанную страницу.
И одно негативное расширение:
4.1. Сервер не нашел соответствующую пару логин/пароль и посылает клиенту уведомление об этом.
Считать, что сценарий 4.1 является «проблемой» и поэтому его надо реализовывать с помощью исключения – достаточно распространенная ошибка. На самом деле это не так. Несоответствие логина и пароля – это часть нашего стандартного взаимодействия с пользователем, предусмотренная бизнес-логикой сценария. Наши бизнес-заказчики ожидают такого развития событий. Следовательно – это не поломка и использовать здесь исключения нельзя.
Поломки, это: разрыв соединения между клиентом и севером, недоступность СУБД, неправильная схема в БД. И еще миллион причин, ломающих наши приложения и не имеющих никакого отношения к бизнес-логике пользователя.
В одном из проектов, в разработке которого я участвовал, была более сложная логика входа в систему. Введя 3 раза подряд неправильный пароль, пользователь временно блокировался на 15 минут. Попадая 3 раза подряд во временную блокировку, пользователь получал постоянную блокировку. Также были дополнительные правила в зависимости от типа пользователя. Реализация с помощью исключений привела к тому, что внесение новых правил было крайне затруднительно.
Рассмотреть это пример было бы интересно, но он слишком большой и не очень наглядный. Как запутанный код с бизнес-логикой на исключениях превращается в понятный и лаконичный, я покажу на другом примере.
Пример Загрузка свойств
Попробуйте посмотреть данный код и четко понять, что он делает. Процедура не большая с достаточно простой логикой. При хорошем стиле программирования понимание ее сути не должно превышать больше 2-3 минут (я не помню сколько времени ушло у меня на полное понимание этого кода, но точно больше 15 минут).
private WorkspaceProperties(){
Properties loadedProperties = readPropertiesFromFile(WORK_PROPERTIES_PATH, true);
//These mappings will replace any mappings that this hashtable had for any of the
//keys currently in the specified map.
getProperties().putAll( loadedProperties );
//Это файл имеет право отсутствовать
loadedProperties = readPropertiesFromFile(MY_WORK_PROPERTIES_PATH, false);
if (loadedProperties != null){
getProperties().putAll( loadedProperties );
}
System.out.println("Loaded properties:" + getProperties());
}
/**
* Возвращает свойства, загруженные из указанного файла.
* @param filepath
* @param throwIfNotFound - кинуть FileNotFoundException, если файл не найден
* @return Загруженные свойства или null, если файл не найден и !throwIfNotFound
* @throws FileNotFoundException throwIfNotFound и файла с таким именем не надено
* @throws IOException ошибка загрузки найденного файла
*/
private Properties readPropertiesFromFile(String filepath, boolean throwIfNotExists){
Properties loadedProperties = new Properties();
System.out.println("Try loading workspace properties" + filepath);
InputStream is = null;
InputStreamReader isr = null;
try{
int loadingTryLeft = 3;
String relativePath = "";
while (loadingTryLeft > 0){
try{
File file = new File(relativePath + filepath);
is = new FileInputStream(file);
isr = new InputStreamReader( is, "UTF-8");
loadedProperties.load(isr);
loadingTryLeft = 0;
} catch( FileNotFoundException e) {
loadingTryLeft -= 1;
if (loadingTryLeft > 0)
relativePath += "../";
else
throw e;
} finally {
if (is != null)
is.close();
if (isr != null)
isr.close();
}
}
System.out.println("Found file " + filepath);
} catch( FileNotFoundException e) {
System.out.println("File not found " + filepath);
if (throwIfNotExists)
throw new RuntimeException("Can`t load workspace properties." + filepath + " not found", e );
}catch (IOException e){
throw new RuntimeException("Can`t read " + filepath, e);
}
return loadedProperties;
}
Итак, раскроем тайну – что же здесь происходит. Осуществляется загрузка свойств из двух файлов – обязательного WORK_PROPERTIES
и дополнительного MY_WORK_PROPERTIES
, добавляя в общее хранилище свойств. При этом есть нюанс – нам точно не известно, где лежит конкретный файл свойств – он может лежать как в текущем каталоге, так и в каталогах-предках (до трех уровней вверх).
Здесь смущает, как минимум, две вещи: параметр throwIfNotExists
и большой блок логики в catch FileNotFoundException
. Все это непрозрачно намекает – исключения используются для реализации бизнес-логики (а как иначе объяснить, что в одном сценарии выброс исключения – это поломка, а в другом – нет?).
Делаем правильный контракт
Сначала разберемся с throwIfNotExists
. При работе с исключениями очень важно понимать – где именно его нужно обработать с точки зрения сценариев использования. В данном случае очевидно, что сам метод readPropertiesFromFile
не может принять решение – когда отсутствие файла «плохо», а когда – «хорошо». Такое решение принимается в точке его вызова. По комментариям видно, что мы решаем – должен существовать этот файл или нет. Но на самом деле нам интересен не сам файл, а настройки из него. К сожалению, это никак не следует из кода.
Исправим оба этим недостатка:
Properties loadedProperties = readPropertiesFromFile(WORK_PROPERTIES_PATH);
if (loadedProperties.isEmpty()) {
throw new RuntimeException("Can`t load workspace properties");
}
loadedProperties = readPropertiesFromFile(MY_WORK_PROPERTIES_PATH);
getProperties().putAll( loadedProperties );
Теперь четко показана семантика –
WORK_PROPERTIES
обязательно должны быть заданы, а MY_WORK_PROPERTIES
— нет. Также при рефакторинге я обратил внимание, что readPropertiesFromFile
никогда не сможет вернуть null
и воспользовался этим при чтении MY_WORK_PROPERTIES
.
Проверяем не ломая
Предыдущий рефакторинг также затронул и реализацию, но не значительно. Я просто удалил блок обработки throwIfNotExists
:
if (throwIfNotExists)
throw new RuntimeException(…);
Рассмотрев реализацию более пристально, мы начинаем понимать логику автора кода по поиску файла. Сначала проверяется, что файл находится в текущем каталоге, если не нашли – проверяем на уровне выше и т.д. Т.е. становится понятно, что алгоритм предусматривает отсутствие файла. При этом проверка делается с помощью исключения. Т.е. нарушен принцип – исключение воспринимается не как «что-то поломалось», а как часть бизнес-логики.
Существует функция проверки доступности файла для чтения File.canRead()
. Используя ее можно избавиться от бизнес-логики в блоке catch
try{
File file = new File(relativePath + filepath);
is = new FileInputStream(file);
isr = new InputStreamReader( is, "UTF-8");
loadedProperties.load(isr);
loadingTryLeft = 0;
} catch( FileNotFoundException e) {
loadingTryLeft -= 1;
if (loadingTryLeft > 0)
relativePath += "../";
else
throw e;
} finally {
if (is != null)
is.close();
if (isr != null)
isr.close();
}
}
Изменив код, получаем следующее:
private Properties readPropertiesFromFile(String filepath) {
Properties loadedProperties = new Properties();
System.out.println("Try loading workspace properties" + filepath);
try {
int loadingTryLeft = 3;
String relativePath = "";
while (loadingTryLeft > 0) {
File file = new File(relativePath + filepath);
if (file.canRead()) {
InputStream is = null;
InputStreamReader isr = null;
try {
is = new FileInputStream(file);
isr = new InputStreamReader(is, "UTF-8");
loadedProperties.load(isr);
loadingTryLeft = 0;
} finally {
if (is != null)
is.close();
if (isr != null)
isr.close();
}
} else {
loadingTryLeft -= 1;
if (loadingTryLeft > 0) {
relativePath += "../";
} else {
throw new FileNotFoundException();
}
}
}
System.out.println("Found file " + filepath);
} catch (FileNotFoundException e) {
System.out.println("File not found " + filepath);
} catch (IOException e) {
throw new RuntimeException("Can`t read " + filepath, e);
}
return loadedProperties;
}
Также я снизил уровень переменных (is
, isr
) до минимально допустимого.
Такой простой рефакторинг значительно повышает читаемость кода. Код напрямую отображает алгоритм (если файл существует, то читаем, а иначе – уменьшаем количество попыток и ищем в каталоге выше).
Выявляем GOTO
Рассмотрим детально происходящее в ситуации, если файл не был найден:
} else {
loadingTryLeft -= 1;
if (loadingTryLeft > 0) {
relativePath += "../";
} else {
throw new FileNotFoundException();
}
}
Видно, что здесь исключение используется для того, чтобы прервать цикл выполнения и фактически выполняют функцию GOTO.
Для сомневающихся сделаем еще одно изменение. Вместо использования мелкого костыля в виде loadingTryLeft = 0
(костыль, потому что на самом деле успешная попытка неизменяет количество оставшихся попыток) явно укажем, что считывание файла приводит к выходу из функции (не забыв при этом написать сообщение):
try {
is = new FileInputStream(file);
isr = new InputStreamReader(is, "UTF-8");
loadedProperties.load(isr);
System.out.println("Found file " + filepath);
return loadedProperties;
} finally {
Это позволяет нам заменить условие while (loadingTryLeft > 0)
на while(true)
:
try {
int loadingTryLeft = 3;
String relativePath = "";
while (true) {
File file = new File(relativePath + filepath);
if (file.canRead()) {
InputStream is = null;
InputStreamReader isr = null;
try {
is = new FileInputStream(file);
isr = new InputStreamReader(is, "UTF-8");
loadedProperties.load(isr);
System.out.println("Found file " + filepath);
return loadedProperties;
} finally {
if (is != null)
is.close();
if (isr != null)
isr.close();
}
} else {
loadingTryLeft -= 1;
if (loadingTryLeft > 0) {
relativePath += "../";
} else {
throw new FileNotFoundException(); // GOTO: FFN
}
}
}
} catch (FileNotFoundException e) { // LABEL: FFN
System.out.println("File not found " + filepath);
} catch (IOException e) {
throw new RuntimeException("Can`t read " + filepath, e);
}
Чтобы избавиться от явного дурно пахнущего throw new FileNotFoundException
, нужно вспомнить контракт функции. Функция в любом случае возвращает набор свойств, если не смогли считать файл – возвращаем его пустым. Поэтому нет никаких причин выбрасывать исключение и перехватывать его. Достаточно обычного условия while (loadingTryLeft > 0)
:
private Properties readPropertiesFromFile(String filepath) {
Properties loadedProperties = new Properties();
System.out.println("Try loading workspace properties" + filepath);
try {
int loadingTryLeft = 3;
String relativePath = "";
while (loadingTryLeft > 0) {
File file = new File(relativePath + filepath);
if (file.canRead()) {
InputStream is = null;
InputStreamReader isr = null;
try {
is = new FileInputStream(file);
isr = new InputStreamReader(is, "UTF-8");
loadedProperties.load(isr);
System.out.println("Found file " + filepath);
return loadedProperties;
} finally {
if (is != null)
is.close();
if (isr != null)
isr.close();
}
} else {
loadingTryLeft -= 1;
if (loadingTryLeft > 0)
relativePath += "../";
}
}
System.out.println("file not found");
} catch (IOException e) {
throw new RuntimeException("Can`t read " + filepath, e);
}
return loadedProperties;
}
В принципе, с точки зрения правильной работы с исключениями здесь все. Остается сомнение в необходимости выбрасывать RuntimeException в случае проблем IOException, но оставим его как есть ради совместимости.
Наводим лоск
Остались немного мелочей, исправив которые мы сделаем код еще более гибким и понятным:
- Название метода readPropertiesFromFile раскрывает его реализацию (кстати, равно как и throws FileNotFoundException). Лучше назвать более нейтрально и лаконично – loadProperties(…)
- Метод одновременно и ищет, и считывает. Для меня это две разных обязанности, которые можно разделить в разных методах.
- Изначально код писался под Java 6, а сейчас используется на Java 7. Это позволяет использовать closable resources.
- По опыту знаю, что при выводе информации о найденном или не найденном файле лучше использовать полный путь к файлу, а не относительный.
if (loadingTryLeft > 0) relativePath += "../";
— если внимательно посмотреть код, то видно – эта проверка лишняя, т.к. при исчерпании лимита поиска все равно новое значение использовано не будет. А если в коде что-то лишнее, это мусор, который следует убрать.
Окончательная версия исходного кода:
private WorkspaceProperties() {
super(new Properties());
if (defaultInstance != null)
throw new IllegalStateException();
Properties loadedProperties = readPropertiesFromFile(WORK_PROPERTIES_PATH);
if (loadedProperties.isEmpty()) {
throw new RuntimeException("Can`t load workspace properties");
}
getProperties().putAll(loadedProperties);
loadedProperties = readPropertiesFromFile(MY_WORK_PROPERTIES_PATH);
getProperties().putAll(loadedProperties);
System.out.println("Loaded properties:" + getProperties());
}
private Properties readPropertiesFromFile(String filepath) {
System.out.println("Try loading workspace properties" + filepath);
try {
int loadingTryLeft = 3;
String relativePath = "";
while (loadingTryLeft > 0) {
File file = new File(relativePath + filepath);
if (file.canRead()) {
return read(file);
} else {
relativePath += "../";
loadingTryLeft -= 1;
}
}
System.out.println("file not found");
} catch (IOException e) {
throw new RuntimeException("Can`t read " + filepath, e);
}
return new Properties();
}
private Properties read(File file) throws IOException {
try (InputStream is = new FileInputStream(file);
InputStreamReader isr = new InputStreamReader(is, "UTF-8")) {
Properties loadedProperties = new Properties();
loadedProperties.load(isr);
System.out.println("Found file " + file.getAbsolutePath());
return loadedProperties;
}
}
Резюме
Разобранный пример наглядно показывает, к чему приводит небрежное обращение с исходным кодом. Вместо использования исключения для обработки поломки, было принято решение использовать его для реализации бизнес-логики. Это сразу привело к сложности его поддержки, что и нашло отражение в его дальнейшей доработке под новые требования и в итоге – к отходу от принципов структурного программирования. Использование простого правила – исключения только для поломок – поможет вам избежать возвращения в эру GOTO и содержать ваш код чистым, понятным и расширяемым.
DMGarikk
да никто этого не понимает, никто, языки где был тот самый страшной goto пропали ещё в конце 80 вместе с обязательной нумерации строк и пререхода на процедурное программирование
народ по привычке продолжает пересказывать страшилки про goto и приводить, этот единственный пример с выходом из цикла причём в яве для этого можно использовать переход по метке через break или continue
люди, если вы ниразу не писали на языке где используется goto который приводит к тяжелым последствиям, не стоит пересказывать эту страшилку, вы просто не понимаете о чем говорите, в современных языках сам принцип программирования исключает использование goto независимо от вашего понимания 'ужасных практик'
p.s. устриц ел, писал на atari basic
p.p.s. в современных бейсиках goto уже тоже давно нет в том самом опасном виде
usdglander
Интересен факт того, что в PHP оператор goto, как раз появился только в версии 5.3. До сих пор не понимаю для чего.
E_STRICT
DMGarikk
в них можно делать goto по метке за пределы процедуры?
fougasse
C++
C
ss-nopol
В С/С++ можно вообще куда угодно с помощью longjmp. Классический goto только в пределах функции.
NooneAtAll
не совсем "куда угодно" правда
E_STRICT
PHP
oam2oam
Легко!
accipiter
Это же gnu расширение возможностей языка.
https://gcc.gnu.org/onlinedocs/gcc/Labels-as-Values.html
oam2oam
Это так. И думается мне, что это и есть собственно язык :)
accipiter
Конечно это язык. Только это GNU C, а не чистый C.
oam2oam
Так и в вопросе не было уточенений! Вот — есть такое…
edogs
Мы начинали с языка где goto вполне себе используется — assembler.
Потом, что интересно, был basic потом c.
Потом был период когда преподавали basic и это позволило увидеть много интересных вещей происходящих с goto.
И вот что можем сказать — goto это не зло, это инструмент (уже ловим гнилые помидоры). Инструмент чертовски опасный, как острый нож — которым легче порезаться, чем что-то им нарезать. В современном программировании, принято делать языки оберегающие программистов от опасных ошибок, поэтому goto там либо нет либо его использование запрещено под страхом увольнения.
Но с этим есть две проблемы
а) Зачастую с водой выплескивают и ребенка. Правильное использование goto в 1% случаев может сделать код проще, быстрее, компактнее и понятнее. Но goto нет, т.к. в 99% случаев его будут использовать неправильно.
б) Народ не понимая сути проблемы с опасными практиками применения goto начинает эмулировать его другими способами, как верно подмечено в статье — например исключениями. В результате goto нет, а опасные практики есть.
Ближайший аналог из чего-то относительно свежего — это register_globals в php. Они влияли только на безопасность неграмотно написанного кода и их отключили, потому что неграмотно написанного кода было слишком много.
На самом деле депрессивная причина, когда удобный инструмент не используют только потому, что куча народа не умеет им правильно пользоваться.
fougasse
Интересно стало посмотреть на необходимость goto в Java.
Tangeman
Не всегда просто (и бесплатно) переписать часть кода так чтобы можно было без подобной конструкции обойтись, и тот факт что вместо «goto» используется «break» не меняет сути — и первое и второе перепрыгивают через кусок кода (как, собственно, вообще любые «break» и «continue», которые по сути те же самые «goto», хотя называются иначе).
Как выше уже отметили, не сам факт использования «goto» печален, а неадекватное его использование.
somurzakov
return это тоже GOTO в своем роде, мало кто этого понимает правда
DMGarikk
если совсем точно проводить аналогии то return это resume/return из gosub
somurzakov
я имел в виду, что иногда встречаешься с кодом где излишне злоупотребляют return'ом и он читается ненамного лучше чем goto, например return в начале тела функции, в середине внутри двух-трех вложенных циклов и в конце тела.
а по сути да, return это прыжок в конец тела функции с каким-то значением
tuxi
Так это же наоборот хорошо, так как «только начал читать метод/функцию, как сразу уже понятно, что при определенных условиях, можно уже не продолжать ее анализ».
KvanTTT
Раньше старался писать "правильно", чтобы на функцию был только один
return
и в конце. Теперь же пишу как удобно и короче, поэтомуreturn
зачастую встречается и в начале. Из-заreturn
в начале приходится городить блокиif
else
, из-за которых появляется дополнительный уровень вложенности.Tangeman
Из чего следует что «правильно» это «один
return
»?Если функция вида:
то всё вполне «правильно», да и несколько return в середине (если уже ясен результат или выполнено всё что нужно) тоже вполне правильно, и совсем не хуже (если не лучше) чем:
return
в начале (или перед куском кода) как раз часто позволяет избавится от дополнительной вложенности.KvanTTT
Из каких-то источников в интернете. Слово и обособлено в кавычки, потому что на самом деле не правильно, да и нельзя возводить какой-то принцип в абсолют.
Я об этом и писал тоже.
Eswcvlad
Структурированное программирование, в итоге, победило, и правило преобразовалось в «только один return в функции», так как о прыжках в середину других функций уже мало кто вспомнит…
mayorovp
Да нет, источник правила — другой, и про него уже писали где-то тут. Если вам надо перед выходом из функции освободить ресурсы — разумно делать это только в одном месте; это просто следствие DRY. Работает правило в языках без автоочистки ресурсов.
Ryppka
Уже много лет назад в c++.moderated была жаркая флейма по поводу multiple return vs. single return. Я из нее вынес примерно такую позицию: в C, где нет исключений, single return имеет смысл, если не требует акробатических трюков в коде. А в C++, где есть конструкторы/деструкторы и исключения и из блока можно вылететь в любом месте — разницы между множественным и единственным возвратом не много. В C goto, кстати, часто приводит и к single return'у, и без goto его не всегда легко достичь.
В конечном случае решает понятность кода и эффективность. Может быть субъективным подходом, но в целом консенсус достижим.
Кстати, отличие в сгенерированном коде multiple return от goto обычно чуть менее, чем отсутствует.
VolCh
У нас принято делать ранний return для проверки валилности аргументов и контекста, для edge кейсов алгоритма типа логарифм 1 = 0, а в остальном рекомендуется использовать один, если это не сильно щагромождает код
chapuza
Забавно, что в любой теософической ветке рано или поздно появится пример с кодом (или с описанием кода) на каком-то языке, без упоминания, какой именно это язык.
Надо в правилах, что ли, обозначить, на заглавной странице: «Дефолтсити: Москва. Дефолтлангадж: Джавапитонскрипт.»
Я это к чему. На языках, на которых в последнее время пишу я — вообще нет такой конструкции «
return
».fougasse
Мы рады за вас, но какое это имеет отношение к обсуждению?
chapuza
В обсуждении всплыло слово «
return
», которое ведет к потере общности дискуссии в целом. Это неочевидно?VolCh
Если всплыло упоминание, значит контекст был (или стал) про языки, где есть подобные конструкции. У нас это правило применяется на PHP, TypeScript, Java, Go и паре диалектов SQL, если ничего не забыл
VolCh
В общем можно сформулировать: должен быть один главный return (не обязательно в конце, часто в теле цикла)
rjhdby
Так и представил...
VolCh
Ну как-то так, да. :) Если в контексте статьи оставаться, то 2 и 3 строчка будут выбрасыванием эксешена :)
Arris
ret/iret точнее.
w0lf
У register_globals другая проблема, почему от него отказались. На мой взгляд основная беда с ним, что его поведение зависит от настроек PHP конкретной хостинг-площадки и разработчик в большинстве случаев не мог на него повлиять. То есть не мог расчитывать, из каких именно и в каком порядке переменные из суперглобальных массивов туда попадут. А то, на что не можешь повлиять, лучше вообще не использовать.
Использование Goto же, вполне поддаётся контролю.
VolCh
Не просто повлиять не мог, а, скажем так, нетипичные настройки могли приводить к серьёзным уязвимостям безопасности. Например давать возможность перезаписать env переменные в query параметрах, от "безобидного" APP_DEBUG=1 до обхода аутентификации и авторизации
IgorPie
goto — просто эквивалент jmp в ассемблере. Смысл выкидывать слова из песни?
Бывает и for приводит к тяжелым последствиям, тем более, это слегка пропатченный while. Давайте его тоже запретим?
Все эти гонения на goto — это как корреляция «убийства и хлеб». Проблема в головах, а не в операторе.
chapuza
Вы, может быть, удивитесь, но в мире есть масса языков, в которых нет циклов (ни
for
, ниwhile
, никаких), и нетreturn
, и вот на них писать — сплошное наслаждение.IgorPie
Не такая уж и масса и языки эти не в топе. В некоторых DSP нет for/while, но в их ассемблере их тоже нет. Языки экспертных систем тоже как-то обходились. Но это все специфика, а не мэйнстрим.
chapuza
А я думал, ФП уже лет пять, как мейнстрим. Да и платят лучше.
IgorPie
Лучше, чем за джаву? Сомневаюсь. И шансы обучиться/трудоустроиться сильно меньше.
chapuza
Обожаю взвешенные пуленепробиваемые аргументы.
IgorPie
Ну, Вы первым начали.
Достаточно показать статистику нуждаемости рынка труда в программистах с той или иной специализацией (на хабре были целые статьи про это) и помножить на медианную ЗП (пусть с тех же сайтов, раз более точных данных нет).
Все годы больше всего было нужно джавистов и объем в $$ — самый большой. Причем, голод такой, что уважаемые фирмы, которые раньше стеснялись, теперь чуть ли не открыто пишут, что мол купить условный мяч — нажмите эту кнопку, а если вы джава программист — соседнюю. Т.е. на рекрутеров и сайты поиска работы уже не надеются.
chapuza
И как из всего этого следует, что зарплата джависта выше? Функциональщиков ищут прицельно, в обход всех этих агентств, эйчаров, и больших кнопок справа. Хотя уже сейчас видно, что ситуация потихонечку меняется. Не, если надо устроиться в нейпомикуда миддлом — то джавистам проще, конечно, тут спору нет.
nickolaym
Жырные намёки в сторону ФП, где… внезапно, есть и for (map), и while, и прямо документировано в спецификациях языков, что концевая рекурсия реализуется через зацикливание (потому что иначе это смерть для стека).
Масса этих языков, кстати, довольно маленькая. Даже если это какой-нибудь дремучий лисп, базу кода на котором с 1960-х годов наполняли.
chapuza
Если вы считаете, что
for
— этоmap
, то вам сто?ит расширить горизонты использованияfor
.while
есть только в формеreduce_while
.Что как реализуется — это вопрос тридцать пятый, потому что понятие «цикл» — тоже высокоуровневое, так-то все реализуется через
jmp
.Ну вот Twitter написан на скале. WhatsApp — на эрланге. Довольно много внутреннего финтеха — на доказательных языках, типа агды / кока. Хаскелл пролезает во все щели. Назовите хоть один язык, не вызывающий рвотного рефлекса одним синтаксисом, по сравению с которым эта масса невелика.
nickolaym
Если вы считаете, что я не знаю, что такое ФП и не умею в него, то давайте на этом месте закончим разговор.
Реализация концевой рекурсии — не тридцать пятый вопрос.
Это либо честная рекурсия и смерть стеку, либо гарантии на размер служебных данных (стека или санок).
А когда у нас есть эти гарантии, то мы наконец можем спокойно выдохнуть и сделать высокоуровневое действие, то есть, например, цикл.
А в языках, где из коробки забыли поддержать линзы, бананы и колючую проволоку, писать циклы рекурсией — это такой же лютый колхоз, как писать циклы на goto.
А называть языки я даже не подумаю, потому что мне надо для этого залезть к вам в глотку и выяснять, с чего лично вас тошнит, а с чего нет.
Меня, например, тошнит со скалы и эрланга. Почему вас с них не тошнит, я не знаю.
С питона вас тошнит или нет?
chapuza
А и давайте.
VolCh
В JavaScript задокументировали, заспецифировали, но вроде до сих пор нигде не реализовали
rsync
если заглянуть в ядро Линукс, то там можно увидеть массу примеров кода с goto. Если этот код переписать без goto будет потеряна лаконичность и код станет ужасно нечитаем.
например такой паттерн: