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

Написать функцию по чужому описанию

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


Описание функции — это договор

Полное описание отвечает на четыре вопроса.

Вопрос Где ответ Пример
Как называется и что принимает заголовок func pallets(boxes, perPallet int) int
В чём измеряются аргументы текст рядом boxes — коробок, perPallet — коробок на поддон
Что возвращает текст и тип сколько поддонов нужно
Что на границах текст неполный поддон — тоже поддон; при нуле коробок — 0

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


Разбор: поддоны

Описание. func pallets(boxes, perPallet int) int — сколько поддонов нужно на boxes коробок, если на один поддон входит perPallet коробок. Неполный поддон тоже занимает место. Если коробок 0, поддонов 0.

Шаг 1. Примеры на бумаге — до кода. Четыре строки, из них три пограничные:

boxes perPallet Поддонов Почему
0 8 0 коробок нет
1 8 1 одна коробка занимает целый поддон
8 8 1 вошло ровно, лишнего поддона нет
9 8 2 одна лишняя коробка требует второго

Шаг 2. Тело. Это знакомое «разделить и округлить вверх» из дня 05:

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

import "fmt"

func pallets(boxes, perPallet int) int {
	return (boxes + perPallet - 1) / perPallet
}

func main() {
	fmt.Println(pallets(0, 8))
	fmt.Println(pallets(1, 8))
	fmt.Println(pallets(8, 8))
	fmt.Println(pallets(9, 8))
}

Шаг 3. Сверка с таблицей.

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day13/scratch$ go run .
0
1
1
2

Четыре числа совпали с четырьмя строками таблицы. Обратите внимание, что main здесь — не программа, а способ проверить функцию: четыре вызова с известными ответами. Завтра, в день 14, эту же работу будет делать тест, и делать её будет каждый раз сам.

Если бы в теле стояло boxes/perPallet + 1, первые две строки таблицы разошлись бы: для 0 получилось бы 1, для 8 — 2. Пограничные примеры и нужны, чтобы поймать это до сдачи.


Как писать по описанию: порядок

  1. Перепишите заголовок дословно. Имя, порядок параметров, типы — как в описании. Поменяли порядок параметров местами — вызовы в чужом коде сломаются молча: типы-то сойдутся.
  2. Выпишите примеры, включая границы. Три-четыре строки таблицы «дано — должно получиться». Ответы считаете вы, а не программа.
  3. Напишите тело. Одно дело, без печати и без чтения ввода, если в описании не сказано иначе.
  4. Проверьте на своих примерах — вызовами из main или сверкой с посчитанным на бумаге.
  5. gofmt, go vet, коммит.

Функция по описанию ничего не печатает не из вредности. Печать — забота того, кто её вызвал: он решает, куда и в каком виде показать число. Функция, которая печатает сама, годится ровно для одного места в программе.


Чего в описании нет — о том и спрашивают

Описание в тикете почти никогда не бывает полным. Дырки видны, если задать ему четыре вопроса из таблицы выше. Вот типичное описание из рабочей переписки и вопросы к нему:

«Нужна функция, которая считает скидку по сумме заказа. Если заказ большой, скидка десять процентов».

Вопрос к описанию Почему он важен
«большой» — это сколько и в чём: в рублях, копейках, штуках? без числа и единицы функцию не написать
граница входит? заказ ровно на пороге — скидка есть? на этом расходятся чаще всего
скидка возвращается или сразу вычитается из суммы? это две разные функции с разными результатами
дробная копейка отбрасывается или округляется? 86.415 копейки — это 86 или 87
отрицательная сумма бывает? если да, нужно поведение; если нет, так и записать

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

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

Код для чтения · разбираем, набирать не нужно
// pallets возвращает число поддонов на boxes коробок: неполный поддон
// тоже занимает место целиком. При нуле коробок возвращает 0.
func pallets(boxes, perPallet int) int {
	return (boxes + perPallet - 1) / perPallet
}

Такой комментарий в Go пишут над объявлением и начинают с имени функции — так его показывает всплывающая подсказка редактора и команда go doc -u:

Пример · только посмотреть, набирать не нужно
stagiaire@lab:~/gocourse/day13/scratch$ go doc -u pallets
func pallets(boxes, perPallet int) int
    pallets возвращает число поддонов на boxes коробок: неполный поддон тоже
    занимает место целиком. При нуле коробок возвращает 0.

Ключ -u здесь обязателен. Без него go doc показывает только функции, имя которых начинается с большой буквы, — те, которыми пакет делится с другими пакетами (про большую и маленькую букву в имени — в блоке 5). Все свои функции в курсе начинаются с маленькой, поэтому go doc pallets без ключа отвечает doc: no symbol pallets in package …, а go doc -u показывает и их.


Ошибки вокруг функций

Все сообщения ниже — с этого стенда, Go 1.27.1.

Функции нет. Задание report начинается именно с этого:

Вывод · это печатает программа, набирать не нужно
# day13/report
./main.go:17:7: undefined: discount

undefined — имя в пакете не объявлено. Либо функция ещё не написана, либо опечатка в имени, либо она объявлена в другом каталоге: файлы одного пакета лежат в одной папке, и report/main.go не видит функций из split/main.go.

Не на всяком пути есть return:

Код для чтения · разбираем, набирать не нужно
func rubles(kopecks int) string {
	fmt.Sprintf("%d.%02d", kopecks/100, kopecks%100)
}
Вывод · это печатает программа, набирать не нужно
# day13/scratch
./main.go:7:1: missing return

Колонка указывает на закрывающую скобку функции: компилятор дошёл до её конца и не встретил return. Здесь вызов Sprintf есть, а результат никуда не отдан.

Число аргументов не сошлось. Две строки на каждое сообщение — have показывает, что передали, want — что объявлено:

Вывод · это печатает программа, набирать не нужно
# day13/scratch
./main.go:10:23: not enough arguments in call to packCost
	have (number)
	want (int, int)
./main.go:11:30: too many arguments in call to packCost
	have (number, number, number)
	want (int, int)

Две функции с одним именем. В Go нельзя объявить две функции с одинаковым именем, даже если у них разное число параметров:

Вывод · это печатает программа, набирать не нужно
# day13/scratch
./main.go:9:6: boxes redeclared in this block
	./main.go:5:6: other declaration of boxes

Вторая строка показывает, где объявление было первым. Лечение — разные имена: boxesFor и boxesForPallet.

Функция объявлена внутри main. Так бывает, когда кусок кода оборачивают в func прямо на месте:

Вывод · это печатает программа, набирать не нужно
# day13/scratch
./main.go:6:7: syntax error: unexpected name boxesFor, expected (

Свои функции объявляют на верхнем уровне файла — рядом с main, до или после него, но не внутри.


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

Цель: программа report собирается и печатает три строки, а функция discount ведёт себя так, как написано в условии.

1. Прочитайте условие и посмотрите, что сейчас говорит компилятор:

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

2. Выпишите на бумаге четыре примера — обычный случай и три границы из раздела условия «ГРАНИЦЫ»: сумма ровно равна порогу, процент 0, сумма ниже порога.

3. Напишите функцию — в main.go рядом с main или в отдельном файле discount.go в той же папке. Проверьте:

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

4. Закоммитьте:

▶ Выполните
cd ~/gocourse/day13
git add .
git commit -m "report: написал discount"

Готово, когда: программа печатает заказ, скидку и сумму к оплате, и эти три числа совпали с посчитанными на бумаге — пункт report. Проверка прогоняет вашу функцию на своих числах, в том числе на границах.


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

Что видите Что это значит Что делать
./main.go:17:7: undefined: discount функции нет в пакете написать её в той же папке, package main
./main.go:7:1: missing return путь без return вернуть значение на каждом пути, в том числе из if
пункт report красный, а программа печатает правдоподобно на границе поведение другое, чем в условии перечитать условие: «не меньше» — это порог включительно
скидка на копейку больше ожидаемой округление вверх вместо отбрасывания целое деление уже отбрасывает дробь, math.Ceil не нужен
функция печатает и возвращает дел два печать оставить в main
syntax error: unexpected name discount, expected ( func discount написана внутри main вынести объявление на верхний уровень файла
функция написана в split, а нужна в report пакет — это папка перенести файл в папку задания

Дальше: когда разбиение на функции вредит, и словарик — course next