День 11 · Своя функция: объявление, параметры, возврат · страница 6 из 7

Свои функции в задаче

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


Когда кусок пора выносить

Признаков три, и ни один из них не про длину:

И обратное: выносить ради одной строки не нужно. Функция func sum(a int, b int) int { return a + b } читается хуже, чем a + b.

Как выносить — по шагам. Возьмём повтор с первой страницы:

Код для чтения · разбираем, набирать не нужно
	qtyA := 14
	boxesA := qtyA / 6
	if qtyA%6 != 0 {
		boxesA++
	}
  1. Обвести кусок. Где он начинается и где кончается: три строки, от деления до закрывающей скобки if.
  2. Что входит. Какие значения кусок берёт снаружи: qtyA и число 6. Это будущие параметры: qty и size.
  3. Что выходит. Какое одно значение кусок производит: boxesA. Это тип результата — int.
  4. Имя. Существительное, потому что возвращается значение: boxes.
  5. Перенести. Заголовок func boxes(qty int, size int) int, тело — тот же кусок, где имена заменены на параметры, и return в конце.
  6. Заменить. Каждый исходный кусок — вызовом: boxes(14, 6).
  7. Проверить. Прогнать программу на тех же вводах и убедиться, что вывод не изменился ни на символ.

Шаг 7 — не формальность. Перенос кода ломается на мелочах: не то имя подставили, забыли +1, перепутали параметры местами. Поэтому прогон «до» и «после» на одних и тех же данных — обязательная часть работы. Завтрашние дни доведут эту мысль до конца: в дне 14 такие прогоны превратятся в тесты, которые запускаются сами.


Попробуйте сейчас: вынести повтор в функцию.

Цель: report печатает ту же сводку, что и раньше, но расчёт коробок написан в программе один раз.

1. Прочитайте условие и запустите программу как есть — запомните вывод:

▶ Выполните
cd ~/gocourse/day11/report
cat TASK.txt
go run . < example.txt

2. Объявите свою функцию по шагам 1–6 из раздела выше и замените все три куска вызовами. Имя придумайте сами.

3. Прогоните на тех же данных и ещё на одном вводе — вывод должен совпасть с тем, что вы запомнили:

▶ Выполните
go run . < example.txt
printf '5\n5\n10\n1\n' | go run .
gofmt -l .

Готово, когда: вывод на example.txt не изменился, в main остались только чтение чисел и печать — ни деления, ни остатка, ни ++, ни цикла, — а gofmt -l . ничего не печатает: пункты report, split и code_fmt.


Правило с порогом внутри функции

Ситуация. Цена за штуку зависит от размера заказа: от какого-то количества она становится ниже, и новая цена действует на весь заказ. Такое правило в main разворачивается в несколько строк, и каждый раз, когда цена понадобится ещё где-то, эти строки приходится повторять.

Внутри функции оно занимает ровно столько места, сколько занимает в условии задачи:

Код для чтения · разбираем, набирать не нужно
package main

import "fmt"

func cost(qty int, price int, bulk int, bulkPrice int) int {
	if qty >= bulk {
		return qty * bulkPrice
	}
	return qty * price
}

func main() {
	fmt.Println("10 шт.:", cost(10, 4500, 20, 3900), "коп.")
	fmt.Println("20 шт.:", cost(20, 4500, 20, 3900), "коп.")
}

Что увидите.

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day11/scratch$ go run .
10 шт.: 45000 коп.
20 шт.: 78000 коп.

Как это читать. Здесь ранний выход с третьей страницы работает в функции с результатом: если условие истинно, return отдаёт ответ и на этом всё; строка после if выполнится только тогда, когда условие ложно. else не нужен — он ничего бы не добавил.

Четыре параметра для такой функции — многовато; читать вызов cost(10, 4500, 20, 3900) тяжело, и порядок аргументов легко перепутать (пятая страница). Когда цена и порог у задачи одни и те же, их пишут внутри функции, а параметром оставляют только то, что меняется от вызова к вызову:

Код для чтения · разбираем, набирать не нужно
package main

import "fmt"

func cost(qty int) int {
	if qty >= 20 {
		return qty * 3900
	}
	return qty * 4500
}

func main() {
	fmt.Println("10 шт.:", cost(10), "коп.")
	fmt.Println("20 шт.:", cost(20), "коп.")
}
Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day11/scratch$ go run .
10 шт.: 45000 коп.
20 шт.: 78000 коп.

Такой выбор — не «всегда правильный». Пока цена одна, короткая форма удобнее; когда цен станет несколько, их снова придётся передавать снаружи. В сегодняшнем задании cost цена и порог — ваши, они написаны в условии, и внутри функции им самое место.

Границу проверяйте отдельно. «От 20 штук» — это qty >= 20, а не qty > 20. Ошибка на единицу из дня 08 никуда не делась: заказ ровно в 20 штук должен посчитаться по новой цене. Проверить это можно только вводом, где количество равно порогу, — в скрытых случаях проверки такой ввод есть.


Попробуйте сейчас: задание cost.

Цель: cost/main.go считает стоимость каждого заказа своей функцией и печатает итог.

1. Прочитайте условие — цена, порог и вторая цена в нём ваши:

▶ Выполните
cd ~/gocourse/day11/cost
cat TASK.txt

2. Напишите программу в main.go. Начните с функции: заголовок func cost(qty int) int и две строки с return. Потом main: чтение строк до конца ввода — как в дне 08, — вызов cost на каждой строке, счётчик заказов и сумма.

3. Проверьте себя на примере из условия, на пустом вводе и на числе, равном порогу:

▶ Выполните
go run . < example.txt
go run . < /dev/null
gofmt -l .

Готово, когда: на example.txt вывод совпадает с примером в TASK.txt, на пустом вводе печатается одна строка Заказов не было, а gofmt -l . молчит — пункты cost и code_fmt.


Проверка «до и после»

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

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day11/report$ go run . < example.txt > ~/gocourse/day11/scratch/before.txt
stagiaire@lab:~/gocourse/day11/report$ cat ~/gocourse/day11/scratch/before.txt
Склад A: 3 кор.
Склад B: 5 кор.
Склад C: 2 кор.
Всего коробок: 10

Дальше выносите расчёт в функцию, а когда программа снова заработает, сравните вывод с сохранённым — командой diff из дня 08:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day11/report$ go run . < example.txt > after.txt
stagiaire@lab:~/gocourse/day11/report$ diff ~/gocourse/day11/scratch/before.txt after.txt
stagiaire@lab:~/gocourse/day11/report$ rm after.txt

diff молчит — значит, вывод совпал буква в букву. Это и есть ответ на вопрос «ничего ли я не сломал».

Одного примера мало: он проверяет один путь через программу. Возьмите хотя бы ещё два — с нулём и с числом, которое делится нацело, — и прогоните оба. В дне 14 такие наборы примеров превратятся в тест, который запускается одной командой и сам сравнивает вывод с ожидаемым; пока это делаете вы руками.


Перед тем как сдавать

Три команды, которые стоит выполнять в каждой папке с программой, прежде чем считать её готовой. Они знакомы с блоков 1 и 2, новое — только то, что теперь у вас в файле несколько функций:

Команда Что проверяет
gofmt -l . отступы и пробелы; молчит — значит, всё на месте
go vet . подозрительные места, которые компилятор пропускает
go run . < example.txt что программа делает то, что написано в условии

gofmt расставит отступы в теле функций сам, а вот пустую строку между функциями он не добавит — её ставите вы. Принято: одна пустая строка между объявлениями, внутри тела пустыми строками разделяют смысловые куски.


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

Что видите Что это значит Что делать
после выноса вывод изменился при переносе потерялась строка или перепутаны имена сравнить с прежним выводом на том же вводе; вернуть оригинал: cp ~/.course/orig/day11/report/main.go ~/gocourse/day11/report/main.go и вынести заново
пункт split красный: «расчёт из main уехал не весь» один из трёх кусков остался на месте или коробки досчитываются в main другим способом заменить вызовом все три; в main оставить только чтение и печать
пункт split красный, а функция есть деление осталось в main — например, в строке «всего» всё, что считает коробки, должно быть внутри функции
заказ ровно в порог считается по старой цене строгое > вместо >= сверить с условием: «столько-то и больше» — это >=
Итого не сходится с суммой строк сумма считается не из того же значения, что печатается считать один раз: положить результат cost(qty) в переменную и использовать её и там и там
на пустом вводе печатается Итого: 0 коп. проверка на «ни одного заказа» стоит не там считать заказы и проверять счётчик после цикла, как в дне 08

Дальше: как сохранить сделанное так, чтобы можно было вернуться — course next