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

Ballerina 0.990.0 была основным выпуском, который представил значительное количество новых функций и изменений в языке. Обязательно ознакомьтесь с примечаниями к выпуску Ballerina 0.990.0 для подробного описания изменений.

Встроенный .freeze() метод

Ballerina предоставляет возможность пометить значение как неизменное, введя встроенный метод .freeze() для anydata типизированных значений (например, array, tuple, map, record, json, xml, table).

Если попытка заморозки успешна, значение помечается как неизменное.

Любые попытки обновить значение с этого момента приведут к панике!

e.g.,

  • записи не могут быть добавлены, обновлены или удалены с замороженной карты
  • значения не могут быть обновлены или добавлены в замороженный массив

Попытка заморозить структурированный тип будет успешной только в том случае, если все элементы структуры являются замораживаемыми (т.е. типа anydata), и в случае успеха все элементы будут помечены как неизменяемые (замораживаются глубоко).

Встроенный .isFrozen() метод

Доступен boolean встроенный .isFrozen() метод, позволяющий проверить, является ли значение неизменяемым.

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

Официальный Балерина на примере неизменных ценностей объясняет / демонстрирует базовое использование встроенных методов .freeze() и .isFrozen().

Давайте посмотрим на несколько продвинутый пример, основанный на типе потока Балерины.

Потоки в Ballerina - это ограниченные типы, которые позволяют

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

Например:

Допустим, у нас есть поток, ограниченный типом int:

stream<int> intStream = new;

Можно подписаться на этот поток, передав указатель функции, который принимает int в качестве единственного параметра.

intStream.subscribe(intFunction);

где intFunction - функция, которая принимает int в качестве единственного параметра.

function intFunction(int i) {
  ...
}

В этот поток можно было опубликовать int значений, используя функцию stream.publish.

intStream.publish(1);

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

Хотя для простых потоков с ограничениями базового типа проблем не возникнет, как насчет потоков с ограничениями структурированного типа?

Скажем, у меня есть поток, ограниченный типом записи.

С изменяемым значением, как только я публикую конкретную запись в потоке, если один из подписчиков обновляет поле в записи, это отразится на всех подписчиках.

import ballerina/io;
import ballerina/runtime;
// The record by which the stream is constrained.
type StreamEvent record {
string eventString;
};
public function main() {
// Define a stream constrained by the `StreamEvent` record.
stream<StreamEvent> s = new;
// Subscribe to the stream with a function that updates a field.
s.subscribe(changeId);
// Subscribe to the stream with a function that just prints a field.
s.subscribe(pauseAndPrintId);
// Publish an event to a stream.
StreamEvent se = { eventString: "Original EventString" };
s.publish(se);
// Allow for all the subscribers to be updated.
runtime:sleep(500);
}
# The function that updates a field.
function changeId(StreamEvent s) {
io:println("[Subs-1] Received Event");
// Update a field in the record
s.eventString = "Updated EventString";
io:println("[Subs-1] Updated Event");
}
# The function that just prints a field.
function pauseAndPrintId(StreamEvent s) {
io:println("[Subs-2] Received Event");
runtime:sleep(300);
io:println("[Subs-2] Received Event 'eventString': ", s.eventString);
}

В приведенном выше примере у нас есть две подписки на поток:

  • changeId - просто обновляет значение поля записи eventString на “Updated EventString”, как только запись получена
  • pauseAndPrintId - делает небольшую паузу после получения записи, а затем печатает значение поля eventString

Для демонстрации мы создали запись со значением поля eventString, установленным на “Original EventString”, и опубликовали ее в потоке.

Когда вы запустите этот пример, результат будет следующим:

$ ballerina run --experimental blog_immutable_values_one.bal
[Subs-1] Received Event
[Subs-2] Received Event
[Subs-1] Updated Event
[Subs-2] Received Event 'eventString': Updated EventString

Обратите внимание, как значение поля eventString также изменилось на “Updated EventString” для второй подписки (pauseAndPrintId).

Но что, если я не хочу, чтобы это произошло? Что, если я не хочу, чтобы подписчики обновляли исходное значение?

Неизменяемые значения спешат на помощь!

Теперь давайте немного обновим приведенный выше пример, чтобы использовать неизменяемые значения.

import ballerina/io;
import ballerina/runtime;
// The record by which the stream is constrained.
type StreamEvent record {
string eventString;
};
public function main() {
// Define a stream constrained by the `StreamEvent` record.
stream<StreamEvent> s = new;
// Subscribe to the stream with a function that updates a field.
s.subscribe(changeId);
// Subscribe to the stream with a function that just prints a field.
s.subscribe(pauseAndPrintId);
// Publish an event to a stream.
StreamEvent se = { eventString: "Original EventString" };
// Freeze `se` and publish it to the stream.
s.publish(se.freeze());
// Allow for all the subscribers to be updated.
runtime:sleep(500);
}
# The function that updates a field.
function changeId(StreamEvent s) {
io:println("[Subs-1] Received Event");
// Update a field in the record only if it is mutable.
if (!s.isFrozen()) {
s.eventString = "Updated EventString";
io:println("[Subs-1] Updated Event");
}
}
# The function that just prints a field.
function pauseAndPrintId(StreamEvent s) {
io:println("[Subs-2] Received Event");
runtime:sleep(300);
io:println("[Subs-2] Received Event 'eventString': ", s.eventString);
}

Обратите внимание, как у нас есть:

  • вызывается .freeze() для значения перед публикацией его в потоке (L21)
  • обновил значение в функции changeId, только если оно не заморожено (L31)

Давайте попробуем запустить обновленную версию сейчас:

$ ballerina run --experimental blog_immutable_values_two.bal
[Subs-2] Received Event
[Subs-1] Received Event
[Subs-2] Received Event 'eventString': Original EventString

Обратите внимание, что исходное значение eventString не изменилось сейчас!

Хотя это все круто, но что, если я хочу обновить полученное значение? Это действительно актуальное требование.

Хотя он еще не поддерживается (начиная с версии 0.990.0), в планах есть встроенный метод для поддержки создания размороженной копии замороженного значения. Таким образом, вы можете выбрать безопасный вариант замораживания общих значений (пометив их как неизменяемые), в то время как получатели могут создавать свои собственные размороженные клоны значения, если они хотят обновить полученные значения.

Надеюсь, вы нашли это полезным! :)