Перейти к содержимому

Конвенции передачи аргументов

В Python у аргумента нет никакого «режима»: он просто передаётся, а что с ним можно делать — выясняется опытным путём.

В Mojo режим написан в сигнатуре. Их пять, но в повседневном коде вы будете пользоваться первыми тремя.

СловоЧто функция получаетКогда нужно
ничего (или imm)ссылку только для чтенияпо умолчанию, почти всегда
mutизменяемую ссылкуфункция меняет аргумент на месте
varвладение значениемфункция забирает значение себе
refссылку, происхождение которой можно назватьфункция возвращает ссылку наружу
outместо под результатконструкторы и фабрики
def describe(value: Int):
print(value)

Копии не происходит: функция получает ссылку. Но менять по ней нельзя.

error: expression must be mutable for in-place operator destination
Что это значит

Вы попытались изменить аргумент, переданный по умолчанию — а он доступен только для чтения.

Как исправить

Решите, что вам нужно:

  • менять значение у вызывающего кода — пометьте аргумент mut;
  • менять локально, не трогая оригинал — возьмите владение (var) или сделайте копию внутри функции.
def add_page(mut pages: Int):
pages += 1
def main():
var pages = 10
add_page(pages)
add_page(pages)
print(pages)
Результат

12

При вызове ничего дописывать не нужно — add_page(pages), как в Python. Но из сигнатуры сразу видно, что функция меняет то, что ей дали.

🐍 Python
def add_page(report):
report["pages"] += 1 # меняет? не меняет?
🔥 Mojo
def add_page(mut report: Report):
report.pages += 1 # видно из сигнатуры

В Python ответ зависит от типа: список и словарь функция изменит, число или строку — нет. В Mojo про это не гадают: mut в сигнатуре или его отсутствие отвечают на вопрос сразу.

Напомним ограничение из главы Функции: у изменяемого аргумента не может быть значения по умолчанию (error: 'mut' arguments must not have defaults). Логика простая — менять нечего, если значение не передали.

Функция забирает значение себе. Вызывающий код обязан это подтвердить сигилом ^:

def archive(var title: String):
title += " (в архиве)"
print(title)
def main():
var title = String("Отчёт")
archive(title^)
Результат

Отчёт (в архиве)

После вызова переменная title недоступна: владение ушло. Значение уничтожится внутри archive, когда перестанет быть нужным там.

Самая непривычная конвенция. ref нужен не для того, чтобы что-то менять, а для того, чтобы функция могла вернуть ссылку на один из своих аргументов:

def longer(ref a: String, ref b: String) -> ref [a, b] String:
return a if a.byte_length() > b.byte_length() else b
def main():
var short = String("раз")
var long = String("двадцать")
print(longer(short, long))
Результат

двадцать

В квадратных скобках после ref перечислено, откуда ссылка может происходить: из a или из b. Компилятору это нужно, чтобы гарантировать, что возвращённая ссылка не переживёт то, на что указывает.

Результат — настоящая ссылка, в неё можно писать:

def longer(ref a: String, ref b: String) -> ref [a, b] String:
return a if a.byte_length() > b.byte_length() else b
def main():
var short = String("раз")
var long = String("двадцать")
longer(short, long) = String("заменено")
print(short, "|", long)
Результат

раз | заменено

Кстати, mut-аргумент тоже годится как источник для возвращаемой ссылки — но не для всех типов:

error: cannot return 'a's origin, because it has RegisterPassable type 'Int'
Что это значит

Маленькие типы вроде Int передаются в регистрах процессора, то есть mut a: Int — это копия. Вернуть на неё ссылку означало бы отдать наружу ссылку на копию, которой скоро не станет.

Как исправить

Возьмите ref вместо mut: def pick(ref a: Int) -> ref [a] Int компилируется, и писать через результат тоже можно. Ограничение касается именно mut, а не чисел вообще.

С обычным аргументом не выйдет ни так, ни так — у него просто нет происхождения в памяти:

error: value of type 'Int' doesn't have a memory origin in origin specifier

Вы уже видели out в конструкторах: def __init__(out self, ...). Работает он и в обычных функциях — как именованное место под результат:

def make_default(out title: String):
title = String("Без названия")
def main():
var title = make_default()
print(title)
Результат

Без названия

Вызов выглядит как обычный: var title = make_default(). Разница в том, что значение строится сразу в переменной вызывающего, без промежуточного результата.

Рядом с out можно объявлять обычные аргументы, в том числе со значениями по умолчанию. А вот два ограничения компилятор проверяет строго:

error: function may not have multiple 'out' arguments
Что это значит

Больше одного out в функции быть не может: результат у функции один.

Как исправить

Нужно вернуть несколько значений — возвращайте кортеж, как обычно.

error: functions must not declare both an 'out' argument and a return type
Что это значит

out-аргумент и стрелка -> — два способа сказать одно и то же. Вместе они не имеют смысла.

Как исправить

Оставьте что-то одно. В обычном коде проще стрелка; out нужен там, где результат обязан строиться прямо на месте — прежде всего в конструкторах.

conventions.mojo
# Пять конвенций передачи аргументов на одном примере.
@fieldwise_init
struct Report(ImplicitlyCopyable, Writable):
"""Отчёт: название и число страниц."""
var title: String
var pages: Int
def write_to[W: Writer](self, mut writer: W):
"""Печатает отчёт человекочитаемо."""
writer.write(self.title, ", страниц: ", self.pages)
def describe(report: Report):
"""Только читает аргумент — конвенция по умолчанию."""
print(" ", report)
def add_page(mut report: Report):
"""Меняет аргумент на месте, изменение видно вызывающему коду."""
report.pages += 1
def archive(var report: Report):
"""Забирает владение: после вызова оригинал недоступен."""
report.title += " (в архиве)"
print(" ", report)
def longer(ref a: String, ref b: String) -> ref [a, b] String:
"""Возвращает ссылку на более длинную из двух строк."""
return a if a.byte_length() > b.byte_length() else b
def make_default(out report: Report):
"""Создаёт значение прямо в аргументе-результате."""
report = Report("Без названия", 1)
def main():
var report = Report("Отчёт за август", 10)
print("по умолчанию — только чтение:")
describe(report)
print("mut — изменение на месте:")
add_page(report)
add_page(report)
describe(report)
print("ref — ссылка, которую можно вернуть наружу:")
var short = String("раз")
var long = String("двадцать")
print(" длиннее:", longer(short, long))
longer(short, long) = String("заменено")
print(" после записи по ссылке:", short, "|", long)
print("out — результат приходит через аргумент:")
var fresh = make_default()
describe(fresh)
print("var — передача владения:")
archive(report^)
Результат
по умолчанию — только чтение:
   Отчёт за август, страниц: 10
mut — изменение на месте:
   Отчёт за август, страниц: 12
ref — ссылка, которую можно вернуть наружу:
   длиннее: двадцать
   после записи по ссылке: раз | заменено
out — результат приходит через аргумент:
   Без названия, страниц: 1
var — передача владения:
   Отчёт за август (в архиве), страниц: 12

Порядок принятия решения на каждый день:

  1. Функция только читает аргумент? Не пишите ничего — это уже правильно и не стоит ни копейки.
  2. Функция меняет аргумент, и изменение должно быть видно снаружи? mut.
  3. Функция забирает значение навсегда — кладёт в контейнер, пишет, отправляет? var, а на стороне вызова ^.
  4. Функция возвращает ссылку на свой аргумент? ref и происхождение в скобках.
  5. Вне конструкторов out нужен редко — разве что в фабриках вроде make_default из примера выше.

🎯 Проверь себя

Чем `ref` отличается от `mut`?

mut даёт изменяемую ссылку: аргумент можно менять прямо в теле функции. ref нужен для другого — чтобы функция могла вернуть ссылку на свой аргумент наружу; внутри функции такой аргумент доступен только для чтения.

Что нужно написать на стороне вызова для аргумента `var`?

Сигил ^: archive(title^). Так в коде видно, что значение уходит навсегда. После этого переменная недоступна.

Что означают квадратные скобки в `-> ref [a, b] String`?

Происхождение ссылки: она может указывать на a или на b. Компилятору это нужно, чтобы гарантировать, что ссылка не переживёт значение, на которое ссылается.

Какую конвенцию выбрать, если функция просто читает аргумент?

Никакую: поведение по умолчанию и есть ссылка только для чтения. Копии при этом не происходит. Явное имя у этой конвенции — imm, писать его не обязательно.

Копирование и перемещение: что именно делает сигил ^, чем move-only типы отличаются от копируемых и сколько стоит каждая операция.

Примеры проверены на Mojo 1.1.0

Тексты курса — CC BY-NC-SA 4.0, код примеров — Apache 2.0