LINUX.ORG.RU

Хранение состояния приложения

 


2

8

Ковыряю потихоньку Rust в свободное время, и возник вопрос. Как в расте принято хранить глобальное состояние приложения? Например, те же настройки.

Будет хорошо, если ткнете в пример кода, где можно посмотреть.

★★★★★

Ответ на: комментарий от RazrFalcon

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

В других языках я бы мог использовать, например, global static или синглтон. Но мне кажется, что это как-то криво будет выглядеть на rust.

hippi90 ★★★★★
() автор топика

Есть несколько вариантов. Допустим в main.rs у тебя есть обьект Config полученый из аргументов командной строки или конфиг файла.

Если это таки конфиг, тоесть он иммутабельный, то варианты такие:

1) Дописать [#derive(Clone)] и просто копировать всем нужным участкам кода. Если состояние маленькое - это проще всего.

2) Завернуть в std::sync::Arc, сделать Arc<Config>. Потом через clone() передать в нужные места. Тогда обьект будет один и тот же. Стоимость платится только во время вызова clone(). Тоесть если ты его передаешь в 5 мест, то можешь совсем не париться. Если у тебя будут постоянно создаваться новые обьекты с клонами Arc, то рассчитывай на пару атомарных операций каждый раз. Это тоже дешево.

Если это не совсем конфиг, а что-то мутабельное, какой-то State, тогда варианты такие

1) Arc<Mutex<State>>/Arc<RwLock<State>>. В первом варианте читатели и писатели равноправны. Во втором случае при большом количестве читателей они не мешают друг другу, но операция чуть тяжеловеснее если много писателей - если тебя это волнует. Этот вариант хорош для того чтобы менять малую часть State. В принципе можно подумать чтобы лучше эту малую часть завернуть в Mutex/Rwlock, а на верхнем уровне оставить Arc<State>

2) Если писатель постоянно переписывает State полностью, то можно сделать lock-free вариант. Arc<crossbeam::atomic::ArcCell<State>> и передать его везде через clone(). ArcCell позволяет атомарно заменять указатель на обьект. Читатель когда получает этот указатель, то тоже получает Arc<State>, тоесть может держаться за старый обьект сколько угодно долго. Читатель может прийти в любой момент и получить последнюю версию State.

vertexua ★★★★★
()
Ответ на: комментарий от vertexua

Ну да, я почему-то допустил что речь о многопоточном коде. Так да, для однопоточного конечно можно вместо Arc и блокировок использовать Rc и Cell/RefCell для мутабельности

vertexua ★★★★★
()
Последнее исправление: vertexua (всего исправлений: 1)

Третий вариант для иммутабельного однопоточного кода - расписать все приложение лайфтаймами и везде одолжить конфиг иммутабельно. Истинный вариант для тех у кого нету личной жизни

vertexua ★★★★★
()
Последнее исправление: vertexua (всего исправлений: 1)
Ответ на: комментарий от vertexua

Подробный ответ, спасибо!

Примерно так я себе это и представлял. Я правильно понимаю, что во всех вариантах мы передаем ссылку на конфиг в качестве параметра функции?

А если хочется чтобы это было global static, чтобы можно было из любого места в коде просто обратиться к конфигу?

hippi90 ★★★★★
() автор топика
Ответ на: комментарий от hippi90

Да, везде как аргумент функции или конструкторов. Если совсем глобально, то может реально попробовать lazy_static как там говорят. Я могу себе представить как это не скомпилируется. Но если скомпилируется то будет работать. Я бы все равно лучше просто пользовался передачей через аргументы. Для тестов так лучше

vertexua ★★★★★
()
Ответ на: комментарий от vertexua

Спасибо, попробую оба варианта!

hippi90 ★★★★★
() автор топика
Ответ на: комментарий от slovazap

Это отлично выглядит и работает в интерпретируемых языках.

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

Crocodoom ★★★★★
()
Ответ на: комментарий от Crocodoom

Это отлично выглядит и работает в интерпретируемых языках.

Потому что там «поломана» область видимости и нет многопоточности?

RazrFalcon ★★★★★
()
Ответ на: комментарий от RazrFalcon
from config import greeting, username

print("{}, {}!".format(greeting, username))  

Где здесь что-либо поломано? И удобнее такого конфига уже трудно что-то придумать. Можно так сделать в компилируемых языках? Сомневаюсь, по-крайней мере я таких языков не знаю.

Crocodoom ★★★★★
()
Ответ на: комментарий от Crocodoom

Это отлично выглядит и работает в интерпретируемых языках.

Дичь какая. Красота и применимость архитектурного решения ну никак не зависит от характеристик языка.

slovazap ★★★★★
()
Ответ на: комментарий от RazrFalcon

1) Нельзя управлять пространством имён, потому что у конфига теперь его нет, это просто объект с полями. В моём сниппете есть выбор между config.greeting и просто greeting, тут будет только первый вариант.

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

2) Поля конфига жёстко фиксированы, для их изменения требуется перекомпиляция.

Crocodoom ★★★★★
()

Глобальная статическая переменная плоха тем, что даже для чтения требует unsafe блок, это зрительно засоряет код, хотя это и легко обходится написанием элементарного макроса.

Virtuos86 ★★★★★
()
Ответ на: комментарий от Crocodoom
from config import greeting, username

print("{}, {}!".format(greeting, username))

Что должен иллюстрировать этот пример? Что в питоне можно использовать как конфиг объект модуля?

Virtuos86 ★★★★★
()
Последнее исправление: Virtuos86 (всего исправлений: 1)
Ответ на: комментарий от Virtuos86

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

Crocodoom ★★★★★
()
Ответ на: комментарий от Crocodoom

В Питоне нет никаких «средств экспорта». В чем вообще соль, ведь поля конфига так или иначе фиксированы, что это за конфиг, где появляются новые поля?

Virtuos86 ★★★★★
()
Ответ на: комментарий от Virtuos86

В Питоне нет никаких «средств экспорта».

Нет, они там есть. Во-первых, есть переменная __all__. Во-вторых, что не покрывает __all__, покрывает __getattr__. Можно вообще всё хэндлить через __getattr__, и тогда это будет управление экспортом в чистом виде.

В чем вообще соль, ведь поля конфига так или иначе фиксированы, что это за конфиг, где появляются новые поля?

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

Crocodoom ★★★★★
()
Ответ на: комментарий от RazrFalcon

Ничего не понял.
Но это не проблема компилируемых языков

Продолжай в том же духе

Crocodoom ★★★★★
()
Ответ на: комментарий от Crocodoom

В Питоне нет никаких «средств экспорта».

Нет, они там есть. Во-первых, есть переменная __all__. Во-вторых, что не покрывает __all__, покрывает __getattr__. Можно вообще всё хэндлить через __getattr__, и тогда это будет управление экспортом в чистом виде.

Ага, понятно. Но __all__ это для from config import *, что сомнительная фича для конфига.

В чем вообще соль, ведь поля конфига так или иначе фиксированы, что это за конфиг, где появляются новые поля?

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

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

Virtuos86 ★★★★★
()
Ответ на: комментарий от Virtuos86

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

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

Crocodoom ★★★★★
()
Ответ на: комментарий от Crocodoom

В интерпретируемых языках это распространяется и на конфиги.

Интерпретаторы интерпретируемых языков написаны на компилируемых языках. Шах и мат, аутисты!

O02eg ★★★★★
()
Ответ на: комментарий от Crocodoom

Объясни сначала, что можно сделать в говне вроде пхытона, но нельзя в любом компилируемом языке. Что-то сдается мне, ты несешь чушь!

anonymous
()
Ответ на: комментарий от Crocodoom

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

Чего, импорта из модуля? Пожалуйста:

mod config {
   pub const PARAM: i32 = 0; // публичная (доступная для экспорта) глобальная константа
   const PARAM1: i32 = 0; // приватная константа
}

fn main() {
   use config;
   println!("{}", config::PARAM); // работает
   println!("{}", config::PARAM1); // не работает
}
https://play.rust-lang.org/?version=stable&mode=debug&edition=2015&am...

Virtuos86 ★★★★★
()
Последнее исправление: Virtuos86 (всего исправлений: 1)

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

Например, написал ты здоровый шмат кода, а затем захотел его использовать в другом приложении, а у тебя в десятке мест идет прямое обращение к синглтону. И придется либо тащить ненужный синглтон в новое приложение, либо долго рефакторить текущий код.

Либо, например, понадобится тебе реализовать в твоем же приложении массовую пакетную обработку данных с разными конфигами. И тогда тебе придется либо делать ее однопоточной, меняя содержимое конфига между задачами, либо, для распараллеливания, долго и упорно рефакторить этот код для избавления от синглтонов.

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

Deleted
()
Ответ на: комментарий от Crocodoom

Функция main это и есть рантайм, это точка входа в программу. Я же ссылку дал, нажимаешь «Run» и смотришь результат. Данный пример отработает неудачно из-за последней строки в main, потому что по умолчанию все объекты глобального неймспейса модуля приватны, а чтобы сделать их публичными используют лидирующий модификатор pub. Поскольку PARAM1 объявлена как приватная, мы не можем ее использовать, хотя и импортировали config. Приватными/публичными можно также объявлять поля типов, то есть управлять интерфейсом «объектов». Причем это будет честная приватность, а не как в питоне :).

Теперь почему константы набраны капсом: это конвенция, т.е. проверяется компилятором и обязательно к выполнению. Если капитализированные идентификаторы коробят взгляд, можно использовать то, что в Хаскеле называют квалифицированным импортом (емнип), а синтаксис слизан с питоньего:

use config::{PARAM as param, PARAM1 as param1};
Аналогом from module import * будет use module::*;. Ах, да, если подключаемый модуль находится в другом крэйте (пакете), то на версиях компилятора до Rust 2018 edition понадобится еще предварительно объявить подключение пакета, но это уже ненужные детали.

Virtuos86 ★★★★★
()
Ответ на: комментарий от Virtuos86

pub const PARAM: i32 = 0

Это не рантайм, значение прописано в коде. Нужно задать PARAM в рантайме, например считать с командной строки или из файла.

Crocodoom ★★★★★
()
Ответ на: комментарий от Deleted

Альтернативы-то какие? Рано или поздно каждый задается вопросом, как хранить стейт приложения (это чуть шире чем просто конфиг, но смысл тот же). Обычно городят древовидную структуру, с единой точкой входа. Ну и потом каждый компонент сам знает, в какой лист забуриться за своими данными.

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

Vit ★★★★★
()
Ответ на: комментарий от Crocodoom

Нужно задать PARAM в рантайме, например считать с командной строки или из файла.

В компайлтайм такое можно сделать только с помощью трюков, как действует макрос lazy_static, но это не в стандартной либе. Пример писать лень и некогда, посмотри по ссылке.

Virtuos86 ★★★★★
()
Ответ на: комментарий от Vit

Обычно городят древовидную структуру, с единой точкой входа.

Или каждому компоненту передается ссылка на поддерево.

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

Может и не знать.

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

Или каждому компоненту передается ссылка на поддерево.

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

Может и не знать.

Лучше пример приведи.

Vit ★★★★★
()
Ответ на: комментарий от Vit

Или каждому компоненту передается ссылка на поддерево.

Тогда кто-то третий должен знать координаты поддерева и уметь делать ссылку

Конечно. Условно говоря, конструктор объекта «приложение» (или функция main).

tailgunner ★★★★★
()
Ответ на: комментарий от Virtuos86

что это за конфиг, где появляются новые поля?

Пользователь настраивает интерфейс под себя, создавая окна, в которые перетаскивает нужные вьюхи.

deep-purple ★★★★★
()
Ответ на: комментарий от tailgunner

конструктор объекта «приложение» (или функция main)

Если стейт в процессе работы меняется, создаются объекты ответственные за часть стейта, а затем удаляются вместе с ненужной частью стейта, то, одним «main» тут не обойдешься.

deep-purple ★★★★★
()
Ответ на: комментарий от tailgunner

Ну фик знает. Разве таким образом изоляция не рушится? Получается что внешний main должен знать путь до поддерева, а если не повезет - еще и определение типа данных узла.

Vit ★★★★★
()
Ответ на: комментарий от Vit

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

tailgunner ★★★★★
()
Ответ на: комментарий от deep-purple

Если стейт в процессе работы меняется, создаются объекты ответственные за часть стейта, а затем удаляются вместе с ненужной частью стейта, то, одним «main» тут не обойдешься.

Почему нет? Если «стейт» нужно сохранять, перед уничтожением объектов main забирает у них поддеревья, собирает их в общее дерево, и сохраняет. Но вообще я говорил о конфиге.

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

Я имел в виду, что если main должен обладать лишним знанием о компоненте, то это потенциальные проблемы с изоляцией. Пока компонентов два-три, усложнять дейсnвительно смысла нет. Но когда больше, main просто засрется (IMHO).

Компоненты не «вынуждены» ходить по всему дереву. Например они могут знать строку «path.to.my.leaf», заправлять ее в класс конфига и получать данные. Конфиг это же не голая структура, но и методы для извлечения-изменения.

Vit ★★★★★
()
Ответ на: комментарий от Vit

Я имел в виду, что если main должен обладать лишним знанием о компоненте, то это потенциальные проблемы с изоляцией

Почему «лишним»? Он просто вызывает конструктор и передает туда поддерево. Но это и всё.

Пока компонентов два-три, усложнять дейсnвительно смысла нет. Но когда больше, main просто засрется (IMHO).

Если компонентов много, делается отдельная функция (функции) конструирования приложения.

Например они могут знать строку «path.to.my.leaf», заправлять ее в класс конфига и получать данные.

То есть все компоненты получают ссылку на весь конфиг, а используют только его часть. Вот это - отсутствие изоляции.

tailgunner ★★★★★
()
Последнее исправление: tailgunner (всего исправлений: 1)
Ответ на: комментарий от tailgunner

main забирает у них поддеревья

А если иерархия глубже? Я вот как делал:

Стейт это DOMDocument. Этот «main», как ты и выразился, есть основной класс приложения. Он знает только о вершине стейта и умеет читать и писать стейт в XML. Когда XML прочитан в DOMDocument, этот дом «оккупируют» модельки, которые обслуживают свой уровень вложения и свои ветки (содержимое узлов). Все модельки от одного базового класса, + каждая умеет в свою специфичную задачу — создает новые вложенные модельки, которые умеют работать со своими узлами, которые тоже могут содержать вложения, удаляет, хоть все вложения рекурсивно, хоть выборочно. Таким образом, «main» понятия не имеет какой швах там в глубине творится.

deep-purple ★★★★★
()
Ответ на: комментарий от deep-purple

main забирает у них поддеревья

А если иерархия глубже?

Поддерево. Оно может быть на любой глубине.

Этот «main», как ты и выразился, есть основной класс приложения.

Я упоминал и этот вариант.

Когда XML прочитан в DOMDocument, этот дом «оккупируют» модельки

Здесь ты меня потерял.

Таким образом, «main» понятия не имеет какой швах там в глубине творится.

У меня тоже.

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

Я упоминал и этот вариант

Я именно про него.

Здесь ты меня потерял

Нет:

У меня тоже
перед уничтожением объектов main забирает у них поддеревья, собирает их в общее дерево, и сохраняет

Твой «main» знает о всех «модельках» на критичных уровнях, мой — только про одну, обслуживающую rootNode.

deep-purple ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.