Раздел 31 · Janet на практике

Определения, функции и чтение скобок

junior~30 мин

открытый урокЭтот раздел читается без входа. Войди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Определения, функции и чтение скобок

Синтаксис данных закрыт, берёмся за синтаксис кода. Главный навык урока: читать скобочный код как дерево вызовов, а не как поток символов. С него и начнём, а дальше def и var, функции с необязательными и переменными аргументами, деструктуризация.

Как читать скобки

Это единственное, что пугает в лиспах. На разбор уйдёт минута, и лучше потратить её сейчас, чем спотыкаться весь урок.

Правило одно: (f a b) означает вызвать f с аргументами a и b. Первый элемент списка это то, что вызывают, остальные аргументы. Всё.

Вложенность читается изнутри наружу:

(* 2 (+ 3 4))       # 14

Пощёлкай по примерам, слева дерево вызовов, справа та же запись в привычном виде:

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

Специальные формы

Некоторые конструкции выглядят как вызовы, но вычисляются иначе. Например, if не может быть функцией: функция сначала вычислила бы оба аргумента, а потом выбрала.

Будь if функцией, напечатались бы обе строки: функция получает аргументы уже вычисленными.

Таких специальных форм ровно двенадцать: def, var, fn, if, while, do, set, quote, quasiquote, splice, break, upscope. Всё остальное, что выглядит управляющей конструкцией (when, cond, each, loop, ->), это макросы, построенные поверх этой дюжины. В уроке про макросы мы научимся писать такие же, и вот это по-настоящему меняет ощущение от языка: граница между “встроено в язык” и “написано в библиотеке” почти исчезает.

Splice

Оператор ; перед коллекцией раскрывает её в список аргументов:

В Clojure и Scheme для этого нужна функция apply, в Janet это часть синтаксиса и работает в любой позиции вызова.

Комментарии

# Комментарий до конца строки
(+ 1 2)   # и в конце строки тоже

Точка с запятой это не комментарий, в отличие от Scheme и Clojure. Это splice. Ошибка обидная: скопировал строку из чужого кода, получил невнятную ошибку компиляции.

def и var

По умолчанию бери def. var нужен там, где значение действительно меняется во времени: счётчик, аккумулятор. Для арифметики над var есть сокращения:

def это не неизменяемость. Запись (def arr @[1 2 3]) запрещает связать имя arr с другим значением, но сам массив остаётся изменяемым, и (array/push arr 4) сработает. def защищает имя, а не данные. Та же разница, что между const и заморозкой объекта в JavaScript, мы её разбирали в уроке про freeze и неизменяемость.

Функции

Докстрока это не комментарий: она уезжает в метаданные определения и достаётся через (doc square). Привыкай писать её сразу, это дёшево и окупается на второй неделе жизни кода.

Анонимная функция это fn, а defn просто def плюс fn:

Необязательные и переменные аргументы

Деструктуризация

Работает и в списке параметров, и в def:

Тело возвращает последнее выражение

Ключевого слова return нет:

(defn describe [n]
  (print "проверяю " n)               # выполняется, результат отбрасывается
  (if (even? n) :even :odd))          # это и есть результат

Из этого следует полезное свойство: в Janet почти всё это выражение, и оно что-то возвращает. if возвращает значение выбранной ветки, поэтому тернарный оператор языку не нужен.

Первый скрипт

Создай файл hello.janet:

#!/usr/bin/env janet

(defn greet [name]
  (string "Привет, " name "!"))

(defn main [& args]
  (def name (get args 1 "мир"))
  (print (greet name)))

Запуск:

janet hello.janet
janet hello.janet Janet
Привет, мир!
Привет, Janet!

Что здесь происходит:

  • (defn main [& args] ...) это точка входа. Если в файле определена функция main, Janet вызовет её после загрузки файла.
  • args это аргументы командной строки. Нулевой элемент имя самого скрипта, поэтому первый пользовательский аргумент лежит в (get args 1).
  • Третий аргумент get это значение по умолчанию, если ключа нет. Удобнее, чем проверять на nil руками.
  • (string ...) это конкатенация с приведением типов, склеивает что угодно, не только строки.

Чтобы файл запускался как обычная программа, добавь строку shebang (она уже в примере) и права на выполнение:

chmod +x hello.janet
./hello.janet Janet

Загрузить файл и остаться в REPL. janet -r hello.janet выполняет файл и оставляет тебя в REPL с его определениями: это та самая петля из трёх шагов, которую мы завели в 31-janet/02 · Установка и REPL: разговор вместо чтения.

Что запомнить

  • def связывает имя, var заводит изменяемую ячейку, set меняет только var.
  • def защищает имя, а не данные: массив под def остаётся изменяемым.
  • &opt даёт необязательные аргументы, & собирает остаток, деструктуризация работает и в параметрах, и в def.
  • Тело возвращает последнее выражение, return в языке нет.
  • (f a b) это вызов f. Первый элемент вызывается, остальные аргументы, приоритетов операций нет.
  • Специальных форм двенадцать, всё остальное макросы поверх них.
  • ; это splice, а не комментарий. Комментарий это #.
  • Функция main в файле это точка входа, (get args 1) первый пользовательский аргумент.

Упражнения

Дальше

Дальше самая важная тема первой половины раздела: почему (= @[1] @[1]) даёт false, что делает присваивание и как осознанно выбирать между изменяемой и неизменяемой веткой.

домашка

Домашка