Упражнения 🟡
Что вы узнаете: практику применения паттернов корректности по построению к реалистичным сценариям аппаратного обеспечения: административные команды NVMe, автоматы состояний обновления прошивки, конвейеры обработки показаний датчиков, phantom-типы для PCIe, проверки здоровья по нескольким протоколам и диагностические протоколы с сессионными типами.
Перекрёстные ссылки: гл. 02 (упражнение 1), гл. 05 (упражнение 2), гл. 06 (упражнение 3), гл. 09 (упражнение 4), гл. 10 (упражнение 5)
Практические задачи
Упражнение 1: команда администрирования NVMe (типизированные команды)
Спроектируйте типизированный командный интерфейс для административных команд NVMe:
Identify→IdentifyResponse(номер модели, серийный номер, версия прошивки)GetLogPage→SmartLog(температура, доступный резерв, количество прочитанных единиц данных)GetFeature→ ответ, специфичный для функции
Требования:
- Тип команды определяет тип ответа
- Никакой диспетчеризации во время выполнения, только статическая
- Добавьте newtype
NamespaceId, который не даёт перепутать идентификаторы пространств имён с другими значениямиu32
Подсказка: следуйте паттерну трейта IpmiCmd из гл. 02, но используйте константы, специфичные для NVMe.
Пример решения (упражнение 1)
use std::io;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct NamespaceId(pub u32);
#[derive(Debug, Clone, PartialEq)]
pub struct IdentifyResponse {
pub model: String,
pub serial: String,
pub firmware_rev: String,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SmartLog {
pub temperature_kelvin: u16,
pub available_spare_pct: u8,
pub data_units_read: u64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ArbitrationFeature {
pub high_priority_weight: u8,
pub medium_priority_weight: u8,
pub low_priority_weight: u8,
}
/// Ключевой паттерн: ассоциированный тип фиксирует ответ каждой команды.
pub trait NvmeAdminCmd {
type Response;
fn opcode(&self) -> u8;
fn nsid(&self) -> Option<NamespaceId>;
fn parse_response(&self, raw: &[u8]) -> io::Result<Self::Response>;
}
pub struct Identify { pub nsid: NamespaceId }
impl NvmeAdminCmd for Identify {
type Response = IdentifyResponse;
fn opcode(&self) -> u8 { 0x06 }
fn nsid(&self) -> Option<NamespaceId> { Some(self.nsid) }
fn parse_response(&self, raw: &[u8]) -> io::Result<IdentifyResponse> {
if raw.len() < 12 {
return Err(io::Error::new(io::ErrorKind::InvalidData, "слишком коротко"));
}
Ok(IdentifyResponse {
model: String::from_utf8_lossy(&raw[0..4]).trim().to_string(),
serial: String::from_utf8_lossy(&raw[4..8]).trim().to_string(),
firmware_rev: String::from_utf8_lossy(&raw[8..12]).trim().to_string(),
})
}
}
pub struct GetLogPage { pub log_id: u8 }
impl NvmeAdminCmd for GetLogPage {
type Response = SmartLog;
fn opcode(&self) -> u8 { 0x02 }
fn nsid(&self) -> Option<NamespaceId> { None }
fn parse_response(&self, raw: &[u8]) -> io::Result<SmartLog> {
if raw.len() < 11 {
return Err(io::Error::new(io::ErrorKind::InvalidData, "слишком коротко"));
}
Ok(SmartLog {
temperature_kelvin: u16::from_le_bytes([raw[0], raw[1]]),
available_spare_pct: raw[2],
data_units_read: u64::from_le_bytes(raw[3..11].try_into().unwrap()),
})
}
}
pub struct GetFeature { pub feature_id: u8 }
impl NvmeAdminCmd for GetFeature {
type Response = ArbitrationFeature;
fn opcode(&self) -> u8 { 0x0A }
fn nsid(&self) -> Option<NamespaceId> { None }
fn parse_response(&self, raw: &[u8]) -> io::Result<ArbitrationFeature> {
if raw.len() < 3 {
return Err(io::Error::new(io::ErrorKind::InvalidData, "слишком коротко"));
}
Ok(ArbitrationFeature {
high_priority_weight: raw[0],
medium_priority_weight: raw[1],
low_priority_weight: raw[2],
})
}
}
/// Статическая диспетчеризация: компилятор мономорфизирует код для каждого типа команды.
pub struct NvmeController;
impl NvmeController {
pub fn execute<C: NvmeAdminCmd>(&self, cmd: &C) -> io::Result<C::Response> {
// Собираем SQE из cmd.opcode()/cmd.nsid(),
// отправляем в SQ, ждём CQ, затем:
let raw = self.submit_and_read(cmd.opcode())?;
cmd.parse_response(&raw)
}
fn submit_and_read(&self, _opcode: u8) -> io::Result<Vec<u8>> {
// Реальная реализация обращается к /dev/nvme0
Ok(vec![0; 512])
}
}
Ключевые моменты:
NamespaceId(u32)не даёт смешивать идентификаторы пространств имён с произвольными значениямиu32.NvmeAdminCmd::Response— это «индекс типа»:execute()возвращает ровноC::Response.- Полностью статическая диспетчеризация: никаких
Box<dyn …>и никакого динамического приведения типов во время выполнения.
Упражнение 2: автомат состояний обновления прошивки (typestate)
Смоделируйте жизненный цикл обновления прошивки BMC:
stateDiagram-v2
[*] --> Idle
Idle --> Uploading : begin_upload()
Uploading --> Uploading : send_chunk(data)
Uploading --> Verifying : finish_upload()
Uploading --> Idle : abort()
Verifying --> Applying : verify() ✅ + токен VerifiedImage
Verifying --> Idle : verify() ❌ или abort()
Applying --> Rebooting : apply(token)
Rebooting --> Complete : reboot_complete()
Complete --> [*]
note right of Applying : нет abort(): необратимо
note right of Verifying : VerifiedImage — токен-доказательство
Требования:
- Каждое состояние — отдельный тип
- Загрузка может начинаться только из
Idle - Проверка требует завершённой загрузки
- Применение возможно только после успешной проверки: примите токен-доказательство
VerifiedImage - Перезагрузка — единственный вариант после применения
- Добавьте метод
abort(), доступный вUploadingиVerifying(но не вApplying: уже поздно)
Подсказка: совместите typestate (гл. 05) с capability-токенами (гл. 04).
Пример решения (упражнение 2)
// --- Типы состояний ---
// Проектное решение: здесь состояние хранится непосредственно (`_state: S`), а не через
// `PhantomData<S>` (подход гл. 05). Это позволяет состояниям нести данные: например,
// `Uploading { bytes_sent: usize }` отслеживает прогресс. Используйте `PhantomData`,
// когда состояния — чистые маркеры (нулевого размера); храните состояние в структуре,
// когда состояния несут значимые данные времени выполнения.
pub struct Idle;
pub struct Uploading { bytes_sent: usize } // не ZST: несёт данные о прогрессе
pub struct Verifying;
pub struct Applying;
pub struct Rebooting;
pub struct Complete;
/// Токен-доказательство: создаётся только внутри verify().
pub struct VerifiedImage { _private: () }
pub struct FwUpdate<S> {
bmc_addr: String,
_state: S,
}
impl FwUpdate<Idle> {
pub fn new(bmc_addr: &str) -> Self {
FwUpdate { bmc_addr: bmc_addr.to_string(), _state: Idle }
}
pub fn begin_upload(self) -> FwUpdate<Uploading> {
FwUpdate { bmc_addr: self.bmc_addr, _state: Uploading { bytes_sent: 0 } }
}
}
impl FwUpdate<Uploading> {
pub fn send_chunk(mut self, chunk: &[u8]) -> Self {
self._state.bytes_sent += chunk.len();
self
}
pub fn finish_upload(self) -> FwUpdate<Verifying> {
FwUpdate { bmc_addr: self.bmc_addr, _state: Verifying }
}
/// Abort доступен во время загрузки: возврат в Idle.
pub fn abort(self) -> FwUpdate<Idle> {
FwUpdate { bmc_addr: self.bmc_addr, _state: Idle }
}
}
impl FwUpdate<Verifying> {
/// При успехе возвращает следующее состояние И токен-доказательство VerifiedImage.
pub fn verify(self) -> Result<(FwUpdate<Applying>, VerifiedImage), FwUpdate<Idle>> {
// Реально: проверяем CRC, подпись, совместимость
let token = VerifiedImage { _private: () };
Ok((
FwUpdate { bmc_addr: self.bmc_addr, _state: Applying },
token,
))
}
/// Abort доступен во время проверки.
pub fn abort(self) -> FwUpdate<Idle> {
FwUpdate { bmc_addr: self.bmc_addr, _state: Idle }
}
}
impl FwUpdate<Applying> {
/// Потребляет токен-доказательство VerifiedImage: без проверки применить нельзя.
/// Примечание: метода abort() здесь НЕТ, потому что после начала записи это слишком опасно.
pub fn apply(self, _proof: VerifiedImage) -> FwUpdate<Rebooting> {
FwUpdate { bmc_addr: self.bmc_addr, _state: Rebooting }
}
}
impl FwUpdate<Rebooting> {
pub fn wait_for_reboot(self) -> FwUpdate<Complete> {
FwUpdate { bmc_addr: self.bmc_addr, _state: Complete }
}
}
impl FwUpdate<Complete> {
pub fn version(&self) -> &str { "2.1.0" }
}
// Использование:
// let fw = FwUpdate::new("192.168.1.100")
// .begin_upload()
// .send_chunk(b"image_data")
// .finish_upload();
// let (fw, proof) = fw.verify().map_err(|_| "проверка не пройдена")?;
// let fw = fw.apply(proof).wait_for_reboot();
// println!("Новая версия: {}", fw.version());
Ключевые моменты:
abort()существует только уFwUpdate<Uploading>иFwUpdate<Verifying>: вызов его уFwUpdate<Applying>— ошибка компиляции, а не проверка во время выполнения.VerifiedImageимеет приватное поле, поэтому создать его может толькоverify().apply()потребляет токен-доказательство: пропустить проверку нельзя.
Упражнение 3: конвейер показаний датчиков (анализ размерностей)
Постройте полный конвейер обработки показаний датчиков:
- Определите newtype:
RawAdc,Celsius,Fahrenheit,Volts,Millivolts,Watts - Реализуйте
From<Celsius> for Fahrenheitи наоборот - Создайте
impl Mul<Volts, Output=Watts> for Amperes(P = V × I) - Постройте обобщённый проверщик порогов
Threshold<T> - Напишите конвейер: АЦП → калибровка → проверка порога → результат
Компилятор должен отвергать: сравнение Celsius с Volts, сложение Watts с Rpm, передачу Millivolts там, где ожидаются Volts.
Пример решения (упражнение 3)
use std::ops::{Add, Sub, Mul};
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct RawAdc(pub u16);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Celsius(pub f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Fahrenheit(pub f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Volts(pub f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Millivolts(pub f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Amperes(pub f64);
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Watts(pub f64);
// --- Безопасные преобразования ---
impl From<Celsius> for Fahrenheit {
fn from(c: Celsius) -> Self { Fahrenheit(c.0 * 9.0 / 5.0 + 32.0) }
}
impl From<Fahrenheit> for Celsius {
fn from(f: Fahrenheit) -> Self { Celsius((f.0 - 32.0) * 5.0 / 9.0) }
}
impl From<Millivolts> for Volts {
fn from(mv: Millivolts) -> Self { Volts(mv.0 / 1000.0) }
}
impl From<Volts> for Millivolts {
fn from(v: Volts) -> Self { Millivolts(v.0 * 1000.0) }
}
// --- Арифметика для величин одной единицы ---
// ПРИМЕЧАНИЕ: сложение абсолютных температур (25°C + 30°C) физически сомнительно.
// См. в гл. 06 обсуждение newtype для ΔT: там подход строже. Здесь для упражнения оставим всё просто.
impl Add for Celsius {
type Output = Celsius;
fn add(self, rhs: Self) -> Celsius { Celsius(self.0 + rhs.0) }
}
impl Sub for Celsius {
type Output = Celsius;
fn sub(self, rhs: Self) -> Celsius { Celsius(self.0 - rhs.0) }
}
// P = V × I (умножение разных единиц)
impl Mul<Amperes> for Volts {
type Output = Watts;
fn mul(self, rhs: Amperes) -> Watts { Watts(self.0 * rhs.0) }
}
// --- Обобщённый проверщик порогов ---
// Упражнение 3 расширяет Threshold из гл. 06 обобщённым ThresholdResult<T>,
// который несёт показание, вызвавшее срабатывание. Это развитие более простого
// enum ThresholdResult { Normal, Warning, Critical } из гл. 06.
pub enum ThresholdResult<T> {
Normal(T),
Warning(T),
Critical(T),
}
pub struct Threshold<T> {
pub warning: T,
pub critical: T,
}
// Обобщённая реализация: работает для любого типа единиц, поддерживающего PartialOrd.
impl<T: PartialOrd + Copy> Threshold<T> {
pub fn check(&self, reading: T) -> ThresholdResult<T> {
if reading >= self.critical {
ThresholdResult::Critical(reading)
} else if reading >= self.warning {
ThresholdResult::Warning(reading)
} else {
ThresholdResult::Normal(reading)
}
}
}
// Теперь `Threshold<Rpm>`, `Threshold<Volts>` и т. д. работают автоматически.
// --- Конвейер: АЦП → калибровка → порог → результат ---
pub struct CalibrationParams {
pub scale: f64, // отсчётов АЦП на °C
pub offset: f64, // °C при АЦП 0
}
pub fn calibrate(raw: RawAdc, params: &CalibrationParams) -> Celsius {
Celsius(raw.0 as f64 / params.scale + params.offset)
}
pub fn sensor_pipeline(
raw: RawAdc,
params: &CalibrationParams,
threshold: &Threshold<Celsius>,
) -> ThresholdResult<Celsius> {
let temp = calibrate(raw, params);
threshold.check(temp)
}
// Проверка на этапе компиляции: эти строки НЕ скомпилируются:
// let _ = Celsius(25.0) + Volts(12.0); // ОШИБКА: mismatched types
// let _: Millivolts = Volts(1.0); // ОШИБКА: no implicit coercion
// let _ = Watts(100.0) + Rpm(3000); // ОШИБКА: mismatched types
Ключевые моменты:
- Каждая физическая величина — отдельный тип: случайного смешивания не происходит.
Mul<Amperes> for VoltsдаётWatts, кодируя P = V × I в системе типов.- Явные преобразования
Fromдля связанных единиц (мВ ↔ В, °C ↔ °F). Threshold<Celsius>принимает толькоCelsius: нельзя случайно проверить порог для об/мин.
Упражнение 4: обход возможностей PCIe (phantom-типы + проверенная граница)
Смоделируйте связный список возможностей PCIe:
RawCapability: непроверенные байты из конфигурационного пространстваValidCapability: разобранная и проверенная (через TryFrom)- Каждый тип возможности (MSI, MSI-X, PCI Express, Power Management) имеет собственную раскладку регистров с phantom-типом
- Обход списка возвращает итератор значений
ValidCapability
Подсказка: совместите проверенные границы (гл. 07) с phantom-типами (гл. 09).
Пример решения (упражнение 4)
use std::marker::PhantomData;
// --- Phantom-маркеры для типов возможностей ---
pub struct Msi;
pub struct MsiX;
pub struct PciExpress;
pub struct PowerMgmt;
// Идентификаторы возможностей PCI из спецификации
const CAP_ID_PM: u8 = 0x01;
const CAP_ID_MSI: u8 = 0x05;
const CAP_ID_PCIE: u8 = 0x10;
const CAP_ID_MSIX: u8 = 0x11;
/// Непроверенные байты: могут быть мусором.
#[derive(Debug)]
pub struct RawCapability {
pub id: u8,
pub next_ptr: u8,
pub data: Vec<u8>,
}
/// Проверенная и помеченная типом возможность.
#[derive(Debug)]
pub struct ValidCapability<Kind> {
id: u8,
next_ptr: u8,
data: Vec<u8>,
_kind: PhantomData<Kind>,
}
// --- TryFrom: граница parse-don't-validate ---
impl TryFrom<RawCapability> for ValidCapability<PowerMgmt> {
type Error = &'static str;
fn try_from(raw: RawCapability) -> Result<Self, Self::Error> {
if raw.id != CAP_ID_PM { return Err("не возможность PM"); }
if raw.data.len() < 2 { return Err("данные PM слишком короткие"); }
Ok(ValidCapability {
id: raw.id, next_ptr: raw.next_ptr,
data: raw.data, _kind: PhantomData,
})
}
}
impl TryFrom<RawCapability> for ValidCapability<Msi> {
type Error = &'static str;
fn try_from(raw: RawCapability) -> Result<Self, Self::Error> {
if raw.id != CAP_ID_MSI { return Err("не возможность MSI"); }
if raw.data.len() < 6 { return Err("данные MSI слишком короткие"); }
Ok(ValidCapability {
id: raw.id, next_ptr: raw.next_ptr,
data: raw.data, _kind: PhantomData,
})
}
}
// (Аналогичные реализации TryFrom для MsiX и PciExpress опущены для краткости)
// --- Типобезопасные методы доступа: доступны только для правильной возможности ---
impl ValidCapability<PowerMgmt> {
pub fn pm_control(&self) -> u16 {
u16::from_le_bytes([self.data[0], self.data[1]])
}
}
impl ValidCapability<Msi> {
pub fn message_control(&self) -> u16 {
u16::from_le_bytes([self.data[0], self.data[1]])
}
pub fn vectors_requested(&self) -> u32 {
1 << ((self.message_control() >> 1) & 0x07)
}
}
impl ValidCapability<MsiX> {
pub fn table_size(&self) -> u16 {
(u16::from_le_bytes([self.data[0], self.data[1]]) & 0x07FF) + 1
}
}
// --- Обходчик возможностей: проходит по связному списку ---
pub struct CapabilityWalker<'a> {
config_space: &'a [u8],
next_ptr: u8,
}
impl<'a> CapabilityWalker<'a> {
pub fn new(config_space: &'a [u8]) -> Self {
// Указатель на первую возможность находится по смещению 0x34 в конфигурационном пространстве PCI
let first_ptr = if config_space.len() > 0x34 {
config_space[0x34]
} else { 0 };
CapabilityWalker { config_space, next_ptr: first_ptr }
}
}
impl<'a> Iterator for CapabilityWalker<'a> {
type Item = RawCapability;
fn next(&mut self) -> Option<RawCapability> {
if self.next_ptr == 0 { return None; }
let off = self.next_ptr as usize;
if off + 2 > self.config_space.len() { return None; }
let id = self.config_space[off];
let next = self.config_space[off + 1];
let end = if next > 0 { next as usize } else {
(off + 16).min(self.config_space.len())
};
let data = self.config_space[off + 2..end].to_vec();
self.next_ptr = next;
Some(RawCapability { id, next_ptr: next, data })
}
}
// Использование:
// for raw_cap in CapabilityWalker::new(&config_space) {
// if let Ok(pm) = ValidCapability::<PowerMgmt>::try_from(raw_cap) {
// println!("Управление PM: 0x{:04X}", pm.pm_control());
// }
// }
Ключевые моменты:
RawCapability→ValidCapability<Kind>— граница parse-don’t-validate.pm_control()существует только уValidCapability<PowerMgmt>: вызов его у возможности MSI — ошибка компиляции.- Итератор
CapabilityWalkerвыдаёт сырые возможности; вызывающий код проверяет нужные ему черезTryFrom.
Упражнение 5: проверка здоровья по нескольким протоколам (capability-миксины)
Создайте фреймворк проверки здоровья:
- Определите трейты-ингредиенты:
HasIpmi,HasRedfish,HasNvmeCli,HasGpio - Создайте трейты-миксины:
ThermalHealthMixin(требует HasIpmi + HasGpio): читает температуры, проверяет сигналы тревогиStorageHealthMixin(требует HasNvmeCli): проверки данных SMARTBmcHealthMixin(требует HasIpmi + HasRedfish): перекрёстная проверка данных BMC
- Постройте
FullPlatformController, который реализует все трейты-ингредиенты - Постройте
StorageOnlyController, который реализует толькоHasNvmeCli - Убедитесь, что
StorageOnlyControllerполучаетStorageHealthMixin, но НЕ остальные
Пример решения (упражнение 5)
// --- Трейты-ингредиенты ---
pub trait HasIpmi {
fn ipmi_read_sensor(&self, id: u8) -> f64;
}
pub trait HasRedfish {
fn redfish_get(&self, path: &str) -> String;
}
pub trait HasNvmeCli {
fn nvme_smart_log(&self, dev: &str) -> SmartData;
}
pub trait HasGpio {
fn gpio_read_alert(&self, pin: u8) -> bool;
}
pub struct SmartData {
pub temperature_kelvin: u16,
pub spare_pct: u8,
}
// --- Трейты-миксины с blanket impl ---
pub trait ThermalHealthMixin: HasIpmi + HasGpio {
fn thermal_check(&self) -> ThermalStatus {
let temp = self.ipmi_read_sensor(0x01);
let alert = self.gpio_read_alert(12);
ThermalStatus { temperature: temp, alert_active: alert }
}
}
impl<T: HasIpmi + HasGpio> ThermalHealthMixin for T {}
pub trait StorageHealthMixin: HasNvmeCli {
fn storage_check(&self) -> StorageStatus {
let smart = self.nvme_smart_log("/dev/nvme0");
StorageStatus {
temperature_ok: smart.temperature_kelvin < 343, // 70 °C
spare_ok: smart.spare_pct > 10,
}
}
}
impl<T: HasNvmeCli> StorageHealthMixin for T {}
pub trait BmcHealthMixin: HasIpmi + HasRedfish {
fn bmc_health(&self) -> BmcStatus {
let ipmi_temp = self.ipmi_read_sensor(0x01);
let rf_temp = self.redfish_get("/Thermal/Temperatures/0");
BmcStatus { ipmi_temp, redfish_temp: rf_temp, consistent: true }
}
}
impl<T: HasIpmi + HasRedfish> BmcHealthMixin for T {}
pub struct ThermalStatus { pub temperature: f64, pub alert_active: bool }
pub struct StorageStatus { pub temperature_ok: bool, pub spare_ok: bool }
pub struct BmcStatus { pub ipmi_temp: f64, pub redfish_temp: String, pub consistent: bool }
// --- Полная платформа: все ингредиенты → все три миксина бесплатно ---
pub struct FullPlatformController;
impl HasIpmi for FullPlatformController {
fn ipmi_read_sensor(&self, _id: u8) -> f64 { 42.0 }
}
impl HasRedfish for FullPlatformController {
fn redfish_get(&self, _path: &str) -> String { "42.0".into() }
}
impl HasNvmeCli for FullPlatformController {
fn nvme_smart_log(&self, _dev: &str) -> SmartData {
SmartData { temperature_kelvin: 310, spare_pct: 95 }
}
}
impl HasGpio for FullPlatformController {
fn gpio_read_alert(&self, _pin: u8) -> bool { false }
}
// --- Только хранилище: только HasNvmeCli → только StorageHealthMixin ---
pub struct StorageOnlyController;
impl HasNvmeCli for StorageOnlyController {
fn nvme_smart_log(&self, _dev: &str) -> SmartData {
SmartData { temperature_kelvin: 315, spare_pct: 80 }
}
}
// StorageOnlyController автоматически получает storage_check().
// Вызов thermal_check() или bmc_health() у него — ОШИБКА КОМПИЛЯЦИИ.
Ключевые моменты:
- Blanket
impl<T: HasIpmi + HasGpio> ThermalHealthMixin for T {}: любой тип, реализующий оба ингредиента, автоматически получает миксин. StorageOnlyControllerреализует толькоHasNvmeCli, поэтому компилятор даёт емуStorageHealthMixin, но отвергаетthermal_check()иbmc_health(): никаких проверок во время выполнения не нужно.- Добавление нового миксина (например,
NetworkHealthMixin: HasRedfish + HasGpio) — это один трейт и одна blanket impl: существующие контроллеры получат его автоматически, если подходят.
Упражнение 6: диагностический протокол с сессионными типами (одноразовые типы + typestate)
Спроектируйте диагностическую сессию с одноразовыми токенами выполнения тестов:
DiagSessionначинает в состоянииSetup- Переход в состояние
RunningвыдаётNтокенов выполнения (по одному на каждый тестовый случай) - Каждый
TestTokenпотребляется при запуске теста, что исключает повторный запуск одного и того же теста - Когда все токены потреблены, переход в состояние
Complete - Формирование отчёта (только в состоянии
Complete)
Продвинутый вариант: используйте const-обобщение N, чтобы отслеживать на уровне типов, сколько тестов осталось.
Пример решения (упражнение 6)
// --- Типы состояний ---
pub struct Setup;
pub struct Running;
pub struct Complete;
/// Одноразовый токен теста. НЕ Clone, НЕ Copy: потребляется при использовании.
pub struct TestToken {
test_name: String,
}
#[derive(Debug)]
pub struct TestResult {
pub test_name: String,
pub passed: bool,
}
pub struct DiagSession<S> {
name: String,
results: Vec<TestResult>,
_state: S,
}
impl DiagSession<Setup> {
pub fn new(name: &str) -> Self {
DiagSession {
name: name.to_string(),
results: Vec::new(),
_state: Setup,
}
}
/// Переход в Running: выдаёт по одному токену на каждый тестовый случай.
pub fn start(self, test_names: &[&str]) -> (DiagSession<Running>, Vec<TestToken>) {
let tokens = test_names.iter()
.map(|n| TestToken { test_name: n.to_string() })
.collect();
(
DiagSession {
name: self.name,
results: Vec::new(),
_state: Running,
},
tokens,
)
}
}
impl DiagSession<Running> {
/// Потребляет токен для запуска одного теста. Перемещение исключает двойной запуск.
pub fn run_test(mut self, token: TestToken) -> Self {
let passed = true; // реальный код здесь запускает настоящую диагностику
self.results.push(TestResult {
test_name: token.test_name,
passed,
});
self
}
/// Переход в Complete.
///
/// **Примечание:** это решение НЕ обеспечивает, что все токены потреблены:
/// `finish()` можно вызвать, когда токены ещё остались. Такие токены просто
/// уничтожатся (они не помечены `#[must_use]`). Для полной проверки на этапе компиляции
/// используйте вариант с const-обобщением из примечания «Продвинутый» ниже, где
/// `finish()` доступен только у `DiagSession<Running, 0>`.
pub fn finish(self) -> DiagSession<Complete> {
DiagSession {
name: self.name,
results: self.results,
_state: Complete,
}
}
}
impl DiagSession<Complete> {
/// Отчёт доступен ТОЛЬКО в состоянии Complete.
pub fn report(&self) -> String {
let total = self.results.len();
let passed = self.results.iter().filter(|r| r.passed).count();
format!("{}: пройдено {}/{}", self.name, passed, total)
}
}
// Использование:
// let session = DiagSession::new("стресс-тест GPU");
// let (mut session, tokens) = session.start(&["vram", "compute", "thermal"]);
// for token in tokens {
// session = session.run_test(token);
// }
// let session = session.finish();
// println!("{}", session.report()); // "стресс-тест GPU: пройдено 3/3"
//
// // Это НЕ скомпилируется:
// // session.run_test(used_token); → ОШИБКА: use of moved value
// // running_session.report(); → ОШИБКА: no method `report` on DiagSession<Running>
Ключевые моменты:
TestTokenне реализуетCloneиCopy: потребление черезrun_test(token)перемещает его, поэтому повторный запуск того же теста — ошибка компиляции.report()существует только уDiagSession<Complete>: вызвать его в процессе выполнения невозможно.- Продвинутый вариант использовал бы
DiagSession<Running, N>с const-обобщениями, гдеrun_testвозвращаетDiagSession<Running, {N-1}>, аfinishдоступен только уDiagSession<Running, 0>. Это гарантирует, что все токены потреблены до завершения.
Ключевые выводы
- Практикуйтесь на реалистичных протоколах: NVMe, обновление прошивки, конвейеры датчиков и PCIe — реальные цели для этих паттернов.
- Каждое упражнение соответствует основной главе: используйте перекрёстные ссылки, чтобы повторить паттерн перед попыткой.
- Решения спрятаны в раскрывающихся блоках: попробуйте каждое упражнение, прежде чем открывать решение.
- Сочетайте паттерны в упражнении 5: проверки здоровья по нескольким протоколам объединяют типизированные команды, размерные типы и проверенные границы.
- Сессионные типы (упражнение 6) — передний край: они обеспечивают порядок сообщений в каналах, распространяя typestate на распределённые системы.