Размывая границу между командами и тегами: скрипты-процедуры
Основы процедур
Возможно, вы работаете над проектом и замечаете, что у вас есть кусок скрипта, который вы пишете снова и снова, — и хотелось бы аккуратно упаковать его, чтобы удобно переиспользовать. Может, у вас есть математика, которую не хочется копировать и вставлять, или скрипт, генерирующий кастомные имена для предметов или NPC. Вы уже знаете, что для переиспользуемых кусков скрипта, которые можно вызвать одной строкой командой run, используются task-скрипты. А что, если нужно что-то ещё меньше и удобнее? Знакомьтесь: скрипты-процедуры!
Их можно разложить на две части:
Контейнер скрипта-процедуры
Скрипт-процедура — это место, где вы описываете свою логику. Если продолжить примеры выше, именно сюда вы впишете математическое уравнение или код генерации случайного имени.
Скрипт-процедура обычно имеет одно или несколько входных определений (точно так же, как входы у task), но это не обязательно. Однако процедура всегда должна determine выходное значение.
Ниже — пример скрипта, который получает список из 9 локаций, центрированных относительно переданной в скрипт.
surrounding_blocks:
type: procedure
definitions: center
script:
- repeat 3 as:x from:-1:
- repeat 3 as:z from:-1:
- define blocks:->:<[center].add[<[x]>,0,<[z]>]>
# A procedure script MUST determine something.
- determine <[blocks]>
Тег процедуры
Тег proc — это то, чем нужно пользоваться, чтобы получить результат из скрипта-процедуры. Продолжая примеры с математикой и генерацией имени: когда мне нужен ответ уравнения или сгенерированное имя, я использую тег процедуры точно так же, как и любой другой тег в Denizen. В следующем примере скрипт-процедура surrounding_blocks из примера выше используется в другом скрипте.
some_script:
type: task
script:
# Get a 3x3 at the player's feet location
- define locations <player.location.proc[surrounding_blocks]>
# turn them into fake stone for a couple seconds
- showfake <[locations]> stone d:2s
Теги процедур дают скриптерам возможность разбивать свой код на более управляемые кусочки и не повторять один и тот же код слишком часто. Нам удалось собрать аккуратную механику появления фейковой платформы всего за несколько строк кода, при этом аккуратно упаковав самые путаные части. Вместо того чтобы думать о деталях реализации задачи, можно сосредоточиться на том, что мы вообще хотим сделать!
Использовать тег процедуры можно несколькими способами, и между ними есть нюансы, о которых стоит знать.
Первый — самый простой способ получить значение из скрипта-процедуры без дополнительных входов (форма proc):
# This assumes 'my_procedure_script' has no input 'definitions' at all
- define my_value <proc[my_procedure_script]>
Если вы хотите передать что-то в скрипт-процедуру, можно воспользоваться частью тега .context — выглядит это примерно так (форма proc.context):
# This assumes 'my_procedure_script' has 3 simple input 'definitions'
- define my_value <proc[my_procedure_script].context[apple|orange|lasagna]>
Так в скрипт-процедуру будут переданы три значения (apple, orange и lasagna), и они попадут в определения, указанные в ключе definitions. Передавать в скрипт-процедуру можно практически что угодно.
Если нужно передать всего одно значение, удобнее воспользоваться сокращённой формой тега (форма ObjectTag.proc):
# This assumes 'my_procedure_script' has 1 simple input definition
- define fruit apple
- define my_value <[fruit].proc[my_procedure_script]>
Так в скрипт-процедуру будет передано apple, и это значение станет первым определением в ключе definitions. По желанию сокращённую форму можно комбинировать с context.
Важно знать, что ключ context может работать не совсем так, как вы ожидаете, когда речь идёт о передаче списка. Если использовать сокращённую форму тега процедуры и передать список — всё сработает более-менее ожидаемо: первое определение в ключе definitions получит весь список. Однако если поместить список в context, он будет передан не одним значением, а по элементам, по отдельности. Ниже — два наглядных примера. Допустим, colors — это список из red, brown и green:
# Our three definitions will be the colors list, 'apple', and '14'
- define my_value <[colors].proc[my_procedure_script].context[apple|14]>
# Our three definitions will be the three colors in the list, while 'apple' and '14' will be ignored or corrupted
- define my_value <proc[my_procedure_script].context[<[colors]>|apple|14]>
Способы обойти это есть: можно обернуть список в list_single, превратить список в строку с запятыми, а потом разбить обратно, либо просто завести один map-аргумент, в котором лежат все нужные определения. В конечном счёте, что лучше использовать — решать вам.
Подробнее об этой потенциальной проблеме — в разделе «Распространённые ошибки» про Object Hacking.
Таски и процедуры
Процедуры не так уж далеки от тасков, но есть несколько важных различий, о которых стоит знать.
Побочные эффекты
Скрипты-процедуры не могут менять внешнее состояние. То есть скрипт-процедура вообще ничего не меняет — он только определяет значение. К побочным эффектам относится, например, размещение блока, удаление/добавление моба, чтение файлов с компьютера, установка флага и так далее. Важно, чтобы при вызове тега процедуры ничего во внешнем мире не менялось; он может (и скорее всего будет) читать значения, но не должен ничего писать. А значит, в теге процедуры можно использовать лишь очень ограниченный набор команд. define и determine, команды управления потоком выполнения вроде if и foreach, а также отладочные команды — пожалуй, и всё, чем можно пользоваться в теге процедуры.
Это то же самое ограничение, что действует для всех тегов в Denizen. Оно распространяется и на процедуры, потому что скрипт-процедура — это, по сути, и есть кастомный тег!
Возвращаемые значения
Task-скрипт может вернуть значение после завершения, а вот тег процедуры обязан вернуть значение. Таск — это что-то вроде работы, которую нужно сделать, а процедура — скорее вопрос, который задаётся. Если вас попросили подстричь газон, вы выполняете таск по стрижке газона, а когда закончите — можете заняться чем-то другим. Вы можете сообщить, что закончили, или сказать, сколько это заняло, но это НЕ обязательная часть работы по стрижке газона. А вот если учитель задал вам вопрос — дать ответ не просто ожидается, а требуется.
Хотя task-скрипты умеют возвращать значения, делать это куда менее удобно, чем через процедуру. В следующих примерах — процедура и таск, и оба они находят все фрукты в предложении и возвращают их нам списком. Мы выберем один наугад и отправим его игроку через narrate.
Вот версия с таском:
get_fruits:
type: task
definitions: message
script:
- determine <[message].split.filter[is_in[orange|apple|banana]]>
task_example:
type: world
events:
on player chats:
# run the task, and save the result. We also need to specify waiting for the task to finish
- ~run get_fruits def:<context.message> save:fruit_list
# get the saved list, get the queue, then the determined list, and a random entry from that determination
- narrate "No way, I like <entry[fruit_list].created_queue.determination.first.random.if_null[nothing]> too!"
А вот версия с процедурой:
get_fruits:
type: procedure
definitions: message
script:
- determine <[message].split.filter[is_in[orange|apple|banana]]>
proc_example:
type: world
events:
on player chats:
# read out the sentence
- narrate "No way, I like <context.message.proc[get_fruits].random.if_null[nothing]> too!"
Обратите внимание: get_fruits в обоих случаях устроен практически одинаково, но событие proc_example получилось намного проще.
Порядок выполнения
И у тасков, и у процедур есть «скриптовая» часть и способ её запустить, но таски запускаются командой вроде inject или run и могут выполняться параллельно с остальным кодом. Процедуры же запускаются в тот момент, когда обрабатывается их тег, — а это может происходить чаще или реже, чем вы думаете, и в самые неожиданные моменты. Процедура не умеет ждать: она обязана выдать значение тега немедленно, иначе сервер «зависнет».
Из-за этого приходится внимательнее следить за тем, какие скрипты могут вызвать лаг. Ниже — пример, который в виде таска не вызвал бы проблем, а в виде процедуры — вполне может.
Вот этот вариант с таском должен работать гладко:
smooth_example:
type: task
script:
- narrate "What's the millionth digit of pi? Hmm..."
# Get the millionth digit of pi. Using the wait operator will wait for the task to finish,
# but other things on the server will keep processing, so there won't be any lag.
- ~run digit_of_pi def:1000000 save:digit
- narrate "Ya, the millionth digit is <entry[digit].queue.determination.first>."
А вот этот вариант с процедурой может немного подвесить сервер:
laggy_example:
type: task
script:
- narrate "What's the millionth digit of pi? Hmm..."
# The server will hang as it is busy processing this procedure script so it can run the narrate.
- narrate "Ya, the millionth digit is <proc[digit_of_pi].context[1000000]>."
Когда что использовать
Хотя таски и процедуры похожи, у каждого из них есть свои сильные стороны.
Task-скрипт обычно лучше подходит, если вам нужно что-то, что:
не требует ответа
может долго выдавать результат
как-то меняет окружающий мир
А скрипт-процедура обычно лучше, если вам нужно что-то, что:
не меняет окружающий мир
всегда выдаёт результат
нужно использовать внутри тега
Связанная техническая документация
Если хотите подробнее почитать про скрипты-процедуры, тег процедуры и его варианты — вот несколько технических руководств, которые могут пригодиться…
Примечание: большинству пользователей, особенно тем, кто знакомится с Denizen впервые, стоит просто перейти к следующей странице руководства. К этим материалам имеет смысл возвращаться позже, после того как вы изучите Denizen в объёме этого руководства.