pub trait Explainable {
fn do_explain(&self)-> String;
}
pub trait HasAccount {
fn get_account(&self)-> String;
fn change_account(&mut self, new_value: &str);
}
pub struct Person {
name: String,
account: String
}
impl Explainable for Person {
fn do_explain(&self)-> String {
return self.name.clone() + " " + self.account.as_str()
}
}
impl HasAccount for Person {
fn change_account(&mut self, new_value: &str) {
self.account = new_value.to_string();
}
fn get_account(&self)-> String {
return self.account.clone();
}
}
fn change_accounts<'a>(persons: impl Iterator<Item = &'a mut Person>) {
for p in persons {
println!("Changing person: {}", p.name);
let mut new_str= String::with_capacity(100);
new_str.push_str(&p.account);
new_str.push_str("_020");
p.change_account(&new_str);
}
}
fn change_accounts2<'a>(persons: impl Iterator<Item = &'a mut dyn HasAccount>) {
for p in persons {
let mut new_str = String::with_capacity(100);
new_str.push_str(&p.get_account());
new_str.push_str("_020");
p.change_account(&new_str);
}
}
fn change_accounts3<'a, I>(persons: I)
where
I: IntoIterator<Item = &'a mut dyn HasAccount>,
{
for p in persons {
let mut new_str = String::with_capacity(100);
new_str.push_str(&p.get_account());
new_str.push_str("_020");
p.change_account(&new_str);
}
}
fn change_accounts4<'a, I>(persons: I)
where
I: IntoIterator<Item = &'a mut Box<dyn HasAccount>>,
{
for p in persons {
let mut new_str = String::with_capacity(100);
new_str.push_str(&p.get_account());
new_str.push_str("_020");
p.change_account(&new_str);
}
}
// Trait with both traits
trait Entity: Explainable + HasAccount {}
impl<T: Explainable + HasAccount> Entity for T {}
fn explain_all<'a>(items: impl IntoIterator<Item = &'a Box<dyn Entity>>) {
for item in items {
println!("{}", item.do_explain());
}
}
fn change_all<'a>(items: impl IntoIterator<Item = &'a mut Box<dyn Entity>>) {
for item in items {
let mut new_str = item.get_account();
new_str.push_str("_mod");
item.change_account(&new_str);
}
}
fn get_boxed_entities1() -> Vec<Box<dyn Entity>> {
vec![
Box::new(Person { name: "Piet".into(), account: "abc".into() }),
Box::new(Person { name: "Klaas".into(), account: "xyz".into() }),
]
}
// return an iterator
fn get_boxed_entities2() -> impl Iterator<Item = Box<dyn Entity>> {
return vec![
Box::new(Person { name: "Piet".into(), account: "abc".into() }),
Box::new(Person { name: "Klaas".into(), account: "xyz".into() }),
]
.into_iter()
.map(|p| p as Box<dyn Entity>)
}
fn main() {
println!("Hello, world!");
let mut persons = vec![
Person { name: "Jan".into(), account: "2345234".into() },
Person { name: "Kees".into(), account: "342523".into() }
];
for p in persons.iter() { // <- using .iter() here to allow multiple loops
let s = p.do_explain();
println!("{}", s);
}
for p in persons.iter_mut() {
let new_str= p.account.clone() + "_010";
p.change_account(&new_str);
}
change_accounts(persons.iter_mut());
change_accounts(persons.iter_mut());
change_accounts2(persons.iter_mut().map(|p| p as &mut dyn HasAccount));
change_accounts3(persons.iter_mut().map(|p| p as &mut dyn HasAccount));
for p in persons.iter() {
let s = p.do_explain();
println!("{}", s);
}
// step 2
let mut boxed: Vec<Box<dyn HasAccount>> = vec![
Box::new(Person { name: "Piet".into(), account: "abc".into() }),
Box::new(Person { name: "Klaas".into(), account: "xyz".into() }),
];
change_accounts4(boxed.iter_mut()); //boxed.iter_mut().map(|p| p.as_mut())
// step 3
let mut list: Vec<Box<dyn Entity>> = vec![
Box::new(Person { name: "Jan".into(), account: "123".into() }),
Box::new(Person { name: "Kees".into(), account: "456".into() }),
];
// read
explain_all(list.iter()); //.map(|x| x.as_ref())
// write
let iter = list.iter_mut(); //.map(|x| x.as_mut())
change_all(iter);
// read again
explain_all(list.iter()); //.map(|x| x.as_ref())
let mut list2 = get_boxed_entities1();
explain_all(list2.iter());
let iter = list2.iter_mut();
change_all(iter);
explain_all(list2.iter());
let mut list3: Vec<Box<dyn Entity>> = get_boxed_entities2().collect();
explain_all(list3.iter());
change_all(list3.iter_mut());
explain_all(list3.iter());
println!("Finished");
}
trait IntoBoxedA<'a>: IntoIterator<Item = &'a Box<dyn A>> {}
impl<'a, T> IntoBoxedA<'a> for T where T: IntoIterator<Item = &'a Box<dyn A>> {}
// 3 variants with the same behaviour:
fn process1<'a, T: IntoBoxedA<'a>>(items: T) {
...
}
fn process1<'a, T>(items: T)
where
T: IntoBoxedA<'a>
{
...
}
fn process1<'a>(items: impl IntoBoxedA<'a>) {
...
}
π Kies impl Trait voor korte functies, en de T: Trait-vorm als je meerdere trait bounds of het concrete generieke type nodig hebt.
Per trait a concrete alias
trait IntoBoxedA<'a>: IntoIterator<Item = &'a Box<dyn A>> {}
impl<'a, T> IntoBoxedA<'a> for T where T: IntoIterator<Item = &'a Box<dyn A>> {}
trait IntoBoxedB<'a>: IntoIterator<Item = &'a Box<dyn B>> {}
impl<'a, T> IntoBoxedB<'a> for T where T: IntoIterator<Item = &'a Box<dyn B>> {}
trait IntoBoxedC<'a>: IntoIterator<Item = &'a Box<dyn C>> {}
impl<'a, T> IntoBoxedC<'a> for T where T: IntoIterator<Item = &'a Box<dyn C>> {}
fn process1<'a, T: IntoBoxedA<'a>>(items: T) {
...
}
fn process2<'a, T: IntoBoxedB<'a>>(items: T) {
...
}
macro_rules! define_into_boxed {
($name:ident, $trait:path) => {
pub trait $name<'a>: IntoIterator<Item = &'a Box<dyn $trait>> {}
impl<'a, T> $name<'a> for T
where
T: IntoIterator<Item = &'a Box<dyn $trait>>,
{}
};
}
trait A {
fn do_a(&self);
}
trait B {
fn do_b(&self);
}
trait C {
fn do_c(&self);
}
define_into_boxed!(IntoBoxedA, A);
define_into_boxed!(IntoBoxedB, B);
define_into_boxed!(IntoBoxedC, C);
fn process1<'a>(items: impl IntoBoxedA<'a>) {
for a in items {
a.do_a();
}
}
fn process2<'a>(items: impl IntoBoxedB<'a>) {
for b in items {
b.do_b();
}
}
Limetime errors
// error[E0597]: `boxed` does not live long enough
// error[E0759]: `boxed` captured by a closure, but it is dropped before the closure is called
let mut boxed: Vec<Box<dyn HasAccount>> = vec![
Box::new(Person { name: "Piet".into(), account: "abc".into() }),
Box::new(Person { name: "Klaas".into(), account: "xyz".into() }),
];
edit_accounts(boxed.iter_mut().map(|p| p.as_mut()));
// solution: collect into a Vec, this causes the iterator to be used
let mut boxed: Vec<Box<dyn HasAccount>> = vec![
Box::new(Person { name: "Piet".into(), account: "abc".into() }),
Box::new(Person { name: "Klaas".into(), account: "xyz".into() }),
];
let mapped: Vec<_> = boxed.iter_mut().map(|p| p.as_mut()).collect();
edit_accounts(mapped);
// link original variable lifetime to iterator:
fn make_iter<'a>(
boxed: &'a mut [Box<dyn HasAccount>]
) -> impl Iterator<Item = &'a mut dyn HasAccount> {
boxed.iter_mut().map(|b| b.as_mut())
}
// Full example:
trait HasAccount {
fn set_account(&mut self, account: &str);
}
struct Person {
name: String,
account: String,
}
impl HasAccount for Person {
fn set_account(&mut self, account: &str) {
self.account = account.to_string();
}
}
fn make_iter<'a>(
boxed: &'a mut [Box<dyn HasAccount>]
) -> impl Iterator<Item = &'a mut dyn HasAccount> {
boxed.iter_mut().map(|b| b.as_mut())
}
fn edit_accounts<'a, I>(accounts: I)
where
I: Iterator<Item = &'a mut dyn HasAccount>,
{
for a in accounts {
a.set_account("updated");
}
}
fn main() {
let mut boxed: Vec<Box<dyn HasAccount>> = vec![
Box::new(Person { name: "Piet".into(), account: "abc".into() }),
Box::new(Person { name: "Klaas".into(), account: "xyz".into() }),
];
let iter = make_iter(&mut boxed);
edit_accounts(iter);
}
Service returning array/vec
Goede vraag β dit raakt precies de ownership-filosofie van Rust π
Het korte antwoord is: nee, je hoeft meestal géén lijst mee te geven om te vullen.
De meest idiomatische manier is dat je functie zelf de data maakt en deze teruggeeft, zodat de caller automatisch owner wordt.
Hieronder zet ik de patronen op een rij, van meest aanbevolen β meer uitzonderlijk.
β
1. Functie retourneert een Vec<T> (aanbevolen)
Dit is veruit het meest gebruikte en duidelijke patroon.
struct Item {
id: i32,
name: String,
}
fn load_items() -> Vec<Item> {
let mut items = Vec::new();
items.push(Item { id: 1, name: "A".into() });
items.push(Item { id: 2, name: "B".into() });
items
}
Gebruik:
fn main() {
let items = load_items(); // main is owner
println!("{}", items.len());
}
Waarom dit goed is
- Ownership is glashelder
- Geen lifetimes nodig
- Compiler kan optimaliseren (NRVO / move elision)
- Sluit perfect aan bij Rust-idiomen
π In 90% van de gevallen is dit wat je wilt
β 2. Result + ownership (bij fouten / IO / services)
Voor services is dit vaak nΓ³g realistischer:
fn load_items() -> Result<Vec<Item>, Box<dyn std::error::Error>> {
Ok(vec![
Item { id: 1, name: "A".into() },
Item { id: 2, name: "B".into() },
])
}
Gebruik:
let items = load_items()?; // caller is owner
β οΈ 3. Caller geeft een &mut Vec<T> mee (meestal niet nodig)
Dit kan, maar is alleen zinvol als je expliciet wilt toevoegen aan bestaande data.
fn load_items_into(items: &mut Vec<Item>) {
items.push(Item { id: 1, name: "A".into() });
}
Gebruik:
let mut items = Vec::new(); load_items_into(&mut items);
Nadelen
- Minder duidelijk ownership
- Minder ergonomisch
- Sneller borrow-complexiteit
π Gebruik dit alleen als:
- je wilt appendβen
- performancekritisch bent
- of API-compatibiliteit nodig hebt
β 4. Iterator teruggeven (lazy / streaming)
Als je service potentieel veel data heeft of lazy wil zijn:
fn load_items() -> impl Iterator<Item = Item> {
vec![
Item { id: 1, name: "A".into() },
Item { id: 2, name: "B".into() },
].into_iter()
}
Gebruik:
let items: Vec<Item> = load_items().collect();
Caller is alsnog owner.
π§ 5. Service-struct met methode (typisch voor βservicesβ)
In jouw βserviceβ-context waarschijnlijk het mooist:
struct ItemService;
impl ItemService {
fn get_items(&self) -> Vec<Item> {
vec![
Item { id: 1, name: "A".into() },
Item { id: 2, name: "B".into() },
]
}
}
Gebruik:
let service = ItemService; let items = service.get_items(); // ownership hier
β Wat je meestal niet wilt
fn get_items<'a>() -> &'a Vec<Item> { ... }
β Probleem:
- Wie is owner?
- Lifetime hell
- Vaak onmogelijk zonder
staticof interne opslag
π§ Beslisboom
| Situatie | Beste keuze |
|---|---|
| Caller moet owner zijn | fn -> Vec<T> |
| Kan falen | fn -> Result<Vec<T>, E> |
| Grote/lazy data | fn -> impl Iterator<Item = T> |
| Append aan bestaande data | fn(&mut Vec<T>) |
| Service-achtig | impl Service { fn -> Vec<T> } |
π― Samenvatting
Rust wil dat ownership expliciet is β en retourneren is de schoonste vorm daarvan.
βοΈ Laat je functie gewoon een Vec<T> teruggeven
β Niet vullen via parameters tenzij je daar echt een reden voor hebt
Als je wilt, kan ik dit ook laten zien met:
- async services
- database calls
- trait-based services
Arc<Vec<T>>/ shared ownership
Zeg maar π
Zeker π Hieronder laat ik drie veelgebruikte manieren zien om deze functie aan te roepen, mΓ©t nette error handling. Ik bouw het voorbeeld stap voor stap op.
Voor de volledigheid eerst een minimale Item:
#[derive(Debug)]
struct Item {
id: i32,
name: String,
}
En jouw functie:
fn load_items() -> Result<Vec<Item>, Box<dyn std::error::Error>> {
Ok(vec![
Item { id: 1, name: "A".into() },
Item { id: 2, name: "B".into() },
])
}
β
1. Idiomatic: ? gebruiken (meest aanbevolen)
Dit is wat je in bijna alle Rust-services en binaries ziet.
fn main() -> Result<(), Box<dyn std::error::Error>> {
let items = load_items()?; // fout? β automatisch terug uit main
for item in items {
println!("{:?}", item);
}
Ok(())
}
Wat gebeurt hier?
load_items()geeftResult<Vec<Item>, E>?:- bij
Okβ unwrapt deVec<Item> - bij
Errβ returned direct uitmain
- bij
mainmagResultteruggeven β Rust print automatisch de fout
βοΈ Simpel, leesbaar, idiomatisch
β 2. Expliciet matchen (als je zelf wilt reageren)
Handig als je logging of alternatieve flow wilt.
fn main() {
match load_items() {
Ok(items) => {
println!("Items geladen:");
for item in items {
println!("{:?}", item);
}
}
Err(err) => {
eprintln!("β Fout bij laden van items: {}", err);
}
}
}
βοΈ Volledige controle
β Meer boilerplate
β
3. if let Err + early return
Fijn als je snel wilt stoppen:
fn main() {
let items = match load_items() {
Ok(items) => items,
Err(e) => {
eprintln!("β Kan items niet laden: {}", e);
return;
}
};
println!("{} items geladen", items.len());
}
π§ͺ 4. Met eigen fouttype (iets geavanceerder)
Als je service groter wordt, is dit vaak beter dan Box<dyn Error>:
use std::fmt;
#[derive(Debug)]
enum LoadError {
DatabaseDown,
}
impl fmt::Display for LoadError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Database is niet bereikbaar")
}
}
impl std::error::Error for LoadError {}
fn load_items() -> Result<Vec<Item>, LoadError> {
Err(LoadError::DatabaseDown)
}
Gebruik:
fn main() -> Result<(), LoadError> {
let items = load_items()?;
println!("{:?}", items);
Ok(())
}
π§ Samenvatting
| Situatie | Aanpak |
|---|---|
| CLI / service | main() -> Result<()> + ? |
| UI / API | match of map_err |
| Library code | Eigen error type |
| Snel stoppen | match + return |
π Aanrader voor jouw voorbeeld:
Gebruik variant #1 (? in main) β schoon en Rust-idiomatisch.
Als je wilt, kan ik dit ook laten zien in:
- async context (
tokio) - Web API handler
- unit tests (
assert!(load_items().is_ok())) - service trait met meerdere implementaties
Mooi voorbeeld π Dit leent zich perfect om te laten zien hoe je in Rust iteratoren maakt die hetzelfde doen als JS function*.
Rust heeft geen generators zoals JS, maar je bereikt hetzelfde met:
- structs +
Iteratortrait - of iterator-adapters (
map,flat_map) - zero-copy waar mogelijk (
&strslices)
Ik laat hieronder een bewust eenvoudige CSV-parser zien, vergelijkbaar met jouw JS-code (dus geen volledige RFC4180).
π― Doel (1-op-1 met jouw JS)
- String β regels iterator
- Regel β CSV-waarden iterator
- Waarden β decode / trim
1οΈβ£ Iterator: regels uit een string
In Rust is dit verrassend simpel: lines() is al een iterator.
fn get_lines_from_string(source: &str) -> impl Iterator<Item = &str> {
source.lines()
}
Gebruik:
for line in get_lines_from_string(csv) {
println!("line: {}", line);
}
2οΈβ£ Iterator: CSV-waarden uit één regel
Hier maken we zelf een iterator (vergelijkbaar met function*).
CSV-iterator struct
struct CsvValues<'a> {
line: &'a str,
separator: char,
pos: usize,
in_string: bool,
}
Constructor
fn get_csv_values_from_line<'a>(
line: &'a str,
separator: char,
) -> CsvValues<'a> {
CsvValues {
line,
separator,
pos: 0,
in_string: false,
}
}
Iterator-implementatie
impl<'a> Iterator for CsvValues<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
let bytes = self.line.as_bytes();
let start = self.pos;
while self.pos < bytes.len() {
let c = bytes[self.pos] as char;
if c == '"' {
self.in_string = !self.in_string;
} else if !self.in_string && c == self.separator {
let value = &self.line[start..self.pos];
self.pos += 1; // separator overslaan
return Some(value);
}
self.pos += 1;
}
if start < self.line.len() {
return Some(&self.line[start..]);
}
None
}
}
βοΈ Dit werkt lazy
βοΈ Geen allocaties
βοΈ Net als JS yield
3οΈβ£ Waarde decoderen (zoals JS decodeValue)
fn decode_value(value: &str) -> String {
value
.trim()
.trim_matches('"')
.replace("\"\"", "\"")
}
4οΈβ£ Alles samen (zoals jouw JS for..of)
fn main() {
let csv = r#"value1,"value,with,comma","value ""with quotes"" inside",value4"#;
for line in get_lines_from_string(csv) {
println!("======================");
for raw in get_csv_values_from_line(line, ',') {
let value = decode_value(raw);
println!("value: {}", value);
}
}
}
π¨οΈ Output
====================== value: value1 value: value,with,comma value: value "with quotes" inside value: value4
π§ Vergelijking JS vs Rust
| JS | Rust |
|---|---|
function* | struct + impl Iterator |
yield | Some(value) in next() |
| Lazy | Lazy |
| String copies | Zero-copy (&str) |
| Runtime checks | Compile-time safety |
π Bonus: iterator chaining (erg Rust-achtig)
Je kunt dit ook combineren:
let values: Vec<String> =
get_lines_from_string(csv)
.flat_map(|l| get_csv_values_from_line(l, ','))
.map(decode_value)
.collect();
π Wanneer dit patroon gebruiken?
- streaming parsing
- grote bestanden
- zero-copy verwerking
- pipeline-achtige logica (net als JS generators)
Als je wilt, kan ik ook:
- async iterators laten zien
- een
CsvParserservice maken - of laten zien hoe dit eruit ziet met
BufRead(file streaming)
Zeg maar π
Ja β dit is precies het punt waar veel mensen uit JS/Python tegenaan lopen in Rust π
Het korte, eerlijke antwoord is:
β Rust heeft geen
yield return-syntax zoals JS of C#
β Maar je kunt bijna hetzelfde ergonomische resultaat krijgen met een paar idiomatische patronen.
Ik laat ze oplopend zien, beginnend bij wat het dichtst bij yield komt.
β
Optie 1: std::iter::from_fn (het dichtst bij yield return)
Dit is conceptueel bijna 1-op-1 yield.
Definitie zoals jij vroeg
fn get_csv_values_from_line<'a>(
line: &'a str,
separator: char,
) -> impl Iterator<Item = &'a str> {
let mut pos = 0usize;
let mut in_string = false;
std::iter::from_fn(move || {
let bytes = line.as_bytes();
let start = pos;
while pos < bytes.len() {
let c = bytes[pos] as char;
if c == '"' {
in_string = !in_string;
} else if !in_string && c == separator {
let value = &line[start..pos];
pos += 1;
return Some(value); // β¬
οΈ yield
}
pos += 1;
}
if start < line.len() {
pos = line.len();
return Some(&line[start..]); // β¬
οΈ final yield
}
None
})
}
Gebruik
for v in get_csv_values_from_line(line, ',') {
println!("value: {}", v);
}
Waarom dit zo goed is
- Geen extra struct nodig
- Geen
impl Iterator for ... - State zit netjes in de closure
Some(...)βyield
π Dit is meestal wat je zoekt als je βyield returnβ denkt
π‘ Optie 2: Custom struct + impl Iterator (meest expliciet)
Dit had je al gezien β dit is meer boilerplate, maar wel het fundament.
Gebruik dit als:
- je iterator herbruikbaar is
- je state wilt exposen
- je performance-maximalisme wilt
π‘ Optie 3: Vec bouwen en into_iter() (geen echte yield)
fn get_csv_values_from_line<'a>(
line: &'a str,
separator: char,
) -> impl Iterator<Item = &'a str> {
let mut values = Vec::new();
let mut start = 0;
let mut in_string = false;
for (i, c) in line.char_indices() {
if c == '"' {
in_string = !in_string;
} else if !in_string && c == separator {
values.push(&line[start..i]);
start = i + 1;
}
}
values.push(&line[start..]);
values.into_iter()
}
β Niet lazy
β Extra allocaties
βοΈ Simpel
π Optie 4: Nightly Rust (#![feature(generators)]) β
Er bestaan generators in nightly Rust, maar:
- niet stabiel
- syntax is anders
- niet aanbevolen voor productie
π§ Vergelijking met C#/JS
| Concept | JS / C# | Rust |
|---|---|---|
yield | yield value | Some(value) |
| Generator state | impliciet | closure / struct |
| Lazy | ja | ja |
| Syntax sugar | ja | nee (bewust) |
| Zero-cost | β | β |
π― Conclusie (belangrijk)
Als je in Rust denkt:
βIk wil gewoon een yield-return-achtig dingβ
π gebruikstd::iter::from_fn
Het is:
- compact
- leesbaar
- idiomatisch
- exact jouw gevraagde signature
Als je wilt, kan ik:
- async
yield-achtige voorbeelden geven - laten zien hoe dit eruitziet bij file streaming
- of dit ombouwen naar een echte CSV-service
Zeg maar π