Cow (Clone-on-Write) - тип, который позволяет переиспользовать данные без копирования, пока это возможно.
ML-проекты работают с большими объёмами данных, где лишние аллокации и копии сильно бьют по производительности (особенно на inference). Cow даёт «бесплатную» оптимизацию: пишешь код так, будто у тебя всегда owned-данные, а Rust копирует только когда действительно нужно изменить. Это делает код проще и быстрее, чем ручное ветвление на &str vs String или &[T] vs Vec.
1. Обработка текстовых данных (NLP-задачи)
- Токенизация и препроцессинг текста
Входной текст часто приходит как &str. Токенизатор может возвращать Vec<Cow<str>>: если токен — это подстрока оригинального текста (без изменений), используется Borrowed, если требуется модификация (lowercase, stemming, добавление special tokens) — только тогда создаётся Owned(String).
Примеры: rust-bert, huggingface tokenizers (в некоторых реализациях), кастомные токенизаторы. - Нормализация и очистка строк
Очистка от стоп-слов, HTML-тегов, пунктуации — Cow позволяет делать модификации лениво, копируя только те строки, которые действительно изменились. - Детокенизация
При генерации текста из токенов часто можно ссылаться на оригинальные подстроки, избегая лишних аллокаций.
2. API моделей и inference
- Приём входных данных
Функции inference принимают Cow<str> (или Cow<[u8]> для байтовых данных), позволяя вызывающему коду передавать как &str, так и String без лишних копий.
Это удобно в веб-сервисах (Axum, Actix), где запросы приходят в разных формах. - Возврат результатов
Модель может возвращать Cow<str> для сгенерированного текста или предсказанных лейблов — borrowed, если данные взяты из статического словаря, owned — если сгенерированы динамически.
3. Работа с датасетами и фичами
- Хранение названий фич и меток классовVec<Cow<str>> или HashMap<Cow<str>, ...> для feature names / class labels. Если названия известны статически — Borrowed('static), если загружаются из файла/генерируются — Owned.
- Векторы признаковCow<[f32]> или Cow<[f64]> для батчей или отдельных сэмплов. Позволяет принимать как borrowed slice из буфера, так и owned Vec, и копировать только при необходимости (например, при нормализации/аугментации).
- DataLoader и итераторы по датасету
При чтении из CSV/Parquet/mmap-файлов строки или массивы можно держать как borrowed, а копировать только при трансформациях (shuffle, augmentation).
4. Конфигурации и метаданные моделей
- Пути к файлам моделей/чекпоинтамCow<Path> — удобно принимать как &Path, так и PathBuf, особенно в CLI-инструментах и конфигах.
- Гиперпараметры и конфиги
Строковые параметры (названия optimizer’ов, scheduler’ов) как Cow<'static, str> — статические значения без аллокаций, динамические — с копированием. - Serde (сериализация/десериализация)
В структурах моделей/датасетов поля типа Cow<str> позволяют десериализовать как borrowed (если данные в буфере), так и owned, и сериализовать эффективно. Serde имеет встроенную поддержку Cow.
5. Словарь (vocabulary) и эмбеддинги
- Vocab в NLP-моделяхHashMap<Cow<str>, usize> или BTreeMap<Cow<str>, ...> — ключи могут быть borrowed из обучающего корпуса, если не требуется владение.
- Embedding lookup
При построении embedding-таблиц можно временно использовать Cow, чтобы избежать копий при инициализации из предобученных весов.
6. Прочие нишевые случаи
- Логирование и метрики
Названия метрик, теги экспериментов как Cow<str> — часто статические строки. - Кастомные трансформации в пайплайнах
В библиотеках вроде linfa, polars-ml или кастомных пайплайнах — Cow помогает писать «универсальные» трансформации, которые работают и с borrowed, и с owned данными без дублирования кода. - Работа с байтовыми данными изображений/аудиоCow<[u8]> для raw-данных — borrowed из файла/mmap, owned после предобработки (resize, normalization).
Пример использования Cow в токенизаторе
Принимаем входной текст как &str, разбиваем его на токены и применяем нормализацию (приведение к нижнему регистру) только когда это действительно нужно. Благодаря Cow мы избегаем лишних аллокаций для токенов, которые остаются без изменений.
use std::borrow::Cow;
fn tokenize_and_normalize(text: &str) -> Vec<Cow<str>> {
text
.split_whitespace() // простая токенизация по пробелам
.map(|token| {
// Пример нормализации: приводим к нижнему регистру только если есть заглавные буквы
if token.chars().any(char::is_uppercase) {
Cow::Owned(token.to_lowercase())
} else {
// Если изменений не нужно — просто ссылаемся на оригинальную подстроку
Cow::Borrowed(token)
}
})
.collect()
}
fn main() {
let text = "Hello World, Rust is GREAT for ML!";
let tokens = tokenize_and_normalize(text);
for token in &tokens {
println!("{:?}", token);
}
// Вывод:
// "hello" ← Owned (была копия, т.к. были заглавные)
// "World," ← Owned (была копия)
// "rust" ← Owned
// "is" ← Borrowed (без изменений)
// "great" ← Owned
// "for" ← Borrowed
// "ML!" ← Owned
}
Почему это работает эффективно
- split_whitespace() возвращает итератор по &str — подстрокам оригинального text. Они живут столько же, сколько text.
- Cow::Borrowed(token) — просто ссылка, без аллокаций.
- Cow::Owned(token.to_lowercase()) — аллокация и копия происходят только для токенов, где есть заглавные буквы.
- Результат Vec<Cow<str>> можно дальше передавать в модель (например, в embedding lookup или transformer), и при необходимости вызвать .to_mut() или .into_owned() для принудительного владения.