В прошлый раз мы начали знакомиться с новым языком Mojo, который похож на Python и задуман как будущий главный язык для нейросетей и машинного обучения. Mojo гораздо быстрее Python, но только если учитывать его особенности при написании кода. Сегодня разберём несколько конструкций этого языка и узнаем, чем он отличается от Python.
Чтобы установить Mojo на компьютер, нужен процессор Apple Silicon или виртуальная среда. Ещё его можно запустить в онлайн-терминале. Важное отличие онлайн-версии от локальной установки — нельзя импортировать библиотеки.
Что нужно знать для начала
Mojo — компилируемый язык, а Python — интерпретируемый. Это значит, что перед запуском кода Mojo собирает его в один файл.
Mojo — статически типизированный язык, а Python — динамически типизированный. Поэтому в работе с синтаксисом Mojo нужно сразу указывать типы данных и не пытаться изменить их во время запуска. Например, нельзя не указать, с чем будет работать функция.
Комментарии
Комментарии добавляются как в Python. Первый вариант — указывать их после символа решётки:
# Это комментарий. Компилятор проигнорирует это строку
var message = "Привет, любимые читатели!" # Это тоже комментарий
Второй вариант — написать большой комментарий между двумя строками с тремя двойными кавычками:
def print_some_text(x: String):
"""Функция для вывода строки на экран
Args:
x: Переменная типа String или строка. Мы выводим её на экран
"""
Переменные
В Mojo переменная — это имя, содержащее значение или объект. Все переменные можно изменять после создания. В языке два типа переменных: необъявленные и объявленные. Первые создаются при помощи оператора присваивания:
# Необъявленная переменная
journal = "Код"
Объявленные переменные создаются с помощью ключевого слова var
и могут включать аннотации типов:
# Объявленная переменная
var journal = "Код"
# Ещё одна объявленная переменная
var user_id: Int
Обратите внимание, что мы объявили тип данных для переменной user_id
, но пока ничего туда не положили. Так можно.
Все переменные имеют свой тип, который определяется либо когда мы его указываем, либо когда первый раз присваиваем переменной значение. Как только переменная получила тип, в неё уже нельзя положить значение другого типа. Например, для переменной нельзя указать целочисленный тип данных и попробовать записать в неё строку:
# Этот код выдаст ошибку
var user_id: Int = "Пять"
Знание типов данных и их точных размеров позволяет компилятору эффективно распределять память. Например, переменная типа Int занимает фиксированный объём памяти.
Пользовательский ввод с клавиатуры
Пока что пользовательский ввод с клавиатуры поддерживается только при импорте модуля Python. Выглядит это так:
# Импортируем модуль Python
from python import Python
# Просим пользователя ввести значение переменной
username = py.input("Введите своё имя: ")
Если попробовать сделать ввод без импорта модуля, Mojo выдаст несколько ошибок, причём их текст может меняться в зависимости от конкретного кода. Например, такой код:
def main():
x = input("Введите свою строку: ")
print(x)
Вызывает такие сообщения в онлайн-терминале:
/source/prog.mojo:2:9: error: use of unknown declaration 'input'
x = input("Введите свою строку: ")
^~~~~
/source/prog.mojo:4:11: error: use of unknown declaration 'x'
print(x)
^
mojo: error: failed to parse the provided Mojo source module
Вывод в терминал
Чтобы вывести что-то на экран, используется тот же оператор, что и в Python — print()
:
print("Привет, кодовчане 🔥!")
Присваивание и сравнение
Присваивание и сравнение в Mojo организовано так же, как в Python. Тут тоже ничего нового:
# Присваивание
x = y
# Сравнение: равно
x == y
# Сравнение: не равно
x != y
# Меньше, больше
x > y
x < y
# Больше или равно, меньше или равно
x >= y
x <= y
Условный оператор if
Тут тоже всё как в Python и других языках. Проверяется какое-то условие, например сравнение, и в зависимости от результата выполняется нужное действие:
x = 3
# Если x делится на 2 без остатка...
if x % 2 == 0:
# то выводим одно сообщение
print("x делится на 2")
# Если x не делится без остатка на 2, но делится на 3...
elif x % 3 == 0:
# то выводим другое сообщение
print("x делится 3")
# В противном случае выводим третье сообщение:
else:
print("x не делится ни на 2, ни на 3")
Теперь посмотрим на новое и интересное, чего в Python нет.
Функции def и fn
Функции можно объявлять, как в Python, через слово def
. В них допустимы необъявленные переменные, поэтому тут ничего не изменилось:
# Объявляем функцию с аргументом name
def greet(name):
# Объявляем переменную-строку
sign = '!'
# Объявляем ещё одну переменную строку, состоящую
# Из строки, аргумента и переменной
greeting = "Привет, " + name + sign
# Возвращаем эту переменную
return greeting
# Выводим результат на экран
print(greet('Игорь'))
# Выведет: Привет, Игорь!
Такой код будет работать и в Mojo, и в Python.
Но есть ещё один способ объявить функцию — используя ключевое слово fn
. Тут есть свои особенности:
- в функции и аргументах нужно указывать типы данных и тип возвращаемого значения:
fn pow(base: Int, exp: Int = 2) -> Int:
- можно использовать только объявленные переменные:
var x = "Привет!"
- возможные исключения должны быть объявлены через ключевое слово
raise
, после которого идёт текст ошибки:raise 'Что-то пошло не так!'
- изначально аргументы функции доступны только для чтения, чтобы ничего случайно не сломать.
Вот как это выглядит на практике:
# Объявляем функцию, которая принимает неизвестное количество
# аргументов типа Int и возвращает значение типа Int
fn sum(*values: Int) -> Int:
# Объявляем переменную типа Int и присваиваем ей значение 0
var sum: Int = 0
# Складываем все значения аргументов
for value in values:
sum = sum + value
# Возвращаем сумму
return sum
Кроме этого, fn
-функции можно объявлять с использованием особенностей Mojo, которые выглядят незнакомо для Python-разработчика. Например, обычно всё происходящее внутри функции не влияет на код в других местах. Так работает Python-функция def
:
def mutate(y: int):
# Прибавляем к аргументу единицу
y += 1
# Но ничего не возвращаем
x = 1
# Вызываем функцию, которая ничего не возвращает
mutate(x)
# Переменная x не изменилась, код ниже выведет 1
print(x)
А вот как работает Mojo-функция fn
, которая принимает аргумент через слово inout
:
def mutate(inout y: Int):
# Прибавляем к аргументу единицу
y += 1
# Всё так же ничего не возвращаем
var x = 1
# Вызываем функцию, которая ничего не возвращает,
# но влияет на то, что происходит снаружи
mutate(x)
# Переменная x изменилась, код ниже выведет 2
print(x)
А ещё использование переменных через var защищает от опечаток в fn
-функциях. Если мы опечатались и вызвали какую-то новую необъявленную переменную, то Mojo просто создаст новый объект. Но в fn
мы должны заранее объявить переменные, и потом их можно использовать.
# Объявляем переменную внутри функции
fn func(x: Any) -> Any:
var name = "Михаил"
# Теперь, если попробовать использовать переменную с ошибкой,
# это вызовет ошибку при компиляции кода в файл:
name = "Миша"
Классы и структуры (structs)
Пока что Mojo не поддерживает классы, но в документации разработчики обещают добавить их позже.
Сейчас вместо классов нужно использовать структуры. Выглядит очень похоже на классы, только вместо class
пишем struct
, а при объявлении методов используем fn
вместо def
:
struct MyPair:
var first: Int
var second: Int
fn __init__(inout self, first: Int, second: Int):
self.first = first
self.second = second
fn dump(self):
print(self.first, self.second)
И классы, и структуры поддерживают методы, конструкторы, магические методы и декораторы, но различия тоже есть:
- Python-классы динамические и могут меняться во время выполнения программы, если это предусмотрено кодом.
- Mojo-структуры статические, фиксируются на этапе компиляции.
- Структуры не поддерживают наследование и статические атрибуты класса.
Структуры отказываются от динамической гибкости для более высокой производительности. Программа точно знает, чего ожидать от каждой структуры и сколько ресурсов нужно выделить, поэтому во время выполнения кода точно не понадобится время на перерасчёты из-за динамических изменений.
Создание объекта структуры и декоратор структур @value
В Mojo, как и Python, есть магические методы — возможности по умолчанию, которые появляются одновременно с созданием класса. Даже если не указать их в коде, они всё равно работают. Например, конструктор __init__
устанавливает правила при создании объекта класса.
Пример конструктора __init__
на Python:
# Создаём класс
class Person:
# Добавляем метод-конструктор и указываем,
# что при создании экземпляра класса нужно указать имя
def __init__(self, name):
self.name = name
Теперь вспомним, что такое декоратор: это функция, которая добавляет в другую функцию или класс дополнительные возможности.
В Mojo декоратор @value
добавляет к структуре несколько магических методов, которые не надо писать самому:
- инициализацию класса
__init__
; - метод создания копии класса
__copyinit__
; - метод для перемещения
__moveinit__
.
Если мы напишем такой код:
@value
struct Person:
var name: String
var age: Int
Это будет равносильно такому коду:
struct Person:
# Указали две переменные
var name: String
var age: Int
# и они автоматически попали в этот метод-конструктор
fn __init__(inout self, owned name: String, age: Int):
self.name = name
self.age = age
# и в этот метод для копирования класса
fn __copyinit__(inout self, existing: Self):
self.name = existing.name
self.age = existing.age
# и в этот метод для перемещения
fn __moveinit__(inout self, owned existing: Self):
self.name = existing.name
self.age = existing.age
Объект — это экземпляр класса или структуры, созданный по шаблону с заданными параметрами. У нас это имя и возраст. В Python мы можем создать класс Person и сразу создать его объект:
developer = Person("Михаил", 39)
В Mojo так не получится. Над структурой нужно добавить декоратор, чтобы Mojo научился создавать копии класса. Без @value
этот код не сработает:
# Создаём объект структуры
var developer = Person("Михаил", 39)
# Делаем копию объекта
var gentleman = developer
# Этот код выведет значение Михаил
print(developer.name)
Черты (traits)
Черты дают установить какое-то общее поведение для объектов структур Mojo — и в будущем, возможно, классов.
В Python одна и та же функция может использовать методы разных классов, если они называются одинаково:
# Создаём класс
class Duck:
# Определяем метод
def quack(self):
print("Кря.")
# Создаём класс
class StealthCow:
# Опреляем метод с тем же названием
def quack(self):
print("Муу!")
# Пишем функцию, которая работает с обоими методами
def make_it_quack_python(maybe_a_duck):
try:
maybe_a_duck.quack()
except:
print("Это не утка")
# Создаём два объекта класса
# выведет Кря.
make_it_quack_python(Duck())
# выведет Муу!
make_it_quack_python(StealthCow())
Функцию make_it_quack_python()
не заботит тип переданных объектов, а только то, что у них есть метод .quack()
. Такой подход работает в динамически типизированных языках, где функция подстраивается под свои аргументы. Но в статически типизированных языках так делать нельзя.
Без использования черт придётся два раза объявлять одну и ту же функцию и прямо указывать, с чем мы работаем.
# Объявляем структуру с декоратором value
@value
struct Duck:
# Создаём метод, который выводит на экран Кря
fn quack(self):
print("Кря")
# Объявляем структуру с декоратором value
@value
struct StealthCow:
# Создаём метод c таким же названием, который выводит на экран Муу!
fn quack(self):
print("Муу!")
# Создаём функцию, в которой нужно точно указать,
# что она работает с объектом структуры Duck
fn make_it_quack(definitely_a_duck: Duck):
definitely_a_duck.quack()
# Создаём функцию, в которой нужно точно указать,
# что она работает с объектом структуры StealthCow
fn make_it_quack(not_a_duck: StealthCow):
not_a_duck.quack()
# выведет Quack.
make_it_quack(Duck())
# выведет Муу!
make_it_quack(StealthCow())
Нам даже не нужна конструкция try-except
: она сработает, только если в функцию передали что-то ещё, кроме экземпляра класса. Но в Mojo функция работает только с одним указанным типом — поэтому такие языки и называются статически типизированными.
Так можно работать, пока классов немного. Но переопределять функцию для 100 или 1000 разных структур будет неудобно. С чертами можно создать функцию, которая не зависит от конкретного типа. Вот как это выглядит — похоже на создание структуры, но используется слово trait
:
trait Quackable:
fn quack(self):
...
В черте можно указать только название метода без реализации, и каждый метод обязательно заканчивается многоточием.
Теперь можно создать структуру с этой чертой. Выглядит как наследование на Python:
@value
# Создаём структуру и в скобках указываем черту
struct Duck(Quackable):
# Пишем метод для утки
fn quack(self):
print("Кря")
@value
# Создаём структуру и в скобках указываем черту
struct StealthCow(Quackable):
# Пишем метод для стелс-коровы
fn quack(self):
print("Муу!")
В скобках можно указать несколько черт через запятую.
Теперь напишем функцию с чертой. Выглядит сложнее, чем обычно:
fn make_it_quack[T: Quackable](maybe_a_duck: T):
maybe_a_duck.quack()
Давайте разберёмся, что мы сделали:
- Функция
make_it_quack
принимает любой тип T, который соответствует чертеQuackable
. - Функция вызывает метод
quack()
на переданном в неё объекте, не заботясь о конкретном типе объекта.
Получается, что черты позволяют писать более гибкий код, который работает с любыми типами, если они подходят под заданные черты. При этом все указанные в функциях типы реализуют свои методы, и это защищает нас от ошибок во время выполнения программы.
На что это в итоге похоже?
Mojo во многом позволяет писать так, как будто вы работаете на Python. Но превращается в другой язык, когда дело доходит до написания кода для сложных задач, где важна скорость.
Возможности добавляются в Mojo постепенно и хорошо описаны в документации: подробно и с наглядными примерами. Поэтому если вы уже немного знаете Python, то Mojo — отличная возможность подтянуть знания по языкам для более быстрых и высоконагруженных задач.
Делать Mojo своим первым языком программирования сейчас не стоит, потому что там ещё всё сто раз поменяется.