5. Проектирование и составление функций

Изучите параметры, возвращаемые значения, значения по умолчанию и передачу по значению.

Wave Foundation

Начиная с повторяющегося кода

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

В этой главе мы начнем с программы, которая несколько раз записывает расчеты скидок. Каждый пример завершается в main.wave и запускается как wavec run main.wave. Мы пока не разделяем файлы.

fun main() {
    var first_price: i32 = 2000;
    var first_discount: i32 = first_price * 10 / 100;
    var first_total: i32 = first_price - first_discount;
    var second_price: i32 = 5000;
    var second_discount: i32 = second_price * 10 / 100;
    var second_total: i32 = second_price - second_discount;
    println("{} {}", first_total, second_total);
}

Результат выполнения:

1800 4500

Два расчета отличаются только ценой и имеют одинаковую структуру. При изменении правил скидок вам потребуется отредактировать оба места. Если вы измените только одну сторону, вы получите разные результаты для продуктов, для которых требуется одна и та же политика.

Определите вход и выход

Перенесите избыточные вычисления в функции. Измененное значение принимается в качестве входных данных price, и возвращается рассчитанная цена.

fun discounted(price: i32) -> i32 {
    var discount: i32 = price * 10 / 100;
    return price - discount;
}

fun main() {
    println("{} {}", discounted(2000), discounted(5000));
}

Результат выполнения:

1800 4500

Имя функции: discounted. price: i32 в скобках — это объявление параметра, а -> i32 — тип результата. Локальная переменная discount в теле используется только внутри этой функции.

discounted(2000) — выражение, вызывающее функцию. 2000 в скобках — это аргумент, который вы фактически передаете. Поскольку значение, возвращаемое функцией, становится результатом этого вызывающего выражения, его можно напрямую использовать в качестве аргумента для println.

терминология код смысл
параметр price Имя входа, указанное при объявлении функции
фактор 2000 Значение, переданное при вызове
тип возвращаемого значения i32 Тип значения, создаваемого выражением вызова
оператор возврата return price - discount Передайте результат и завершите этот вызов

Следуйте порядку вызова и выполнения

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

fun calculate(value: i32) -> i32 {
    println("inside: {}", value);
    return value * 2;
}

fun main() {
    println("before");
    var result: i32 = calculate(7);
    println("after: {}", result);
}

Результат выполнения:

before
inside: 7
after: 14

Порядок выполнения следующий: первый вывод main, основной текст calculate и последний вывод main. Когда return выполняется, вызов calculate заканчивается результатом 14, и инициализация result main завершается.

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

несколько параметров

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

fun discounted(price: i32, percent: i32) -> i32 {
    return price - price * percent / 100;
}

fun main() {
    var standard: i32 = discounted(2000, 10);
    var special: i32 = discounted(2000, 25);
    println("standard={}", standard);
    println("special={}", special);
}

Результат выполнения:

standard=1800
special=1500

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

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

аргумент по умолчанию

Вы можете указать часто используемые значения по умолчанию.

fun discounted(price: i32, percent: i32 = 10) -> i32 {
    return price - price * percent / 100;
}

fun main() {
    println("{}", discounted(2000));
    println("{}", discounted(2000, 25));
}

Результат выполнения:

1800
1500

Первый вызов опускает второй аргумент и использует 10. Второй вызов использует указанные 25. Для необязательных параметров в конце оставляются значения по умолчанию. В грамматике не используется запись пробела, чтобы опустить только первый аргумент.

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

означает передачу по значению

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

fun next(value: i32) -> i32 {
    return value + 1;
}

fun main() {
    var count: i32 = 4;
    var later: i32 = next(count);
    println("count={} later={}", count, later);
    count = next(count);
    println("count={}", count);
}

Результат выполнения:

count=4 later=5
count=5

Первый вызов читает count и инициализирует later. count по-прежнему равно 4. После второго вызова результат присваивается count, поэтому он становится 5. Конструкция возврата по значению делает очевидным для вызывающей стороны, где данные изменились.

Если вы хотите изменить исходное пространство памяти внутри функции, вы можете передать указатель. Это описано в глава указателя. Даже если вы передаете указатель, вы должны различать само значение указателя и пространство для хранения его адреса.

Не оставляйте пути, которые не возвращаются

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

// 의도적으로 잘못된 함수 조각

fun positive(value: i32) -> i32 {
    if (value > 0) {
        return value;
    }
}

Если value меньше или равно 0, заданного значения для возврата нет. После того, как вы установили предполагаемые правила, вам нужно записать все свои маршруты.

fun positive_or_zero(value: i32) -> i32 {
    if (value > 0) {
        return value;
    }

    return 0;
}

fun main() {
    println("{} {} {}", positive_or_zero(-2), positive_or_zero(0), positive_or_zero(8));
}

Результат выполнения:

0 0 8

Вызов, который первым выполняет return, не переходит к return ниже. Последний return достигается только тогда, когда условие ложно. Мы проверили правило для трех случаев: положительного, 0 и отрицательного.

Функция без результата

Если вы выполняете только такие операции, как печать, вы можете опустить тип возвращаемого значения. Функция без результата также может быть завершена досрочно с помощью return;.

fun show_positive(value: i32) {
    if (value <= 0) {
        return;
    }

    println("positive={}", value);
}

fun main() {
    show_positive(-3);
    show_positive(6);
}

Результат выполнения:

positive=6

Первый вызов возвращается, ничего не печатая. Второй вызов печатает: «Нет результата» и «Не возвращается к точке вызова» — это разные вещи. Функции, которые не возвращают значения, например функции, завершающие процесс, классифицируются по типу возвращаемого значения !.

Составление полноценной программы с множеством функций

Теперь мы разделяем проверку ввода, расчет и вывод на разные функции. Поскольку мы ограничили диапазон цен, промежуточное умножение в этом примере находится в диапазоне i32.

fun valid_order(price: i32, quantity: i32, percent: i32) -> bool {
    return price >= 0 && price <= 100000
        && quantity >= 1 && quantity <= 100
        && percent >= 0 && percent <= 100;
}

fun discounted_unit(price: i32, percent: i32) -> i32 {
    return price - price * percent / 100;
}

fun order_total(price: i32, quantity: i32, percent: i32) -> i32 {
    var unit: i32 = discounted_unit(price, percent);
    return unit * quantity;
}

fun show_order(price: i32, quantity: i32, percent: i32) {
    if (!valid_order(price, quantity, percent)) {
        println("invalid order");
        return;
    }

    var total: i32 = order_total(price, quantity, percent);
    println("total={}", total);
}

fun main() {
    show_order(2000, 3, 10);
    show_order(2000, 0, 10);
    show_order(2000, 3, 120);
}

Результат выполнения:

total=5400
invalid order
invalid order

Ответьте по одному вопросу на каждую функцию. valid_order определяет, разрешен ли ввод, discounted_unit определяет размер одной скидки, order_total определяет, какова общая цена, и show_order определяет, что показывать.

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

практические задачи

  1. Напишите maximum, что вернет большее из двух целых чисел.
  2. Напишите функцию clamp, которая возвращает значение между двумя границами. В приведенном ниже решении low <= high установлено в качестве условия вызова.
  3. Создайте функцию, которая добавляет налог к значению, и объедините ее с функцией суммы заказа. Сначала определите диапазон и целочисленные точки обрезки.

Решение: функция с границей

fun maximum(left: i32, right: i32) -> i32 {
    if (left > right) {
        return left;
    }

    return right;
}

fun clamp(value: i32, low: i32, high: i32) -> i32 {
    if (value < low) {
        return low;
    }
    if (value > high) {
        return high;
    }

    return value;
}

fun main() {
    println("max={}", maximum(7, 4));
    println("{} {} {}", clamp(-3, 0, 10), clamp(6, 0, 10), clamp(20, 0, 10));
}

Результат выполнения:

max=7
0 6 10

clamp имеет три пути: меньше диапазона, внутри диапазона и больше диапазона. Проверьте также вручную добавив граничные значения 0 и 10. Если вы хотите подать заявку до low > high, вы должны решить, как выразить отказ. Глава Обработка ошибок решает эту проблему с помощью структур результатов.

рекурсивный вызов

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

fun factorial(value: i32) -> i32 {
    if (value <= 1) {
        return 1;
    }

    return value * factorial(value - 1);
}

fun main() {
    println("{}", factorial(5));
}

Результат выполнения:

120

Вычисление 5 приводит к 5 * factorial(4), который возвращает 1 при достижении. Возвращаемое значение передается последовательно предыдущему вызову, в результате чего получается 120. Эта функция является примером объяснения малых положительных целых чисел. При больших входных данных необходимо учитывать диапазон результатов и глубину вызова. Вы можете избежать проблемы увеличения глубины вызова, написав одну и ту же операцию в цикле.

Распространенные ошибки

явление Проверить
Ошибка с недостаточным или слишком большим количеством аргументов Количество параметров и необязательные значения по умолчанию
Тип возвращаемого значения не соответствует return Тип выражения и объявление функции
Не возвращаясь с определенного пути Возвращается ли он, даже если условие ложно?
Отсутствует аргумент универсального типа. После имени функции <Type>
Исходное значение изменяется после вызова функции указателя. Это функция, которая только считывает значение, или функция, которая его изменяет?

Экспортируемые извне функции, такие как export(c), используют определенные сигнатуры. Сами по себе универсальные функции не могут быть экспортированы с использованием соглашения о внешних вызовах. ptr<T> и array<T, N> — это типы встроенной памяти языка, которые отличают их от объявлений общих пользовательских структур.

функциональное обучение · Учебные модули и дженерики · FFI