mirror of
https://github.com/Naios/continuable.git
synced 2026-01-01 03:12:12 +08:00
Started on simplifying the all and seq composition heavily
This commit is contained in:
parent
a9da11149b
commit
92ba25cd23
@ -108,15 +108,12 @@ class result_submitter
|
||||
assert((left_ == 0U) && "Expected that the submitter is finished!");
|
||||
std::atomic_thread_fence(std::memory_order_acquire);
|
||||
|
||||
auto cleaned =
|
||||
map_pack(remapping::unpack_result_guards{}, std::move(result_));
|
||||
|
||||
// Call the final callback with the cleaned result
|
||||
traits::unpack(std::move(cleaned), [&](auto&&... args) {
|
||||
std::call_once(flag_, std::move(callback_),
|
||||
std::forward<decltype(args)>(args)...);
|
||||
std::call_once(flag_, [&](auto&&... args) {
|
||||
remapping::finalize_data(std::move(callback_), std::move(result_));
|
||||
});
|
||||
}
|
||||
|
||||
// Completes one result
|
||||
void complete_one() {
|
||||
assert((left_ > 0U) && "Expected that the submitter isn't finished!");
|
||||
@ -127,16 +124,16 @@ class result_submitter
|
||||
}
|
||||
}
|
||||
|
||||
template <typename Target>
|
||||
template <typename Box>
|
||||
struct partial_all_callback {
|
||||
Target* target;
|
||||
Box* box;
|
||||
std::shared_ptr<result_submitter> me;
|
||||
|
||||
template <typename... Args>
|
||||
void operator()(Args&&... args) && {
|
||||
|
||||
// Assign the result to the target
|
||||
*target = remapping::wrap(std::forward<decltype(args)>(args)...);
|
||||
box->assign(std::forward<decltype(args)>(args)...);
|
||||
|
||||
// Complete one result
|
||||
me->complete_one();
|
||||
@ -157,11 +154,11 @@ public:
|
||||
}
|
||||
|
||||
/// Creates a submitter which submits it's result into the storage
|
||||
template <typename Target>
|
||||
auto create_callback(Target* target) {
|
||||
template <typename Box>
|
||||
auto create_callback(Box* box) {
|
||||
left_.fetch_add(1, std::memory_order_seq_cst);
|
||||
return partial_all_callback<std::decay_t<Target>>{target,
|
||||
this->shared_from_this()};
|
||||
return partial_all_callback<std::decay_t<Box>>{box,
|
||||
this->shared_from_this()};
|
||||
}
|
||||
|
||||
/// Initially the counter is created with an initial count of 1 in order
|
||||
@ -171,7 +168,7 @@ public:
|
||||
complete_one();
|
||||
}
|
||||
|
||||
constexpr Result* result_ptr() noexcept {
|
||||
constexpr auto& head() noexcept {
|
||||
return &result_;
|
||||
}
|
||||
};
|
||||
@ -180,12 +177,11 @@ template <typename Submitter>
|
||||
struct continuable_dispatcher {
|
||||
std::shared_ptr<Submitter>& submitter;
|
||||
|
||||
template <typename Index, typename Target,
|
||||
std::enable_if_t<
|
||||
base::is_continuable<std::decay_t<Index>>::value>* = nullptr>
|
||||
void operator()(Index* index, Target* target) const {
|
||||
template <typename Box, std::enable_if_t<remapping::is_continuable_box<
|
||||
std::decay_t<Box>>::value>* = nullptr>
|
||||
void operator()(Box&& box) const {
|
||||
// Retrieve a callback from the submitter and attach it to the continuable
|
||||
std::move(*index).next(submitter->create_callback(target)).done();
|
||||
box.fetch().next(submitter->create_callback(std::addressof(box))).done();
|
||||
}
|
||||
};
|
||||
} // namespace all
|
||||
@ -205,7 +201,7 @@ struct composition_finalizer<composition_strategy_all_tag> {
|
||||
(auto&& callback) mutable {
|
||||
|
||||
// Create the target result from the composition
|
||||
auto result = remapping::create_result_pack(std::move(composition));
|
||||
auto result = remapping::box_continuables(std::move(composition));
|
||||
|
||||
using submitter_t =
|
||||
all::result_submitter<std::decay_t<decltype(callback)>,
|
||||
@ -217,10 +213,8 @@ struct composition_finalizer<composition_strategy_all_tag> {
|
||||
|
||||
// Dispatch the continuables and store its partial result
|
||||
// in the whole result
|
||||
// TODO Fix use after move here
|
||||
remapping::relocate_index_pack(
|
||||
all::continuable_dispatcher<submitter_t>{state}, &composition,
|
||||
state->result_ptr());
|
||||
traverse_pack(all::continuable_dispatcher<submitter_t>{state},
|
||||
state->head());
|
||||
|
||||
// Finalize the composition if all results arrived in-place
|
||||
state->accept();
|
||||
|
||||
@ -55,171 +55,154 @@ namespace composition {
|
||||
/// - multiple async value -> tuple of async values.
|
||||
namespace remapping {
|
||||
// Guard object for representing void results
|
||||
struct void_result_guard {};
|
||||
// Guard object for representing multiple results
|
||||
template <typename... Args>
|
||||
struct multi_result_guard {
|
||||
std::tuple<Args...> result_;
|
||||
template <typename Continuable>
|
||||
class continuable_box;
|
||||
template <typename Data>
|
||||
class continuable_box<continuable_base<Data, hints::signature_hint_tag<>>> {
|
||||
|
||||
multi_result_guard& operator=(std::tuple<Args...> result) {
|
||||
result_ = std::move(result);
|
||||
return *this;
|
||||
continuable_base<Data, hints::signature_hint_tag<>> continuable_;
|
||||
|
||||
public:
|
||||
explicit continuable_box(
|
||||
continuable_base<Data, hints::signature_hint_tag<>>&& continuable)
|
||||
: continuable_(std::move(continuable)) {
|
||||
}
|
||||
};
|
||||
|
||||
// Callable object that maps void_result_guard zo zero arguments
|
||||
struct unpack_result_guards {
|
||||
auto operator()(void_result_guard) const noexcept {
|
||||
continuable_base<Data, hints::signature_hint_tag<>>&& fetch() {
|
||||
return std::move(continuable_);
|
||||
}
|
||||
|
||||
void assign() {
|
||||
}
|
||||
|
||||
auto unbox() && {
|
||||
return spread_this();
|
||||
}
|
||||
template <typename... Args>
|
||||
auto operator()(multi_result_guard<Args...> guard) const noexcept {
|
||||
// Spread the result of the continuable into the current depth.
|
||||
return traits::unpack(std::move(guard.result_), [](auto&&... args) {
|
||||
};
|
||||
template <typename Data, typename First>
|
||||
class continuable_box<
|
||||
continuable_base<Data, hints::signature_hint_tag<First>>> {
|
||||
|
||||
continuable_base<Data, hints::signature_hint_tag<First>> continuable_;
|
||||
First first_;
|
||||
|
||||
public:
|
||||
explicit continuable_box(
|
||||
continuable_base<Data, hints::signature_hint_tag<First>>&& continuable)
|
||||
: continuable_(std::move(continuable)) {
|
||||
}
|
||||
|
||||
continuable_base<Data, hints::signature_hint_tag<First>>&& fetch() {
|
||||
return std::move(continuable_);
|
||||
}
|
||||
|
||||
void assign(First first) {
|
||||
first_ = std::move(first);
|
||||
}
|
||||
|
||||
auto unbox() && {
|
||||
return std::move(first_);
|
||||
}
|
||||
};
|
||||
template <typename Data, typename First, typename Second, typename... Rest>
|
||||
class continuable_box<
|
||||
continuable_base<Data, hints::signature_hint_tag<First, Second, Rest...>>> {
|
||||
|
||||
continuable_base<Data, hints::signature_hint_tag<First, Second, Rest...>>
|
||||
continuable_;
|
||||
std::tuple<First, Second, Rest...> args_;
|
||||
|
||||
public:
|
||||
explicit continuable_box(
|
||||
continuable_base<Data,
|
||||
hints::signature_hint_tag<First, Second, Rest...>>&&
|
||||
continuable)
|
||||
: continuable_(std::move(continuable)) {
|
||||
}
|
||||
|
||||
continuable_base<Data, hints::signature_hint_tag<First, Second, Rest...>>&&
|
||||
fetch() {
|
||||
return std::move(continuable_);
|
||||
}
|
||||
|
||||
void assign(First first, Second second, Rest... rest) {
|
||||
args_ = std::make_tuple(std::move(first), std::move(second),
|
||||
std::move(rest)...);
|
||||
}
|
||||
|
||||
auto unbox() && {
|
||||
return traits::unpack(std::move(args_), [](auto&&... args) {
|
||||
return spread_this(std::forward<decltype(args)>(args)...);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
constexpr void_result_guard wrap() {
|
||||
return {};
|
||||
}
|
||||
template <typename First>
|
||||
constexpr decltype(auto) wrap(First&& first) {
|
||||
return std::forward<First>(first);
|
||||
}
|
||||
template <typename First, typename Second, typename... Rest>
|
||||
constexpr decltype(auto) wrap(First&& first, Second&& second, Rest&&... rest) {
|
||||
return std::make_tuple(std::forward<First>(first),
|
||||
std::forward<Second>(second),
|
||||
std::forward<Rest>(rest)...);
|
||||
}
|
||||
template <typename T>
|
||||
struct is_continuable_box : std::false_type {};
|
||||
template <typename Continuable>
|
||||
struct is_continuable_box<continuable_box<Continuable>> : std::true_type {};
|
||||
|
||||
namespace detail {
|
||||
struct result_extractor_mapper {
|
||||
/// Create slots for a void result which is removed later.
|
||||
/// This is required due to the fact that each continuable has exactly
|
||||
/// one matching valuen inside the result tuple.
|
||||
static constexpr auto initialize(hints::signature_hint_tag<>) noexcept {
|
||||
return void_result_guard{};
|
||||
}
|
||||
/// Initialize a single value
|
||||
template <typename First>
|
||||
static constexpr auto initialize(hints::signature_hint_tag<First>) {
|
||||
return First{};
|
||||
}
|
||||
/// Initialize a multiple values as tuple
|
||||
template <typename First, typename Second, typename... Args>
|
||||
static constexpr auto
|
||||
initialize(hints::signature_hint_tag<First, Second, Args...>) {
|
||||
// TODO Fix non default constructible values
|
||||
return multi_result_guard<First, Second, Args...>{
|
||||
std::make_tuple(First{}, Second{}, Args{}...)};
|
||||
}
|
||||
|
||||
/// Remap a continuable to its corresponding result values
|
||||
/// A void result is mapped to a guard type, single values to the value
|
||||
/// itself and multiple ones to a tuple of values.
|
||||
/// Maps a deeply nested pack of continuables to a continuable_box
|
||||
struct continuable_box_packer {
|
||||
template <
|
||||
typename T,
|
||||
std::enable_if_t<base::is_continuable<std::decay_t<T>>::value>* = nullptr>
|
||||
auto operator()(T&& /*continuable*/) {
|
||||
auto constexpr const hint = hints::hint_of(traits::identify<T>{});
|
||||
return initialize(hint);
|
||||
auto operator()(T&& continuable) {
|
||||
return continuable_box<std::decay_t<T>>{std::forward<T>(continuable)};
|
||||
}
|
||||
};
|
||||
|
||||
/// Relocates the target of a deeply nested pack of indexed_continuable objects
|
||||
/// to the given target.
|
||||
template <typename Evaluator>
|
||||
struct result_relocator_mapper {
|
||||
Evaluator evaluator;
|
||||
|
||||
template <typename Index, typename Result>
|
||||
void traverse_one(std::false_type, Index*, Result*) {
|
||||
// Don't do anything when dealing with casual objects
|
||||
}
|
||||
template <typename Index, typename Result>
|
||||
void traverse_one(std::true_type, Index* index, Result* result) {
|
||||
|
||||
// Call the evaluator with the address of the indexed object and its target
|
||||
evaluator(index, result);
|
||||
}
|
||||
template <typename Index, typename Result>
|
||||
void traverse(traversal::container_category_tag<false, false>, Index* index,
|
||||
Result* result) {
|
||||
|
||||
traverse_one(traits::is_invocable<Evaluator, Index*, Result*>{}, index,
|
||||
result);
|
||||
}
|
||||
|
||||
/// Traverse a homogeneous container
|
||||
template <bool IsTupleLike, typename Index, typename Result>
|
||||
void traverse(traversal::container_category_tag<true, IsTupleLike>,
|
||||
Index* index, Result* result) {
|
||||
auto index_itr = index->begin();
|
||||
auto const index_end = index->end();
|
||||
|
||||
auto result_itr = result->begin();
|
||||
auto const result_end = result->end();
|
||||
|
||||
using element_t = std::decay_t<decltype(*index->begin())>;
|
||||
traversal::container_category_of_t<element_t> constexpr const tag;
|
||||
|
||||
for (; index_itr != index_end; ++index_itr, ++result_itr) {
|
||||
assert(result_itr != result_end);
|
||||
traverse(tag, &*index_itr, &*result_itr);
|
||||
}
|
||||
}
|
||||
|
||||
template <std::size_t... I, typename Index, typename Result>
|
||||
void traverse_tuple_like(std::integer_sequence<std::size_t, I...>,
|
||||
Index* index, Result* result) {
|
||||
|
||||
(void)std::initializer_list<int>{
|
||||
((void)traverse(traversal::container_category_of_t<
|
||||
std::decay_t<decltype(std::get<I>(*index))>>{},
|
||||
&std::get<I>(*index), &std::get<I>(*result)),
|
||||
0)...};
|
||||
|
||||
(void)index;
|
||||
(void)result;
|
||||
}
|
||||
|
||||
/// Traverse tuple like container
|
||||
template <typename Index, typename Result>
|
||||
void traverse(traversal::container_category_tag<false, true>, Index* index,
|
||||
Result* result) {
|
||||
|
||||
std::make_index_sequence<std::tuple_size<Index>::value> constexpr const i{};
|
||||
|
||||
traverse_tuple_like(i, index, result);
|
||||
/// Maps a deeply nested pack of continuable_boxes to its result
|
||||
struct continuable_box_unpacker {
|
||||
template <
|
||||
typename T,
|
||||
std::enable_if_t<is_continuable_box<std::decay_t<T>>::value>* = nullptr>
|
||||
auto operator()(T&& box) {
|
||||
return std::forward<T>(box).unpack();
|
||||
}
|
||||
};
|
||||
} // namespace detail
|
||||
|
||||
/// Returns the result pack of the given deeply nested pack.
|
||||
/// This invalidates all non-continuable values contained inside the pack.
|
||||
///
|
||||
/// This consumes all non continuables inside the pack.
|
||||
/// Returns the boxed pack of the given deeply nested pack.
|
||||
/// This transforms all continuables into a continuable_box which is
|
||||
/// capable of caching the result from the corresponding continuable.
|
||||
template <typename... Args>
|
||||
constexpr auto create_result_pack(Args&&... args) {
|
||||
return cti::map_pack(detail::result_extractor_mapper{},
|
||||
constexpr auto box_continuables(Args&&... args) {
|
||||
return cti::map_pack(detail::continuable_box_packer{},
|
||||
std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
/// Sets the target pointers of indexed_continuable's inside the index pack
|
||||
/// to point to their given counterparts inside the given target.
|
||||
template <typename Relocator, typename Index, typename Target>
|
||||
constexpr void relocate_index_pack(Relocator&& relocator, Index* index,
|
||||
Target* target) {
|
||||
/// Returns the unboxed pack of the given deeply nested boxed pack.
|
||||
/// This transforms all continuable_boxes into its result.
|
||||
template <typename... Args>
|
||||
constexpr auto unbox_continuables(Args&&... args) {
|
||||
return cti::map_pack(detail::continuable_box_unpacker{},
|
||||
std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
constexpr traversal::container_category_of_t<std::decay_t<Index>> const tag;
|
||||
namespace detail {
|
||||
template <typename T, typename Callback, typename Data>
|
||||
void finalize_impl(traits::identity<void>, Callback&& callback, Data&&) {
|
||||
std::forward<Callback>(callback)();
|
||||
}
|
||||
template <typename T, typename Callback, typename Data>
|
||||
void finalize_impl(traits::identity<T>, Callback&& callback, Data&& data) {
|
||||
// Call the final callback with the cleaned result
|
||||
traits::unpack(unbox_continuables(std::forward<Data>(data)),
|
||||
std::forward<Callback>(callback));
|
||||
}
|
||||
} // namespace detail
|
||||
|
||||
detail::result_relocator_mapper<std::decay_t<Relocator>> mapper{
|
||||
std::forward<Relocator>(relocator)};
|
||||
template <typename Callback, typename Data>
|
||||
void finalize_data(Callback&& callback, Data&& data) {
|
||||
using result_t =
|
||||
decltype(traits::unpack(unbox_continuables(std::forward<Data>(data)),
|
||||
std::forward<Callback>(callback)));
|
||||
|
||||
mapper.traverse(tag, index, target);
|
||||
// Guard the final result against void
|
||||
return detail::finalize_impl(traits::identity<result_t>{},
|
||||
std::forward<Data>(data),
|
||||
std::forward<Callback>(callback));
|
||||
}
|
||||
} // namespace remapping
|
||||
} // namespace composition
|
||||
|
||||
@ -69,65 +69,10 @@ auto sequential_connect(Left&& left, Right&& right) {
|
||||
});
|
||||
}
|
||||
|
||||
/// Contains an continuable together with a location where the
|
||||
/// result shall be stored.
|
||||
template <typename Continuable, typename Target>
|
||||
struct indexed_continuable {
|
||||
Continuable continuable;
|
||||
Target* target;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct is_indexed_continuable : std::false_type {};
|
||||
template <typename Continuable, typename Target>
|
||||
struct is_indexed_continuable<indexed_continuable<Continuable, Target>>
|
||||
: std::true_type {};
|
||||
|
||||
/// Maps a deeply nested pack of continuables to an indexed continuable
|
||||
struct result_indexer_mapper {
|
||||
/// Index a given continuable together with its target location
|
||||
template <
|
||||
typename T,
|
||||
std::enable_if_t<base::is_continuable<std::decay_t<T>>::value>* = nullptr>
|
||||
auto operator()(T&& continuable) {
|
||||
auto constexpr const hint = hints::hint_of(traits::identify<T>{});
|
||||
|
||||
using target =
|
||||
decltype(remapping::detail::result_extractor_mapper::initialize(hint));
|
||||
|
||||
using type = indexed_continuable<std::decay_t<T>, target>;
|
||||
|
||||
// We have to pass the continuables as l-value so we can move the whole pack
|
||||
// afterwards as r-value, thus we move the continuable from a l-value here.
|
||||
// NOLINTNEXTLINE(misc-move-forwarding-reference)
|
||||
return type{std::move(continuable), nullptr};
|
||||
}
|
||||
};
|
||||
|
||||
/// Returns the result pack of the given deeply nested pack.
|
||||
/// This invalidates all non-continuable values contained inside the pack.
|
||||
///
|
||||
/// This consumes all continuables inside the pack.
|
||||
template <typename... Args>
|
||||
constexpr auto create_index_pack(Args&&... args) {
|
||||
return cti::map_pack(result_indexer_mapper{}, std::forward<Args>(args)...);
|
||||
}
|
||||
|
||||
struct index_relocator {
|
||||
template <typename Index, typename Target,
|
||||
std::enable_if_t<
|
||||
is_indexed_continuable<std::decay_t<Index>>::value>* = nullptr>
|
||||
void operator()(Index* index, Target* target) const noexcept {
|
||||
// Assign the address of the target to the indexed continuable
|
||||
index->target = target;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename Callback, typename Index, typename Result>
|
||||
template <typename Callback, typename Box>
|
||||
struct sequential_dispatch_data {
|
||||
Callback callback;
|
||||
Index index;
|
||||
Result result;
|
||||
Box box;
|
||||
};
|
||||
|
||||
template <typename Data>
|
||||
@ -139,37 +84,29 @@ class sequential_dispatch_visitor
|
||||
|
||||
public:
|
||||
explicit sequential_dispatch_visitor(Data&& data) : data_(std::move(data)) {
|
||||
// Assign the address of each result target to the corresponding
|
||||
// indexed continuable.
|
||||
remapping::relocate_index_pack(index_relocator{}, &data_.index,
|
||||
&data_.result);
|
||||
}
|
||||
|
||||
virtual ~sequential_dispatch_visitor() = default;
|
||||
|
||||
/// Returns the pack that should be traversed
|
||||
auto& head() {
|
||||
return data_.index;
|
||||
return data_.box;
|
||||
}
|
||||
|
||||
template <typename Index, std::enable_if_t<is_indexed_continuable<
|
||||
std::decay_t<Index>>::value>* = nullptr>
|
||||
bool operator()(async_traverse_visit_tag, Index&& /*index*/) {
|
||||
template <typename Box, std::enable_if_t<remapping::is_continuable_box<
|
||||
std::decay_t<Box>>::value>* = nullptr>
|
||||
bool operator()(async_traverse_visit_tag, Box&& /*box*/) {
|
||||
return false;
|
||||
}
|
||||
|
||||
template <typename Index, typename N>
|
||||
void operator()(async_traverse_detach_tag, Index&& index, N&& next) {
|
||||
assert(index.target && "The target should be non null here!"
|
||||
"Probably this is caused through a bug in "
|
||||
"result_relocator_mapper!");
|
||||
|
||||
std::move(index.continuable)
|
||||
.then([ target = index.target,
|
||||
template <typename Box, typename N>
|
||||
void operator()(async_traverse_detach_tag, Box&& box, N&& next) {
|
||||
box->fetch()
|
||||
.then([ box = std::addressof(box),
|
||||
next = std::forward<N>(next) ](auto&&... args) mutable {
|
||||
|
||||
// Assign the result to the target
|
||||
*target = remapping::wrap(std::forward<decltype(args)>(args)...);
|
||||
box->assign(std::forward<decltype(args)>(args)...);
|
||||
|
||||
// Continue the asynchronous sequential traversal
|
||||
next();
|
||||
@ -182,24 +119,10 @@ public:
|
||||
.done();
|
||||
}
|
||||
|
||||
void finalize(traits::identity<void>) {
|
||||
std::move(data_.callback)();
|
||||
}
|
||||
template <typename T>
|
||||
void finalize(traits::identity<T>) {
|
||||
auto cleaned =
|
||||
map_pack(remapping::unpack_result_guards{}, std::move(data_.result));
|
||||
|
||||
// Call the final callback with the cleaned result
|
||||
traits::unpack(std::move(cleaned), std::move(data_.callback));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void operator()(async_traverse_complete_tag, T&& /*pack*/) {
|
||||
// Guard the final result against void
|
||||
using result_t = decltype(
|
||||
map_pack(remapping::unpack_result_guards{}, std::move(data_.result)));
|
||||
finalize(traits::identity<result_t>{});
|
||||
return remapping::finalize_data(std::move(data_.callback),
|
||||
std::move(data_.box));
|
||||
}
|
||||
};
|
||||
} // namespace seq
|
||||
@ -220,21 +143,19 @@ struct composition_finalizer<composition_strategy_seq_tag> {
|
||||
return [composition = std::forward<Composition>(composition)] // ...
|
||||
(auto&& callback) mutable {
|
||||
|
||||
auto index = seq::create_index_pack(composition);
|
||||
auto result = remapping::create_result_pack(std::move(composition));
|
||||
auto boxed = remapping::box_continuables(std::move(composition));
|
||||
|
||||
// The data from which the visitor is constructed in-place
|
||||
using data_t =
|
||||
seq::sequential_dispatch_data<std::decay_t<decltype(callback)>,
|
||||
std::decay_t<decltype(index)>,
|
||||
std::decay_t<decltype(result)>>;
|
||||
std::decay_t<decltype(boxed)>>;
|
||||
|
||||
// The visitor type
|
||||
using visitor_t = seq::sequential_dispatch_visitor<data_t>;
|
||||
|
||||
traverse_pack_async(async_traverse_in_place_tag<visitor_t>{},
|
||||
data_t{std::forward<decltype(callback)>(callback),
|
||||
std::move(index), std::move(result)});
|
||||
traverse_pack_async(
|
||||
async_traverse_in_place_tag<visitor_t>{},
|
||||
data_t{std::forward<decltype(callback)>(callback), std::move(boxed)});
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user