Skip to content

Latest commit

 

History

History
368 lines (300 loc) · 21.5 KB

03-From-Repeated-Expressions-to-Functions.md

File metadata and controls

368 lines (300 loc) · 21.5 KB

2 От Повторяющихся Выражений к Функциям

2.1 Пример: Вес на Луне

Представьте себе, что нам нужно оснастить группу космонавтов, которые должны высадиться на Луне. Нам среди прочего нужно выяснить, сколько каждый из них будет весить, находясь там. Мы уже знаем, какое выражение нужно написать, чтобы это сделать. Чтобы вычислить вес на Луне для каждого космонавта, нам нужно написать одно и то же выражение столько раз, сколько у нас есть человек в команде, заменяя в выражении лишь одно число --- вес на Земле.

  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)

дадут нам те же значения, что и выражения, которые мы писали вначале.

2.2 Пример: Японский Флаг

Давайте теперь напишем ещё одну функцию. Вспомним пример с японским флагом. Всякий раз, когда мы хотим получить флаг другого размера, нам приходится менять значение, связанное с переменной 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.

2.3 Тесты: Использование Примеров

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

Теперь, когда всё готово, нужны ли нам ещё те исходные примеры? Кажется, что их можно безболезненно выбросить. Однако, делать это всё же не следует. Существует одно важное правило, касающееся компьютерных программ, в котором утверждается, что программы имеют свойство развиваться. С течением времени, любая программа, которой кто-нибудь пользуется, меняется и разрастается, что может в конечном итоге привести к тому, что программа будет производить значения, отличные от тех, которые были изначально в примерах. Иногда это делается осознанно, а иногда это является результатом ошибок, которые были внесены в программу в процессе её изменения. Поэтому и важно сохранить эти примеры, на которые можно будет опираться в будущем, и если функция начнёт производить значения, которые расходятся с примерами, то вы сразу же об этом узнаете.

В 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 в теле функции и посмотрите, что из этого получится.

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

2.4 Аннотации Типов

Положим, мы попробовали применить операцию 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 может быть числом, нечётным числом, простым числом.

2.5 Пошаговое Определение Функций

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

  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