Представьте себе, что нам нужно оснастить группу космонавтов, которые должны высадиться на Луне. Нам среди прочего нужно выяснить, сколько каждый из них будет весить, находясь там. Мы уже знаем, какое выражение нужно написать, чтобы это сделать. Чтобы вычислить вес на Луне для каждого космонавта, нам нужно написать одно и то же выражение столько раз, сколько у нас есть человек в команде, заменяя в выражении лишь одно число --- вес на Земле.
80 * 1/6
70 * 1/6
95 * 1/6
100 * 1/6
Но переписывать одно и то же много раз --- довольно скучно и неэффективно. К тому же, чем больше мы будем это переписывать, тем больше вероятность того, что где-нибудь мы допустим ошибку, забудем где-то что-то дописать, или, наоборот, напишем что-нибудь лишнее. В данном выражении есть, так сказать, фиксированная часть, и есть часть изменяющаяся. Изменяющаяся часть --- вес конкретного космонавта --- это то, что мы вынуждены прописывать каждый раз для каждого отдельного космонавта. А вот фиксированная --- это то, что всегда остаётся неизменным, и потому писать её каждый раз при использовании выражения нам бы не хотелось. Это то, чего избежать как раз можно и нужно.
Сделать это мы можем, написав функцию. Функция получает один или несколько параметров, которые составляют изменяющуюся часть. Создавать функцию мы будем так:
-
Напишем несколько примеров нужных нам вычислений. Рассмотрим разные веса и посчитаем, какими они будут на Луне.
-
Определим, какие части являются фиксированными (в данном случае это * 1/6), а какие --- меняющимися (
80
,70
,95
,100
). -
Для каждой меняющейся части выберем имя (пусть в нашем случае это будет
earth-weight
), которое мы будем использовать в качестве параметра функции. -
Перепишем выражение с использованием параметра (или параметров):
earth-weight * 1/6
-
Придумаем для функции осмысленное название, например,
moon-weight
. -
Напишем необходимую конструкцию для функции вокруг выражения:
fun <ИмяФункции>(<Параметры>): <Выражение> end
В итоге у нас должно получиться следующее:
fun moon-weight(earth-weight):
earth-weight * 1/6
end
Выражение earth-weight * 1/6
называется здесь телом функции.
Как нам теперь ею воспользоваться? С точки зрения Pyret, moon-weight
--- это
просто ещё один оператор, типа num-sqrt
или overlay
.
Поэтому выражения
moon-weight(80)
moon-weight(70)
moon-weight(95)
moon-weight(100)
дадут нам те же значения, что и выражения, которые мы писали вначале.
Давайте теперь напишем ещё одну функцию. Вспомним пример с японским флагом.
Всякий раз, когда мы хотим получить флаг другого размера, нам приходится менять
значение, связанное с переменной unit
и перезапускать целиком всю программу.
Вместо этого мы должны написать функцию, которая генерирует японские флаги.
Сколько параметров нужной этой функции? Вернувшись к нашему коду, можно
увидеть, что меняется только перменная unit
. Всё остальное вычисляется на
основе её значения. Поэтому нам нужно превратить unit
в параметр, а всё
остальное оставить без изменений.
fun japan-flag(unit):
bg-width = unit * 3
bg-height = unit * 2
circ-rad = 3/5 * 1/2 * bg-height
red-circ = circle(circ-rad, "solid", "red")
white-rect = rectangle(bg-width, bg-height, "solid", "white")
overlay(red-circ, white-rect)
end
тело этой функции создаёт несколько переменных и затем производит результат
overlay
выражения, который является изображением флага.
Теперь мы можем пользоваться этой функцией в правом окне:
>>> japan-flag(100)
>>> japan-flag(200)
>>> japan-flag(50)
без необходимости перезапускать программу после каждого изменения переменной
unit
.
В каждой функции выше мы начинали с примеров того, что мы хотим вычислить, эти примеры обобщались до некоторой формулы, из которой уже была сделана функция, которую мы использовали вместо изначальных выражений.
Теперь, когда всё готово, нужны ли нам ещё те исходные примеры? Кажется, что их можно безболезненно выбросить. Однако, делать это всё же не следует. Существует одно важное правило, касающееся компьютерных программ, в котором утверждается, что программы имеют свойство развиваться. С течением времени, любая программа, которой кто-нибудь пользуется, меняется и разрастается, что может в конечном итоге привести к тому, что программа будет производить значения, отличные от тех, которые были изначально в примерах. Иногда это делается осознанно, а иногда это является результатом ошибок, которые были внесены в программу в процессе её изменения. Поэтому и важно сохранить эти примеры, на которые можно будет опираться в будущем, и если функция начнёт производить значения, которые расходятся с примерами, то вы сразу же об этом узнаете.
В Pyret это делается просто. Каждая функция может также сопровождаться словом
where
, с помощью которого можно записать все необходимые примеры. Наша
функция лунного веса может быть модифицирована вот так:
fun moon-weight(earth-weight):
earth-weight * 1/6
where:
moon-weight(100) is 100 * 1/6
moon-weight(150) is 150 * 1/6
moon-weight(90) is 90 * 1/6
end
Когда функция написана таким образом, Pyret будет сверять ответы всякий раз,
когда вы запускаете программу, и непременно сообщать вам, если вы изменили
функцию таким образом, что она теперь не соответствует написанным примерам.
Попробуйте заменить 1/6
на 1/3
в теле функции и посмотрите, что из этого
получится.
Разумеется, практически невозможно допустить ошибку в такой простой функции, ведь тело функции почти совпадает с выражениями примеров. Однако, позднее мы увидим, что примеры будут в разы меньше тела функции, и с такой же лёгкостью утверждать о соответствии функции примерам уже будет нельзя. На самом деле, в реальном промышленном программировании профессиональные программисты всегда пишут такие примеры, которые они называют тестами, чтобы удостовериться в том, что их программы ведут себя так, как они этого ожидают.
Положим, мы попробовали применить операцию moon-weight
к строке:
>>> moon-weight('Гагарин')
Pyret в ответ на это сгенерирует ошибку, в которой говорится, что вы не можете умножать строки на числа. В такой маленькой функции это не так важно. Но если бы у вас была функция значительно большего размера, то получить такую ошибку откуда-то из глубины её недр было бы довольно неприятно. Хуже того, если эту функцию писали не вы, то вам бы пришлось вчитываться в тело функцию (которое, я повторяю, может состоять из, например, нескольких десятков строк), чтобы выяснить, какие виды данных данных эта функция потребляет, и какой вид данных производит.
К счастью, мы можем сделать кое-что получше. Pyret позволяет вам писать
аннотации функций, которые указывают её значения. Конкретно в случае
moon-weight
нам следует написать
fun moon-weight(earth-weight :: Number) -> Number:
earth-weight * 1/6
end
Теперь же достаточно лишь прочитать первую строку, чтобы узнать, что функция
получает в качестве параметра число (часть :: Number
) и производит также
число (часть -> Number
). Сохраните внесённые изменения в определении функции,
записав её с аннотациями, как это сделано выше, и нажав Run
; после чего в
правом окне попробуйте выполнить moon-weight('Гагарин')
. Изменилась ли
ошибка, которую сгенерировал Pyret? Если да, то о чём теперь там сказано?
Вернёмся теперь опять к нашей функции, рисующей японский флаг. Поскольку она принимает число и производит изображение, мы пишем:
fun japan-flag(unit :: Number) -> Image:
bg-width = unit * 3
bg-height = unit * 2
circ-rad = 3/5 * 1/2 * bg-height
red-circ = circle(circ-rad, "solid", "red")
white-rect = rectangle(bg-width, bg-height, "solid", "white")
overlay(red-circ, white-rect)
end
Обратите внимание на то, что аннотации не являются обязательной частью функций:
до этой главы наши функции их не имели. На самом деле, вы можете как
использовать аннотации в одном месте, так и не использовать в другом. Также вы
можете писать аннотации для любых переменных, не только для тех, которые
являются параметрами какой-нибудь функции. Например, переменные внутри
japan-flag
тоже могут быть аннотированы.
Полностью аннотированная функция выглядит так:
fun japan-flag(unit :: Number) -> Image:
bg-width :: Number = unit * 3
bg-height :: Number = unit * 2
circ-rad :: Number = 3/5 * 1/2 * bg-height
red-circ :: Image = circle(circ-rad, "solid", "red")
white-rect :: Image = rectangle(bg-width, bg-height, "solid", "white")
overlay(red-circ, white-rect)
end
Измените одну из аннотаций в теле функции на неверную, например такую:
red-circ :: Number = circle(circ-rad, "solid", "red")
В какой момент вы получаете ошибку? Когда вы нажимаете Run
или только тогда,
когда уже пытаетесь воспользоваться japan-flag
? К какой части вашей программы
вас отсылает ошибка?
Штуки, которые мы вставляем в аннотации --- Number
, Image
, String
, и т.
д. --- называются типами. Типы помогают нам различать разные виды данных.
Каждое значение имеет тип, и никакое значение не имеет больше одного типа.
Таким образом, 3
это Number
(и ничто иное), "hello"
это String
(и ничто
иное), и т. д. В некоторых языках программирования эти аннотации типов
проверяются перед тем как программа запускается, так что вы можете узнать о
потенциальных ошибках ещё до того, как запустите программу. В других языках вы
узнаете о них только во время исполнения программы. Pyret стремится
предоставить вам оба этих режима, поэтому вы сами можете выбрать тот, который
имеет смысл использовать в данной контексте.
В дальнейшем мы увидим, что мы можем "уточнять" типы так, что значение сможет
иметь больше одного уточнённого типа: 3
может быть числом, нечётным числом,
простым числом.
При написании функций полезно делать это поэтапно. Сначала дайте функции имя, удостоверьтесь в том, что вы понимаете её типы, и напишите небольшую документацию, чтобы напомнить пользователю и читателю вашей программы, возможно незнакомому с вашей функцией --- через несколько недель этим человеком можете быть вы! ---, о том, что она должна делать. Вот, к примеру, функция, вычисляющая зарплату по количеству отработанных часов.
fun hours-to-wages(hours :: Number) -> Number:
doc: "Вычисляет размер зарплаты, исходя из часов, с учётом переработок, из расчёта ₽250 в час"
end
Заметьте, что документация оставляет неясным момент, когда начинается «переработка». Будем считать, что переработка начинается после 40 часов в неделю.
Далее, выписываем примеры:
fun hours-to-wages(hours :: Number) -> Number:
doc: "Вычисляет размер зарплаты, исходя из часов, с учётом переработок, из расчёта ₽250 в час"
where:
hours-to-wages(40) is 10000
hours-to-wages(40.5) is 10187.5
hours-to-wages(41) is 10375
hours-to-wages(0) is 0
hours-to-wages(45) is 12250
hours-to-wages(20) is 5000
end
Примеры должны покрывать как минимум все различные случаи, упомянутые в определении данных. Здесь, например, важно отметить, что 40-й час не считается за переработку, а 41-й --- считается. Заметьте, что если мы напишем примеры так, как указано выше, то будет непонятно, с помощью каких вычислений мы получили такие ответы. Перепишем примеры для первых 40 часов, чтобы это прояснить:
hours-to-wages(0) is 0 * 250
hours-to-wages(20) is 20 * 250
hours-to-wages(40) is 40 * 250
Для >40 часов формула должна учитывать часы переработки. Будем считать, что каждый час после 40-го оплачивается в полуторном размере.
hours-to-wages(40.5) is (40 * 250) + ((40.5 - 40) * (250 * 1.5))
hours-to-wages(41) is (40 * 250) + ((41 - 40) * (250 * 1.5))
hours-to-wages(45) is (40 * 250) + ((45 - 40) * (250 * 1.5))
Глядя на эти примеры, мы можем определить, каким должно быть тело функции:
fun hours-to-wages(hours :: Number) -> Number:
doc: ```Вычисляет размер зарплаты, исходя из часов,
с учётом переработок, из расчёта ₽250 в час```
if hours <= 40:
hours * 250
else:
(40 * 250) + ((hours - 40) * (250 * 1.5))
end
where:
hours-to-wages(40) is 10000
hours-to-wages(40.5) is 10187.5
hours-to-wages(41) is 10375
hours-to-wages(0) is 0
hours-to-wages(45) is 11875
hours-to-wages(20) is 5000
end
Функция hours-to-wages
всегда предполагает почасовую оплату равной ₽250. Мы
можем изменить функцию так, чтобы она высчитывала зарплату по другому тарифу,
например, по ₽300/час, заменив все вхождения константы 250
на 300
в теле
функции:
fun hours-to-wages-250(hours :: Number) -> Number:
doc: ```Вычисляет размер зарплаты, исходя из часов,
с учётом переработок, из расчёта ₽300 в час```
if hours <= 40:
hours * 300
else:
(40 * 300) + ((hours - 40) * (300 * 1.5))
end
end
Мы также можем сделать ещё несколько копий функции для работающих за ₽450/час, за ₽500/час и т. д. Однако, можно ещё --- и это несложно сделать --- изменить функцию так, чтобы она работала с произвольной почасовой оплатой. Достаточно убрать из тела функции соответствующую константу и заменить её новым параметром.
fun hours-to-wages-at-rate(hours :: Number, rate :: Number) -> Number:
doc: ```Вычисляет размер зарплаты, исходя из количества часов и размера
почасовой оплаты, с учётом переработок```
if hours <= 40:
hours * rate
else:
(40 * rate) + ((hours - 40) * (rate * 1.5))
end
end