PunchJune 12, Abiola’s death shaped my belief in democracy — ShowunmiInquirerWitness: Zaldy Co’s alleged kickbacks have acknowledgment receiptsInquirer EntertainmentUy Pilipins! Manny Pacquiao earns mention in Marvel series ‘VisionQuest’Daily MaverickWHAT’S COOKING: Throwback Thursday: Ash-baked potatoesThe Jerusalem Post'Like a biblical battle': WP journalist wounded in West Bank credits Palestinian youth with rescueCNN TürkMERKEZ BANKASI FAİZ KARARI EKİM 2026: Ekim Ayı TCMB Faiz Kararı Ne Olacak? TCMB Ekim Toplantı Tarihi Belli OlduBollywood HungamaDaler Mehndi controversy: FWICE writes to PM Narendra Modi, Uttarakhand CM Pushkar Singh Dhami after minister’s alcohol allegationZDF heuteAktuelle Pressemitteilungen des ZDFUOLBarclays vai cortar 5.000 empregos com mudanças impulsionadas pela IANotJustOkSalt Lyrics by 6uff & OdumodublvckAntara NewsMataram to reintroduce Sasak language lessons in schoolsDeadlineTV Festival Unveils Dates, Venues & Advisory Chair For First-Ever Greater Manchester Edition
The Daily Newsstand · Free, Always
Thursday, October 22, 2026

Чудеса nightly, часть 3: std уже пишет на Rust, которого у вас нет

Translate

Попробуйте на свежем stable реализовать для своего типа юниксовое расширение из std, которое отвечает за запуск процессов. Компилятор откажет одной аккуратной ошибкой и ткнёт вас в строчку исходников std, написанную синтаксисом, которого в stable нет. Ещё в 1.97 тот же код давал две ошибки, и одна из них проговаривалась про какой-то приватный трейт с пометкой nightly-only.

Прикол в том, что случай не единичный. В исходниках std, которые лежат рядом со stable 1.98, таких трейтов 41, а в nightly уже 55. Это третья часть «Чудес nightly». В первой были gen-блоки, специализация и прочие чудеса, во второй ключи, которые std выписала только себе. Сегодня фичи, которые выкидывают привычные костыли.

Запечатать трейт без приватного трейта

Представьте, что вы выпускаете библиотеку с публичным трейтом и хотите, чтобы пользователи его вызывали, но не реализовывали. Тогда в трейт можно спокойно добавлять методы без мажорного релиза. В Rust для этого всегда был костыль под названием sealed trait:

pub mod shapes {
    mod private { pub trait Sealed {} }

    pub trait Shape: private::Sealed {
        fn area(&self) -> f64;
    }

    pub struct Circle { pub r: f64 }
    impl private::Sealed for Circle {}
    impl Shape for Circle { fn area(&self) -> f64 { 3.14 * self.r * self.r } }
}

Работает это через дыру в видимости. Sealed лежит в приватном модуле, снаружи его не назвать, значит, и не реализовать, а без него не реализовать и Shape. Костыль рабочий, но ошибки пользователь получает странные. Вот что выдают 1.96 и 1.97 на попытку реализовать CommandExt из std, который запечатан так же:

use std::os::unix::process::CommandExt;
struct MyCmd;
impl CommandExt for MyCmd {}

Сначала прилетает E0046 со списком из девяти нереализованных методов, от uid до setsid, и компилятор услужливо предлагает заглушки с todo!(). Потом E0277: the trait bound MyCmd: std::sealed::Sealed is not satisfied с подсказкой, что nightly-only, unstable трейт std::sealed::Sealed не реализован для MyCmd, и только ближе к концу note, что CommandExt это sealed trait. То есть сначала вас зовут писать девять методов, а потом объясняют, что всё равно нельзя.

На stable 1.98.1 тот же файл даёт одну ошибку:

error: trait cannot be implemented outside `std`
  --> ce.rs:3:1
note: trait restricted here
  --> .../library/std/src/os/unix/process.rs:36:5
   |
36 | pub impl(self) trait CommandExt {
   |     ^^^^^^^^^^

pub impl(self) trait значит «видно всем, реализовать можно только внутри этого модуля». Работает фича impl_restriction, и std её включает у себя сама, хотя вам на stable она недоступна. Я поставил исходники 1.94, 1.96 и 1.97: там CommandExt ещё объявлен как pub trait CommandExt: Sealed, и объявлений через impl(...) в библиотеке ноль. В 1.98 std переписали, и таких трейтов стало 41. В текущем nightly их 55: 31 расширение в std::os под разные платформы, 11 в portable-simd и ещё 13 в core, alloc и std::io. Считал грепом по impl(crate|self|super|in путь) перед словом trait, включая unsafe trait и const trait. Без последних двух выходит меньше, так что цифра зависит от того, как искать.

У фичи есть близнец mut_restriction, и он решает вторую вечную боль: поле, которое снаружи можно читать, но нельзя менять. Раньше его прятали и писали геттер.

Теперь так:

#![feature(impl_restriction, mut_restriction)]
pub mod shapes {
    pub impl(self) trait Shape { fn area(&self) -> f64; }
    pub struct Circle { pub mut(self) r: f64 }
    impl Circle {
        pub fn new(r: f64) -> Self { Circle { r } }
        pub fn grow(&mut self) { self.r *= 2.0; }
    }
    impl Shape for Circle { fn area(&self) -> f64 { 3.14 * self.r * self.r } }
}

Снаружи модуля c.r читается и c.grow() работает. А вот c.r = 10.0, &mut c.r и даже деструктуризация let Circle { r } = &mut c дают field r cannot be mutated outside crate::shapes. Литерал Circle { r: 1.0 } снаружи тоже запрещён: Circle cannot be constructed using a struct expression outside crate::shapes, иначе проверку можно было бы обойти в момент создания. Заменить значение целиком через c = Circle::new(5.0) или mem::swap можно, но новое значение всё равно придётся взять из конструктора модуля, так что инварианты он держит. Реализовать Shape у себя тоже не выйдет: trait cannot be implemented outside crate::shapes.

10 августа в блоге Inside Rust вышел call for testing на обе фичи. Когда их стабилизируют, там не сказано, а синтаксис назван открытым для обсуждения, так что impl(self) ещё может поменяться. По-моему, это самая недооценённая штука в сегодняшнем списке: костыль с Sealed живёт во многих крупных библиотеках, а std в nightly его уже выкинула целиком.

Запечатанные трейты чинят API библиотек. Следующая фича чинит то, что вы пишете внутри функций каждый день, а именно match.

match видит сквозь Box и String

База любого, кто парсил команды: есть String, хочется сравнить его с литералами, а match требует &str. Все пишут match s.as_str() и живут дальше. С deref_patterns можно так:

#![feature(deref_patterns)]
fn cmd(s: String) -> &'static str {
    match s {
        "start" => "go",
        "stop" => "halt",
        _ => "?",
    }
}

Паттерн сам проходит через Deref до str и сравнивает там. Я проверил, не прячется ли тут лишняя работа. Собрал с -O две функции, одну с match s по &String, другую с match s.as_str(), и сравнил ассемблер. В обеих по 20 инструкций и одинаковая логика: сначала длина, 4 или 5, потом сами байты. «star» сравнивается как одно 32-битное число 1918989427 плюс отдельный байт t, «stop» как число 1886352499. У версии с deref-паттерном переходов вышло даже меньше, два против четырёх, так что платить тут не за что.

Со строками всё мило, но настоящий кайф начинается с Box. Упрощаем выражение, где двойное отрицание надо схлопнуть, а ноль в сложении выкинуть:

enum Expr { Num(i64), Add(Box<Expr>, Box<Expr>), Neg(Box<Expr>) }

fn simplify(e: Expr) -> Expr {
    match e {
        Expr::Neg(Expr::Neg(inner)) => simplify(*inner),
        Expr::Add(Expr::Num(0), rhs) => simplify(*rhs),
        other => other,
    }
}

Neg хранит Box<Expr>, а мы матчим прямо Expr::Neg(...) внутри, без промежуточного match на каждом уровне. Для -(-(0 + 42)) функция возвращает Num(42). Двигать значения из Box паттерн тоже умеет: inner вынимается из внешнего Box по значению. Полнота проверяется сквозь указатель: match по Box<bool> с армами true и false собирается без . С Vec работают слайсовые паттерны, [first, .., last] матчится прямо по вектору без .asslice(). А если хочется, чтобы проход сквозь указатель был виден глазами, его можно записать: deref!(Some(x)).

С Rc так уже не выйдет.

Паттерн Some("secret") по Rc<Option<String>> проходит сквозь два уровня Deref и работает, а вот арм Some(s) => s.len() падает с E0507: cannot move out of a shared reference. Двигать из Rc нельзя, он общий, так что пишем Some(ref s). Box тут особенный, как и везде в языке.

А что со своими умными указателями?

Пишу MyBox<T> с обычным Deref, матчу по нему Some(x) и получаю E0277: the trait bound MyBox<Option<u8>>: DerefPure is not satisfied. DerefPure это unsafe-трейт-маркер. В документации к нему сказано, что он perma-unstable и что на него опирается корректность deref-паттернов. Смысл в том, что компилятор может звать deref сколько угодно раз и считает, что ответ не меняется. Представьте Deref, который на каждый вызов возвращает разное: проверка полноты match превратится в фикцию. Поэтому своё обещание вы подписываете через unsafe impl за отдельной фичей deref_pure_trait, а в std маркер уже стоит на Box, Rc, Arc, String, Vec, Cow, Pin, ManuallyDrop, ссылках и гардах от RefCell.

Под deref-паттерны заодно похоронили старичка. Фича box_patterns с синтаксисом box x в паттерне висела в nightly годами (в unstable book она описана как минимум с версии 1.25). В конце августа её удалили: nightly-2026-08-25 код с ней ещё собирает, а nightly-2026-08-26 уже нет. Сообщение об ошибке вышло такое:

error[E0557]: feature has been removed
  = note: removed in CURRENT_RUSTC_VERSION
  = note: superseded by `deref_patterns`

CURRENT_RUSTC_VERSION это плейсхолдер, который должны заменить номером версии при ветвлении релиза. В nightly он так и светится в тексте ошибки. В PR с удалением написано, что deref_patterns лучше и общее, поэтому box_patterns стабилизировать не будут.

Паттерны разобрали, пора к тому, как эти структуры вообще создаются.

Две точки вместо ..Default::default()

Конфиг с десятком полей, где задать хочется два, а остальные взять по умолчанию. Обычно руками пишут impl Default, а потом Config { port: 443, ..Default::default() }. А если у одного поля нет адекватного дефолта, например у числа воркеров, Default для него всё равно придётся выдумать. default_field_values пускает значения по умолчанию прямо в объявление:

#![feature(default_field_values)]
pub mod net {
    #[derive(Debug, Default)]
    pub struct Config {
        pub host: &'static str = "localhost",
        pub port: u16 = 8080,
        pub workers: usize,
        pub retries: u8 = 3,
        pub tags: Vec<String> = Vec::new(),
    }
}

Теперь Config { workers: 4, .. } собирает конфиг с localhost:8080, тремя ретраями и пустыми тегами. Голое .. без выражения после него значит «остальное из объявления». Поле без значения по умолчанию обязательно: Config { .. } падает с missing field workers in initializer и пояснением, что поля без дефолта надо указывать явно, а это именно то, чего не умел Default. #[derive(Default)] тоже подхватывает значения из объявления, а для workers берёт обычный ноль.

Значение по умолчанию вычисляется как константа. Vec::new() и String::new() можно, они const fn, а String::from("localhost") уже нельзя, там E0015: cannot call non-const associated function, и std::process::id() тоже мимо. Хотите дефолт, который считается в рантайме, живите пока с Default. В трекинг-задаче об этом как раз спорят.

Если retries сделать приватным, снаружи модуля Config { workers: 4, .. } падает с E0451: field retries of struct Config is private, хотя значение у поля есть. .. не открывает доступ к тому, что вы не видите. С #[non_exhaustive] фича тоже не дружит, компилятор прямо пишет, что #[non_exhaustive] нельзя вешать на тип с дефолтами полей.

Зато есть выигрыш, про который мало кто думает. ..Default::default() строит целиком новый дефолтный объект, забирает из него недостающие поля, а остальное выбрасывает. Проверим на типе, который печатает, когда умирает:

struct Old { name: Loud, cache: Loud }
impl Default for Old {
    fn default() -> Self {
        println!("  Old::default()");
        Old { name: Loud("default-name"), cache: Loud("default-cache") }
    }
}

struct New { name: Loud = Loud("default-name"), cache: Loud = Loud("default-cache") }

Собираем оба с name: Loud("мой") и смотрим вывод:

старый способ:
  Old::default()
  drop default-name
  собрали
  drop мой
  drop default-cache
новый способ:
  собрали
  drop мой
  drop default-cache

Старый способ создал default-name и тут же его уничтожил, ещё до того, как мы получили объект. Новый способ перекрытый дефолт вообще не создаёт. С Loud это забавно, а с полем, где Default выделяет мегабайтный буфер, уже ощутимо.

Ещё в unstable book описан линт default_overrides_default_fields. Он срабатывает, если вы руками пишете impl Default и там перекрываете значения из объявления, и по умолчанию он deny, то есть это ошибка компиляции. Так два источника правды не разъедутся.

Следующая история про замыкания.

.use подвинули, не успев выпустить

Каждый, кто запускал потоки или таски с общим Arc, знает этот танец:

let cfg = Arc::new(String::from("prod"));
let h = {
    let cfg = cfg.clone();
    thread::spawn(move || println!("thread sees {cfg}"))
};

Тут и блок, и клон, и теневая переменная, и move, а на трёх Arc это уже полэкрана. В 2025 году эргономичный подсчёт ссылок был одной из целей проекта Rust, и на nightly жил эксперимент ergonomic_clones: постфиксное .use и замыкания use ||, которые сами клонируют захваченные Arc, Rc и прочее с маркером UseCloned, а остальное просто перемещают:

#![feature(ergonomic_clones)]
let cfg = Arc::new(String::from("prod"));
let h = std::thread::spawn(use || println!("thread sees {cfg}"));
h.join().unwrap();
let c2 = cfg.use;

Работает: поток печатает prod, а после cfg.use у Arc становится два владельца. Клонирует .use только типы с маркером UseCloned, и критерии в его документации расписаны занятно. Клон должен быть O(1), без аллокаций, копировать не больше примерно 64 байт (строка кэша), не блокировать поток, не иметь побочных эффектов и стоить не дороже пары атомарных операций. Маркер стоит на числах, bool, char, NonZero, Arc, Rc и их Weak, на Option и Result из таких типов. На String его нет, и let t = s.use; для строки её просто перемещает: дальше println!("{s}") получает E0382: borrow of moved value. Дорогой клон так случайно не проскочит.

А дальше случился плот-твист: в цели проекта Rust на 2026 год направление пересобрали, и вместо .use и use || теперь другие штуки. Про старую работу сказано, цитирую, «remains available on nightly and informed our thinking». Главный принцип сменился на «Semantic over operational»: трейт должен говорить, что значит клон, а цена дело второе. Отсюда две новые штуки, и обе уже лежат в nightly рядом со старыми.

Первая это трейт Share за фичей share_trait. Он для типов, у которых клон создаёт ещё один псевдоним того же значения: Arc, Rc, &T, отправители каналов mpsc. Option<u8> клонируется дёшево, но псевдонимом не становится, поэтому UseCloned на нём есть, а Share нет. Внутри трейта сидит маленькое чудо в чуде:

pub trait Share: Clone {
    final fn share(&self) -> Self {
        Clone::clone(self)
    }
}

final fn это метод трейта, который нельзя переопределить в реализации, фича final_associated_functions. Я попробовал переопределить такой метод у себя и получил cannot override hello because it already has a final definition in the trait. share() гарантированно равен clone(), и никакая реализация это не сломает.

Вторая штука это move-выражения, фича move_expr. Внутри обычного замыкания можно написать move(выражение), и выражение вычислится в момент создания замыкания, а внутрь переедет результат:

#![feature(move_expr, share_trait)]
use std::clone::Share;
let cfg = Arc::new(String::from("prod"));
let name = String::from("worker");
let h = thread::spawn(|| {
    println!("{} sees {}", move(name), move(cfg.share()));
});
h.join().unwrap();

Печатает worker sees prod, а после join у cfg снова один владелец, клон уехал в поток и там умер. Никакого блока с теневыми переменными, и видно, что именно клонируется. Я ещё проверил, когда вычисляется move(...): вставил туда println! и получил «move(...) evaluated» раньше, чем «closure created». Если внутри move(v.len()), то в замыкание уезжает число, а сам v остаётся снаружи целым.

ergonomic_clones и move_expr помечены как incomplete, так что это прототипы, Share уже без такой пометки.

Компилятор, который берёт производные

Функция на три строки:

#![feature(autodiff)]
use std::autodiff::autodiff_reverse;

#[autodiff_reverse(d_f, Duplicated, Active)]
fn f(x: &[f64; 2]) -> f64 {
    x[0] * x[0] + 3.0 * x[0] * x[1] + x[1].sin()
}

Атрибут просит компилятор сгенерировать рядом функцию d_f, которая считает градиент f в обратном режиме. Duplicated значит, что у x появится теневой аргумент &mut [f64; 2], куда запишутся производные. Active на результате значит, что последним аргументом передаётся затравка, обычно 1.0. Вызываем d_f(&x, &mut dx, 1.0) в точке (2, 1) и получаем f = 10.841470984807897, grad = [7.0, 6.54030230586814]. Проверяем: по первой переменной 2·2 + 3·1 = 7, по второй 3·2 + cos 1 ≈ 6,5403, всё сходится.

Делает это Enzyme, плагин к LLVM. Работает он на уровне LLVM IR после оптимизаций, так что дифференцирует уже оптимизированный код. Enzyme должен видеть IR всех функций, сквозь которые идёт дифференцирование, включая функции из чужих крейтов, отсюда требование толстого LTO. Без него компилятор пишет using the autodiff feature requires setting lto="fat" in your Cargo.toml, а без флага using the autodiff feature requires -Z autodiff=Enable. Сам Enzyme теперь ставится компонентом rustup, rustup component add enzyme --toolchain nightly, и собирать компилятор из исходников больше не надо. В манифесте nightly компонент есть для x86_64 и aarch64 под Linux, macOS и windows-gnullvm, для MSVC пока нет. Ещё весной в отчёте по целям проекта библиотеку Enzyme предлагали скачивать и класть руками, так что прогресс заметный.

Окей, но что будет на чём-то потяжелее? Взял функцию на n переменных, стандартный тест из оптимизации:

#[autodiff_reverse(d_rosen, Duplicated, Active)]
fn rosen(x: &[f64]) -> f64 {
    let mut s = 0.0;
    for i in 0..x.len() - 1 {
        let a = x[i + 1] - x[i] * x[i];
        let b = 1.0 - x[i];
        s += 100.0 * a * a + b * b;
    }
    s
}

И сравнил три способа посчитать градиент: сгенерированный d_rosen, градиент, который я вывел руками, и центральные конечные разности с шагом 1e-6. Лучшее время из пяти серий, всё через black_box:

n = 1000

время на вызов

ошибка против ручного

сама rosen

1,25 мкс

—

ручной градиент

1,81 мкс

эталон

autodiff

2,38 мкс

0

конечные разности

2106 мкс

2,3·10⁻⁶

Autodiff совпал с ручной производной до последнего бита, максимальная разница ровно ноль. Стоил он примерно два вызова самой функции, это ожидаемо для обратного режима. Конечные разности вышли медленнее примерно в 900 раз: им нужно два вызова функции на каждую переменную. На n = 10 000 разрыв вырос примерно до 9000 раз, 24 мкс против 215 мс, а ошибка конечных разностей выросла до 0,19. Функция там доходит до миллиардов, и разность двух почти равных больших чисел, делённая на 2·10⁻⁶, превращается в шум. Autodiff снова дал ноль.

Теневой аргумент складывает новые производные поверх старых, и если звать сгенерированную функцию в цикле на одном и том же буфере, градиент поплывёт. Для x² в точке 3 первый вызов даёт [6.0], второй на том же буфере даёт [12.0]. Перед каждым вызовом буфер надо обнулять, в моём бенчмарке g.fill(0.0) стоит внутри цикла замера.

Градиент есть, значит, можно спускаться. Стартуем из обычной для двумерного Розенброка точки x = −1,2, y = 1 и делаем простой градиентный спуск с шагом 10⁻³:

let mut x = vec![-1.2, 1.0];
for step in 0..=20000 {
    let mut g = vec![0.0; 2];
    let f = d_rosen(&x, &mut g, 1.0);
    if step % 5000 == 0 { println!("шаг {step:5}: f = {f:.6}, x = [{:.4}, {:.4}]", x[0], x[1]); }
    x[0] -= 1e-3 * g[0];
    x[1] -= 1e-3 * g[1];
}

Производную я тут не писал ни разу, а спуск доезжает до минимума в (1, 1):

шаг     0: f = 24.200000, x = [-1.2000, 1.0000]
шаг  5000: f = 0.003761, x = [0.9387, 0.8810]
шаг 10000: f = 0.000059, x = [0.9924, 0.9847]
шаг 15000: f = 0.000001, x = [0.9990, 0.9979]
шаг 20000: f = 0.000000, x = [0.9999, 0.9997]

Поменяете функцию, и градиент перегенерируется сам при следующей сборке.

Но nightly есть nightly. Первую версию спуска я написал на массиве [f64; 2] вместо Vec, и она не сошлась вообще: f на каждом шаге была 24,2, а x уползал в тысячи. Сузил до минимального примера:

#[autodiff_reverse(d_sum_sq, Duplicated, Active)]
fn sum_sq(x: &[f64]) -> f64 {
    let mut s = 0.0;
    for v in x { s += v * v; }
    s
}

fn main() {
    let mut x = [1.0, 2.0];
    for step in 0..3 {
        let mut g = [0.0; 2];
        let f = d_sum_sq(&x, &mut g, 1.0);
        println!("шаг {step}: x = {x:?}, f = {f}, g = {g:?}, честное f = {}", sum_sq(&x));
        x[0] += 1.0;
    }
}

С -O на nightly-2026-09-28 это печатает:

шаг 0: x = [1.0, 2.0], f = 5, g = [2.0, 4.0], честное f = 5
шаг 1: x = [2.0, 2.0], f = 5, g = [2.0, 4.0], честное f = 8
шаг 2: x = [3.0, 2.0], f = 5, g = [2.0, 4.0], честное f = 13

Сгенерированная функция в цикле возвращает один и тот же ответ, хотя массив меняется. Два вызова подряд без цикла отрабатывают правильно, с Vec вместо массива тоже всё честно, а без -O спуск на массиве сходится. По-моему, оптимизатор считает вызов не зависящим от содержимого массива и выносит его из цикла, но в IR я не лез, так что это гипотеза. Если у вас воспроизведётся, это кандидат в баг-репорт.

Есть и прямой режим, autodiff_forward с активностями Dual и Const. Он выгоднее, когда выходов больше, чем входов. Пример с двумерным Розенброком из документации к макросу у меня воспроизвёлся: в точке (1, 3) значение 400 и производная по x минус 800.

До stable тут далеко. В трекинг-задаче отмечено только одобрение от команды компилятора, а одобрения языкового эксперимента и описания дизайна ещё нет. Но собрать и потрогать можно уже сегодня, и по ощущениям это самое близкое к магии, что я видел в nightly.

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

Как потрогать всё это самому

Команды и атрибуты, которыми я пользовался, все проверены на nightly-2026-09-28:

rustup toolchain install nightly
# Enzyme для autodiff ставится отдельным компонентом
rustup component add enzyme --toolchain nightly
# autodiff: флаг плюс толстый LTO, в Cargo.toml нужен [profile.release] lto = "fat"
RUSTFLAGS="-Zautodiff=Enable" cargo +nightly run --release
# одиночный файл без cargo
rustc +nightly --edition 2024 -Zautodiff=Enable -Clto=fat -O main.rs
#![feature(impl_restriction, mut_restriction)] // pub impl(self) trait, pub mut(self) поле
#![feature(deref_patterns)]                    // match по String, Box, Vec, Rc
#![feature(default_field_values)]              // поле: Тип = константа, Struct { .. }
#![feature(share_trait, move_expr)]            // Share и move(выражение), incomplete только move_expr
#![feature(ergonomic_clones)]                  // incomplete: .use и use ||
#![feature(autodiff)]                          // #[autodiff_reverse], #[autodiff_forward]

Incomplete-фичи сыплют предупреждением, его глушит #![allow(incomplete_features)]. В продакшен, понятное дело, ничего из этого не тащите: nightly обновляется каждую ночь, и история с .use показала, как быстро фичу могут подвинуть.

Короче, у всех пяти фич общее одно: каждая выкидывает костыль, к которому все настолько привыкли, что перестали его замечать. Что из этого доживёт до stable и в каком виде, хз, но смотреть на это изнутри оч интересно. Если я где-то накосячил или у вас листинг перестал собираться на свежем nightly, напишите в комментах, поправлю.

Размещайте облачную инфраструктуру и масштабируйте сервисы с надежным облачным провайдером Beget.
Эксклюзивно для читателей Хабра мы даем бонус 10% при первом пополнении.

View the original on Хабр →

KioskNews shows a cleaned-up reading view extracted from the publisher’s page — the original always lives on their site, not ours.