Рекурсивные функции erlangs не просто goto?

Просто, чтобы понять это прямо в моей голове. Рассмотрим этот примерный бит кода Erlang:

 test() ->
      receive
          {From, whatever} ->
                %% do something
               test();
          {From, somethingelse} ->
               %% do something else
               test();
 end.

Не вызов test(), просто переход?

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

В основном. У нас было два разных способа вызова функций: goto и gosub. goto просто управлял потоком программы где-то в другом месте, и gosub вспомнил, откуда вы пришли, чтобы вы могли вернуться.

Учитывая этот способ мышления, я могу легче рассмотреть рекурсию erlang, так как если бы я просто прочитал: test() как goto, тогда нет никакой проблемы.

следовательно, мой вопрос: не erlang просто использовать goto вместо того, чтобы помнить обратный адрес в стеке?

EDIT:

Только для того, чтобы прояснить мою мысль:

Я знаю, что goto может использоваться на некоторых языках, чтобы прыгать повсюду. Но просто suupose вместо выполнения someFunction() вы также можете сделать: goto someFunction() в первом примере поток возвращается, во втором примере поток просто продолжается в someFunction и никогда не возвращается.

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

Если вы видите так, что вызов рекурсивной функции erlang выглядит как goto.

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

Ответ 1

Рекурсивный вызов хвоста является скорее "возвратом и немедленно вызывает эту другую функцию", чем goto из-за проведенного домашнего хозяйства.

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

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

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

" goto &NAME" утверждение в Perl ближе к тому, о чем вы думаете, но не совсем, поскольку оно отбрасывает локальных жителей. Параметры сохраняются для вновь вызванной функции.

Еще одно простое отличие: хвостовой вызов может переходить только к точке входа функции, в то время как goto может прыгать куда угодно (некоторые языки ограничивают цель goto, например C, где goto не может прыгать за пределы функция).

Ответ 2

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

Кроме того, он также способен обнаруживать круговую хвостовую рекурсию, например

test() -> ..., test2().
test2() -> ..., test3().
test3() -> ..., test().

также будет оптимизирован.

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

Ответ 3

У вас здесь два вопроса.

Во-первых, нет, вам не грозит опасность перебора стека в этом случае, потому что эти вызовы test() являются tail-recursive.

Во-вторых, нет, вызовы функций не являются goto, они являются функциональными вызовами.:) То, что делает проблему проблематичной, заключается в том, что она обходит любую структуру вашего кода. Вы можете выпрыгнуть из высказываний, перепрыгнуть в утверждения, обойти задания... всевозможные болтовни. Функциональные вызовы не имеют этой проблемы, поскольку они имеют очевидный поток управления.

Ответ 4

Я думаю, что разница здесь между "реальным" goto и тем, что в некоторых случаях может показаться goto. В некоторых особых случаях компилятор может обнаружить, что можно очистить стек текущей функции до вызова другой функции. Это когда вызов является последним вызовом функции. Разумеется, разница заключается в том, что, как и в любом другом вызове, вы можете передать аргументы новой функции.

Как отмечали другие, эта оптимизация не ограничивается рекурсивными вызовами, а всеми последними вызовами. Это используется в "классическом" способе программирования FSM.

Ответ 5

Это a goto тем же, почему if есть goto и while есть goto. Он реализуется с использованием (морального эквивалента) goto, но он не предоставляет полный потенциал стрельбы самостоятельно в ноге goto непосредственно программисту.

Ответ 6

Фактически, эти рекурсивные функции конечный GOTO в соответствии с Guy Steele.

Ответ 7

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

Пролог

В некоторых архитектурах нет вещей, которые они называют "функциями", которые "называются", но имеют что-то аналогичное (сообщения могут называть их "методами" или "обработчиками сообщений", архитектуры на основе событий имеют "обработчики событий" или просто "обработчики" ). Я буду использовать термины "кодовый блок" и "вызов" для общего случая, хотя (строго говоря) "кодовый блок" может включать в себя вещи, которые не являются довольно функциональными. Вы можете заменить подходящую форму "вызова" для "вызова" или "вызывать", как я мог бы в нескольких местах. Функции архитектуры, описывающие вызов, иногда называются "стилями", как в "стиле продолжения прохождения" (CPS), хотя это ранее не официальный термин. Чтобы вещи не были слишком абстрактными, мы рассмотрим стек вызовов, продолжение передачи, обмен сообщениями (à la OOP) и стили вызовов обработки событий. Я должен указать модели, которые я использую для этих стилей, но я оставляю их в интересах пространства.

Функции вызова

или, C для продолжения, координации и контекста, это достаточно для меня

Hohpe идентифицирует три хорошо аллитеративные функции вызова стиля call-stack: Continuation, Coordination, Context (все капитализируются, чтобы отличить их от других использование слов).

  • Continuation решает, когда выполнение будет продолжено, когда закончится кодовый блок. Функция "Продолжение" связана с " первоклассными продолжениями" (часто просто называемыми "продолжениями", в том числе мной), в этих продолжениях сделать функцию продолжения видимой и манипулируемой на программном уровне.
  • Координация означает, что код не выполняется до тех пор, пока необходимые данные не будут готовы. Внутри одного стека вызовов вы получаете координацию бесплатно, потому что счетчик программ не возвращается к функции до тех пор, пока вызываемая функция не завершится. Координация становится проблемой в (например) параллельном и управляемом событиями программировании, первая из которых связана с тем, что производитель данных может отставать от потребителя данных, а второй потому, что, когда обработчик запускает событие, обработчик продолжает немедленно, не дожидаясь ответа.
  • Контекст относится к среде, которая используется для разрешения имен в блоке кода. Он включает в себя распределение и инициализацию локальных переменных, параметров и возвращаемых значений. Передача параметров также распространяется на соглашение о вызове (сохранение аллитерации); для общего случая вы можете разделить Контекст на функцию, которая охватывает локали, которая охватывает параметры, а другая - для возвращаемых значений. Для CPS возвращаемые значения покрываются передачей параметров.

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

Список Hohpe не обязательно исчерпывающий, но достаточно будет выделить хвостовые вызовы из gotos. Предупреждение: я мог бы остановиться на касательных, например, исследовать пространство вызова на основе функций Хохпе, но я постараюсь сдержать себя.

Задачи функции вызова

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

Хвост вызовов

или, ответ

или, в принципе, нет необходимости в Rest Rest

Хвост вызывает все о оптимизации продолжения, и это вопрос распознавания, когда основная задача продолжения (запись ссылки обращения) может быть пропущена. Другие функции задачи стоят сами по себе. "Goto" представляет собой оптимизацию задач для продолжения и контекста. Это в значительной степени, почему хвостовой вызов не является простым "goto". Следующее будет определять, какие хвостовые вызовы выглядят в разных стилях вызова.

Хвост звонков в специальных стилях вызова

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

  • В стеке вызовов есть только одна цепочка вызовов в клубок; расширение цепи означает нажатие программного счетчика. Вызов хвоста означает, что нет прокрутки счетчика программ.
  • В CPS пул состоит из сохранившихся продолжений, которые образуют reverse arborescence (ориентированное дерево, где каждое ребро указывает на центральный node), где каждый путь назад к центру является цепочкой вызовов (обратите внимание: если точка входа в программу передается "нулевого" продолжения, клубок может быть целым лесом обратных древовидностей). Одна конкретная цепочка является значением по умолчанию, в котором во время вызова добавляется ссылка на вызов. Хвоитные вызовы не будут добавлять ссылку на вызов в цепочку вызовов по умолчанию. Обратите внимание, что "цепочка вызовов" здесь в основном является синонимом "продолжения" в смысле "продолжения первого класса".
  • При передаче сообщений цепочка вызовов представляет собой цепочку заблокированных методов, каждая из которых ожидает ответа от метода до него в цепочке. Метод, который вызывает другой, является "клиентом"; вызванный метод является "поставщиком" (я целенаправленно не использую "сервис", хотя "поставщик" не намного лучше). Путаница обмена сообщениями представляет собой набор несвязанных цепочек вызовов. Эта структура клубок похожа на наличие нескольких потоков или пакетов процессов. Когда метод просто эхо-ответа другого метода как собственный, метод может заставить своего клиента ждать своего поставщика, а не самого себя. Обратите внимание, что это дает несколько более общую оптимизацию, которая требует оптимизации координации, а также продолжения. Если конечная часть метода не зависит от ответа (и ответ не зависит от данных, обработанных в последней части), этот метод может продолжаться, как только он передаст свою клиентскую зависимость ожидания от поставщика. Это аналогично запуску нового потока, где конечная часть метода становится главной функцией потока, за которой следует хвостовой вызов стиля вызова-стека.

Что такое стиль обработки событий?

При обработке событий в запросах нет ответов, а обработчики не ждут, поэтому "цепочки вызовов" (как используется выше) не является полезной концепцией. Вместо путаницы у вас есть очереди приоритетов событий, которые принадлежат каналам, и подписки, которые являются списками пар прослушивателя-обработчика. В некоторых случаях управляемые архитектуры, каналы являются свойствами слушателей; каждый слушатель владеет ровно одним каналом, поэтому каналы становятся синонимом слушателей. Вызов означает запуск события по каналу, который вызывает все подписанные обработчики-слушатели; параметры передаются как свойства события. Код, который будет зависеть от ответа в другом стиле, становится отдельным обработчиком при обработке событий с ассоциированным событием. Хвост вызова будет обработчиком, который запускает событие на другом канале и больше ничего не делает. Оптимизация звонков может включать повторную подписку слушателей для события со второго канала на первый или, возможно, с обработчиком, который активировал событие на первом канале, вместо этого загорается второй канал (оптимизация, выполняемая программистом, а не компилятором/переводчик). Вот как выглядит первая оптимизация, начиная с не оптимизированной версии.

  • Listener Alice подписывается на "инаугурацию" на BBC News, используя обработчик "party".
  • Алиса возбуждает событие "выборы" на канале BBC News.
  • Боб слушает "выборы" в BBC News, поэтому вызывается обработчик Bob "openPolls".
  • Боб подписывается на событие "инаугурация" на канале CNN.
  • Боб запускает событие "голосование" на канале CNN
  • Другие события запускаются и обрабатываются. В конце концов, один из них (например, "выигрыш" ) вызывает событие "инаугурации" на CNN.
  • Боб запретил обработчик "инаугурации" в новостях BBC.
  • Вызывается обработчик именования Алисы.

И оптимизированная версия:

  • Listener Alice подписывается на "инаугурацию" на BBC News.
  • Алиса возбуждает событие "выборы" на канале BBC News.
  • Боб слушает "выборы" в BBC News, поэтому вызывается обработчик Bob "openPolls".
  • Боб подписывает любого, кто слушает "инаугурацию" на BBC News, на событие инаугурации на CNN *.
  • Боб запускает событие "голосование" на канале CNN
  • Другие события запускаются и обрабатываются. В конце концов, один из них запускает событие "инаугурации" на CNN.
  • Обработчик инаугурации Алисы вызывается для события инаугурации на CNN.

Обратите внимание, что хвостовые вызовы сложнее (несостоятельны?) при обработке событий, потому что они должны учитывать подписки. Если бы Алиса позже отказалась от подписки на "инаугурацию" на BBC News, подписка на инаугурацию на CNN также должна была быть отменена. Кроме того, система должна убедиться, что она ненадлежащим образом вызывает обработчик несколько раз для слушателя. В приведенном выше оптимизированном примере, что, если есть еще один обработчик для "инаугурации" на CNN, который запускает "инаугурацию" на BBC News? Событие "вечеринка" Алисы будет выпущено дважды, что может вызвать у нее проблемы на работе. Одно из решений заключается в том, чтобы * Боб отписаться от всех слушателей от "инаугурации" на BBC News на шаге 4, но затем вы вводите еще одну ошибку, в которой Алиса будет пропускать события инаугурации, которые не приходят через CNN. Возможно, она хочет отпраздновать как американские, так и британские инаугурации. Эти проблемы возникают из-за того, что существуют различия, которые я не делаю в модели, возможно, основанные на типах подписки. Например, может быть, есть специальный вид одноразовой подписки (например, обработчики сигналов System-V), или некоторые обработчики отписывают себя и хвост вызова оптимизация применяется только в этих случаях.

Что дальше?

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

Получите все это? Я все еще пытаюсь переварить его сам.

Литература:

Ответ 8

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

Ответ 9

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

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

Вызовы являются хвостовыми рекурсивными, что означает, что это "своего рода" goto. Убедитесь, что вы понимаете, что такое хвостовая рекурсия, прежде чем пытаться понять или написать какой-либо код. Чтение книги Джо Армстронга, вероятно, неплохая идея, если вы новичок в Erlang.

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