День 13 · Разбить задачу на функции · страница 3 из 7

Как читать программу, разложенную на функции

Программу из одного main читают сверху вниз — это дни 04 и 05. С функциями порядок чтения другой: текст в файле лежит в одном порядке, а выполняется в другом. Этому и учимся: сегодняшний навык нужен каждый день на работе, где кода намного больше, чем помещается в голове.


Два правила чтения

  1. Начинайте с main и читайте его сверху вниз. Что бы ни лежало в файле выше, программа начинается с main. Функции, объявленные до или после него, — просто описания: они ничего не делают, пока их не позвали.
  2. Вглубь — только когда нужен ответ. Встретили вызов packCost(...) — сначала спросите себя, достаточно ли вам имени. Если вы читаете программу, чтобы понять, что она делает в целом, имени хватает. Если вам нужно значение или вы ищете ошибку — тогда идёте внутрь функции, дочитываете её до return и возвращаетесь в ту же строку main, откуда ушли.

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


Ситуация: программа коллеги

В каталоге ~/gocourse/day13/read лежит программа. Надо сказать, что она напечатает, не запуская её.

Код для чтения · разбираем, набирать не нужно
 1  // Программа для чтения, страница 3. Её не надо менять и пока не надо
 2  // запускать: сначала ответ на вопрос q2, потом уже go run .
 3  package main
 4
 5  import "fmt"
 6
 7  func perTrip(total, trips int) int {
 8  	return (total + trips - 1) / trips
 9  }
10
11  func loadPlan(total, trips int) int {
12  	load := perTrip(total, trips)
13  	if load > 40 {
14  		load = 40
15  	}
16  	return load
17  }
18
19  func main() {
20  	total := 150
21  	for trips := 2; trips <= 4; trips++ {
22  		fmt.Println(trips, loadPlan(total, trips))
23  	}
24  }

Номера строк слева — как в VS Code: от начала файла, считая комментарии и пустые строки. В самом файле их, конечно, нет.

Обзор по правилу чтения 1. В main три строки: переменная total, цикл по trips от 2 до 4 включительно и печать двух чисел. Значит, вывод — три строки, в каждой два числа. Первое число — trips, второе — результат loadPlan. Что такое loadPlan, пока неважно: чтобы узнать форму вывода, внутрь заходить не нужно.

Вглубь по правилу чтения 2. Второе число нам как раз нужно, поэтому идём внутрь — сначала в loadPlan, из неё в perTrip.


Два правила трассировки к прежним

Правила 1–22 из дней 02, 06, 08, 11 и 12 действуют. Держать в голове пять прошлых страниц не нужно: все правила собраны одним списком в памятке блока 3 — course page 15 2, раздел «Трассировка: правила 1–24». Открывайте его рядом с таблицей; по нему же сверяется таблица на контрольной точке. Сегодня к прежним правилам добавляются два.

23. Одноимённые переменные разных функций — разные колонки. В заголовке пишется, чья переменная: total (main), total (loadPlan). Параметр — копия (день 11), а не та же ячейка, поэтому и колонка своя. Пока в «где мы» стоит main, в колонках loadPlan прочерк, и наоборот — это правило 20 дня 11, просто теперь видно, к какой из двух total он относится.

24. Вложенный вызов раскрывают отдельной таблицей. Строка с вызовом внутри функции по правилу 22 — один шаг: результат уже присвоен, внутрь не идём. Если ответ нужен и оттуда, для этого вызова ведут вторую таблицу, а в первую подставляют её результат — сколько бы уровней ни было.


Трассировка с уходом в функцию

Первый проход цикла, trips равно 2. Идём по правилу чтения 2 — вглубь только там, где нужен ответ, — поэтому вызов perTrip на строке 12 пока один шаг (правило 22).

шаг строка где мы total (main) trips (main) total (loadPlan) trips (loadPlan) load
1 20 main 150 - - - -
2 21 main 150 2 - - -
3 22 main 150 2 - - -
4 11 loadPlan 150 2 150 2 -
5 12 loadPlan 150 2 150 2 75
6 13 loadPlan 150 2 150 2 75
7 14 loadPlan 150 2 150 2 40
8 16 loadPlan 150 2 150 2 40
9 22 main 150 2 - - -

Как это читать:


Тот же шаг, но на уровень глубже

Шаг 5 закрыт одной клеткой: load равна 75. Если нужно не поверить, а проверить, для этого вызова ведут вторую таблицу — правило 24. Колонок в ней всего две: у perTrip только свои параметры.

шаг строка где мы total (perTrip) trips (perTrip)
5.1 12 loadPlan - -
5.2 7 perTrip 150 2
5.3 8 perTrip 150 2
5.4 12 loadPlan - -

Шаги 5.1 и 5.4 — та же строка 12, уход и возврат. На шаге 5.2 параметры получили копии, на 5.3 считается (150 + 2 - 1) / 2, то есть 151 / 2, то есть 75: деление целых отбрасывает дробь (день 03). Результат 75 уезжает в клетку load шага 5 первой таблицы — и вторая таблица больше не нужна. Так и работают вместе оба правила чтения: таблица на одном уровне, а вглубь — ровно там, где число вызывает сомнение.

Дальше цикл повторяется для trips = 3 и trips = 4. Числа считаются так же: (150 + 3 - 1) / 3 и (150 + 4 - 1) / 4.


Попробуйте сейчас: читаем program.go по вызовам.

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

1. Откройте программу и заготовку таблицы:

▶ Выполните
cd ~/gocourse/day13/read
cat program.go
code ~/gocourse/day13/read/table.txt

2. Заполните table.txt: это второй проход цикла, trips равно 3. Шаги и номера строк уже стоят — меняйте только знаки ?: имя функции в колонке «где мы» и значения переменных после того, как строка выполнилась. Прочерки оставьте как есть, строк не добавляйте. Вызов perTrip на строке 12 — один шаг (правило 22); если его число вызывает сомнение, распишите этот вызов отдельной таблицей на бумаге (правило 24).

3. Тем же порядком пройдите третий проход, trips равно 4, — уже на бумаге. Выпишите, что напечатает третья строка вывода: два числа через пробел.

4. Сдайте их:

▶ Выполните · выделенное замените своим
course answer day13.q2 ЧИСЛО ЧИСЛО

Вместо ЧИСЛО ЧИСЛО — третья строка вывода целиком, два числа через пробел.

5. Только теперь запустите и сверьте все три строки:

▶ Выполните
go run .

Готово, когда: пункты trace и q2 в course check 13 зелёные, а третья строка запуска совпала с отправленной. Не совпало — найдите шаг, на котором ваша таблица разошлась с программой, и отправьте ответ заново.


Переход к объявлению: F12

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

Откройте папку дня в VS Code (code ~/gocourse/day13), а в ней read/program.go. Поставьте курсор на имя loadPlan в строке 22 — там, где эта функция вызывается:

Что нажать Что произойдёт
F12 или Ctrl+клик курсор прыгает к строке 11 — туда, где loadPlan объявлена
Alt+← вернуться назад, туда, откуда прыгнули
Shift+F12 показать все места, где эта функция вызывается
наведение мыши на имя всплывающая подсказка с заголовком функции

Ровно так читают чужой код: F12 — вглубь, Alt+← — обратно. Это и есть правило чтения 2, только руками редактора.

Полный список клавиш и настройка редактора — в гайде по рабочему месту: docs/setup/ide.md, раздел «Клавиши, которые понадобятся с первого дня».

Если F12 никуда не прыгает, значит, не работает языковой сервер gopls — редактор просто не знает, где что объявлено. Что делать, написано там же, в разделе «Если что-то не так».


Дерево вызовов

Когда функций становится больше трёх, держать в голове, кто кого зовёт, уже не выходит. Помогает набросок на бумаге — дерево вызовов: от main вниз, по одной ветке на вызов.

Возьмём программу приёмки со страницы 1 и добавим к ней одну связь: представим, что packCost больше не получает порог скидки аргументом, а спрашивает у bulkDelivery со страницы 2, крупная ли отгрузка. Тогда дерево выглядит так:

Образец · набирать не нужно
main
 ├─ boxesFor
 ├─ packCost
 │   └─ bulkDelivery
 └─ rubles

Читается сверху вниз и слева направо: main зовёт три функции, а одна из них, packCost, внутри зовёт четвёртую. Дерево строится за две минуты и отвечает сразу на несколько вопросов:

Листья дерева — функции, которые никого не зовут (boxesFor, rubles, bulkDelivery). Их проще всего и читать, и проверять: результат зависит только от аргументов. С них и начинают, когда разбираются в незнакомом коде снизу вверх.

То же дерево редактор умеет показывать сам: Shift+F12 на имени функции даёт список мест, откуда её зовут, — это ветка вверх. F12 на вызове — ветка вниз.


Как этим деревом сужают поиск ошибки — в дополнительном чтении дня (course extra 13), раздел «Где искать ошибку: сужаем по дереву вызовов». Приём тот же, что деление поиска пополам в дне 09, только делятся не строки, а функции.


Что кажется и как на самом деле

Что кажется Как на самом деле
функции выполняются в том порядке, в каком написаны в файле выполняются в порядке вызовов; порядок объявления в файле не важен вовсе
раз perTrip объявлена первой, она и выполнится первой она выполнится, когда её позовут — внутри loadPlan
переменная load останется после возврата она исчезает вместе с вызовом
total внутри loadPlan — та же переменная, что в main это её копия; имена совпали случайно
чтобы понять программу, надо прочитать все функции достаточно main, пока не нужен конкретный ответ
печать происходит там, где посчитано значение печатает только тот, в чьём коде стоит Println

Что может пойти не так

Что видите Что это значит Что делать
предсказание не сошлось на одном числе ошибка внутри одного вызова расписать этот вызов отдельной таблицей — правило 24
потеряли, куда возвращаться после вызова читаете вглубь без пометки отмечать строку возврата: в редакторе это Alt+←
F12 не работает нет gopls или папка открыта не как корень модуля открыть именно ~/gocourse/day13, проверка — docs/setup/ide.md
читаете функцию, а она никем не вызвана мёртвый код или опечатка в имени при вызове Shift+F12: если вызовов нет, функция не работает
в двух функциях переменные с одним именем — путаница это разные переменные колонка на каждую, в заголовке имя функции — правило 23
в таблице строка вызова записана один раз забыт возврат уход и возврат — два шага, правило 17 дня 11
строка заголовка функции пропущена параметры «появились сами» заголовок — шаг, на нём параметры получают копии, правило 18
после возврата в колонках функции остались числа её переменные «доживают» до следующих шагов они исчезают вместе с вызовом: прочерк, правило 20

Дальше: как функции получают данные — course next