SICP. Функции

Определяемся с терминами

Вначале нужно объяснить путаницу с терминами. В коде программы может быть маленькая подпрограмма и для неё нужно какое-то название. Таких названий три: функция, процедура и метод.
Функция — принимает какое-то значение, что-то делает и возвращает другое значение.
Некоторые функции не принимают значения, но всё равно что-то возвращают.
Процедура — принимает и ничего не возвращает.
Метод — это функция или процедура, но она относится к классу или объекту.
Вот автомат для продажи чипсов — это объект. У него есть функция: выдавать чипсы, когда получил деньги. Поморгать вывеской с какой-нибудь рекламой — метод. Но тут они принадлежат к объекту «чипсовый автомат», поэтому их следовало бы назвать методы.

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

Функция как чёрный ящик

В прошлой статье был пример с квадратным корнем, теперь будем возводить в квадрат.
Берём калькулятор: \(7^2\), получается 49. Мы даём ему задачу и ждём результата, не задумываемся как он считает и что там происходит, оно нам не надо.
В программировании это называется «чёрный ящик» — использование чего-то без понимания внутренних механизмов. Ещё так называют один из видов теста программы, но об этом когда-нибудь потом.

Сейчас сделаем чёрный ящик. Мы выбрали три разных алгоритма для вычисления квадрата целого числа. Они все принимают число и отдают квадрат, но код и алгоритм у всех свой.

Умножение на себя

Самый простой и компактный способ. Не интересно, давайте по-мудрёней.

def square(x):
    return x * x

Логарифмы и экспоненты

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

from math import log, exp

def square(x):
    return round(exp(log(x) * 2))

Рекурсивный

Суть в том, что нужно раскрутить алгоритм до n=0, чтобы было от чего отталкиваться.
n дойдёт до 0 и алгоритм начнёт сворачиваться.

def square(x, n=2):
    if n == 0:
        return 1
    return x * square(x, n - 1)



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

Формальные параметры

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

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

def summ(a, b):  # a, b тут формальные параметры
    buffer = a + b  # buffer – свободная переменная, a и b – связанные
    return buffer


current_summ = summ(3, 9)
print(current_summ)

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

Область видимости на примере
Сейчас напишем две функции, чтобы показать как интерпретаторы и математическая логика облегчают жизнь программистам. Возведение в квадрат и проверка значения: равен ли квадрат первого числа значению второго, \(3^2\) = 9

Представим, что интерпретатор совсем глупый и читает сверху вниз good_enough(3, 9):

  1. Вызываем good_enough и проставляем (3, 9). Глобальный Х получает значение 9
  2. Из неё вызывается square с аргументом 3, но Х уже проставлен. В square попадает 9, потому что в нём тоже стоит Х и 3 перезаписывается на 9
  3. Получается 9×9−9. Не равно нулю, нам так не надо

В реальности всё по-другому: интерпретатор понимает, что оба X относятся к разными областям видимости. Поэтому вызов good_enough(3, 9) отрабатывает как надо:

  1. Х принимает значение 9
  2. В square попадает 3, потому что X в square и X в good_enough разные переменные.
  3. Получается 3×3−9. Равно нулю, теперь всё работает как мы хотим

Если бы не было механизма со связанными и свободными переменными писать код было бы тяжелее.

Блочная структура

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

Функция sqrt использует для вычислений четыре внешние функции. Это не совсем чёрный ящик, потому что всё что нужно другому программисту — функция sqrt, а кроме неё там ещё четыре процедуры, и все в глобальной области видимости.

Все имена и переменные мешаются в глобальной области и засоряют архитектуру

Программа будет разрастаться и с новыми такими функциями будут появляться новые уродства типа float_value_x_5, y_for_abs и сотня других.
Сейчас функция good-enough определяет достаточное приближение для корня числа, но может определять любое другое приближение. Чтобы решить эту проблему нам нужно спрятать все эти функции внутри sqrt.

Теперь в глобальной области находится только sqrt. Все дополнительные функции скрыты и не занимают имена для новых функций

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

Убираем X из формальных параметров всех внутренних функций и получается красота

Поделиться
Отправить
Запинить
 83   1 мес   sicp
Популярное