Jak prawidłowo napisać iterator zwracający fragmenty stringa wejściowego?

0

Cześć, ćwiczebnie próbuję napisać klasę, która zapewni dopasowywanie stringów. Ze względu na naukę, chciałbym w miarę możliwości używać referencji &str. Dopóki robię proste, sekwencyjne dopasowywanie wszystko jest łatwo. Mam po prostu w strukturze referencję do tego co zostało mi do przeczytania i nie mam z tym problemów. Problem pojawia się gdy chcę powtórzyć wielokrotnie to samo dopasowanie. Jedyny pomysł, jaki przyszedł mi do głowy to stworzyć metodę, która przyjmie funkcję/lambdę robiącą poszczególne działania i stworzę iterator, który będzie robić co trzeba. Ostatecznie, chciałbym uzyskać taką składnię:

let mut it = matcher("foo=1 bar=2 coo zza=3")
              .many(|m1|
                       m1.until_word()
                         .const_str("=")
                         .merge::<u64,(&str, u64),_>(|name, val| (name,val)));

Wszystko działa dobrze, dopóki nie próbuję zwracać referencji na stringa, bo wtedy kompilator marudzi, że nie może mi zagwarantować, że ta referencja nie przeżyje obiektu, na który pokazuje. Tak więc jeśli zwracam cokolwiek innego niż referencję (liczby, String, etc) jest OK i jest to jakieś rozwiązanie. Z drugiej strony nie wydaje mi się to rozsądne, bo przecież jest dla mnie oczywiste, że nie mam się czego obawiać, a alokować miejsce na stercie ilekroć chcę pokazać kawałek tekstu z iteratora wydaje się być co najmniej nieładne i niefektywne. Tak wiec pewnie ten problem ma dobre rozwiązanie o którym nie wiem.

Coś przeoczyłem, że kompilator nie jest w stanie podążyć za cyklem życia tych referencji?
Powinienem inaczej użyć anotacji lifetimeów (wszędzie praktycznie jest 'a -> 'a, więc teoretycznie chyba mógłby załapać…)?
Może jest jakiś magiczny obiekt, który w magiczny sposób odróżni taki bezpieczny przypadek jak mam tutaj, a potencjalnie niebezpiecznym przypadkiem kiedy rzeczywście referencja przeżyje to na co pokazuje.

Poglądowo wklejam kod użytych tu klas:

trait Matchable {
    type Out;
    fn scan(inp: &str) -> Option<(&str, Self::Out)>;
}

impl Matchable for u64 {
    type Out=u64;

    fn scan(inp: &str) -> Option<(&str, u64)> {
        let it = inp.chars();
        let mut ans : u64 = 0;
        let mut i = 0;
        for c in it {
            match c.to_digit(10) {
                Some(x) => {
                    ans = ans*10 + x as u64;
                    i += 1;
                },
                None => break
            }
        }

        if i == 0 {
            None
        } else {
            Some((&inp[i..], ans))
        }
    }
}

#[cfg(test)]
struct MatcherLoop<'a, R, F>
where
    F: Fn(Matcher<()>) -> Matcher<R>,
{
    tail: Option<&'a str>,
    f: F,
}

#[cfg(test)]
impl<'a, R, F> Iterator for MatcherLoop<'a, R, F>
where
    F: Fn(Matcher<()>) -> Matcher<R>,
{
    type Item = R;

    fn next(&mut self) -> Option<Self::Item> {
        let tail = self.tail.take();
        let m = (self.f)(matcher(tail?));
        self.tail = Some(m.tail);
        m.val
    }
}

struct Matcher<'a,T> {
    tail: &'a str,
    val: Option<T>
}


impl<'a,T> Matcher <'a,T> {
    fn then<F, R> (self, f: F) -> Matcher<'a, R>
            where F: Fn(&'a str, T) -> Matcher<'a, R> {
        match self.val {
            Some(v) => f(self.tail, v),
            None => Matcher { tail: self.tail, val: None }
        }
    }

    #[cfg(test)]
    fn after(mut self, refs: &'a str) -> Self {
        let mut finder = matcher(self.tail);
        while finder.tail.len() > refs.len() {
            finder = finder.const_str(refs);
            if finder.val.is_some() {
                self.tail = finder.tail;
                return self;
            }
            finder.tail = &finder.tail[1..];
            finder.val = Some(());
        }

        self.val = None;
        return self;
    }

    #[cfg(test)]
    fn until_word(self) -> Matcher<'a, &'a str> {
        let mut cur = self.tail;
        let mut ans;
        while cur.len() > 0 {
            ans = matcher(cur).word();
            if ans.val.is_some() {
                return ans;
            }
            cur = &cur[1..];
        }

        Matcher { tail: self.tail, val: None }
    }

    #[cfg(test)]
    fn until<R: Matchable>(self) -> Matcher<'a, <R as Matchable>::Out> {
        let mut cur = self.tail;
        let mut ans;
        while cur.len() > 0 {
            ans = matcher(cur).value::<R>();
            if ans.val.is_some() {
                return ans;
            }
            cur = &cur[1..];
        }

        Matcher { tail: self.tail, val: None }
    }

    #[cfg(test)]
    fn many<R,F>(self, f:F) -> MatcherLoop<'a,R,F>
            where F: Fn(Matcher<()>) -> Matcher<R> {
        MatcherLoop { tail: Some(self.tail), f }
    }

    #[cfg(test)]
    fn word(self) -> Matcher<'a, &'a str> {
        let mut i: usize = 0;
        let mut cur = self.tail.chars();
        while cur.next().map(|c| c.is_alphabetic()).unwrap_or(false) {
            i += 1;
        }

        if i > 0 {
            Matcher { tail: &self.tail[i..], val: Some(&self.tail[0..i]) }
        } else {
            Matcher { tail: self.tail, val: None }
        }
    }

    fn const_str(self, refs: &'a str) -> Self {
        self.then(|tail, val| {
            let mut it = tail.chars();
            for refc in refs.chars() {
                match it.next() {
                    Some(c) => match c == refc {
                                    true => (),
                                    false => return Matcher{ tail, val: None }
                            },
                    None => return Matcher { tail, val: None }
                }
            }

            Matcher { tail: it.as_str(), val: Some(val) }
        })
    }

    fn value<R: Matchable> (self) -> Matcher<'a,<R as Matchable>::Out> {
        self.then(|tail, _| {
            match R::scan(tail) {
                Some((t2, val)) => Matcher { tail: t2, val: Some(val) },
                None => Matcher { tail, val: None }
            }
        })
    }

    fn merge<R, V, F> (self, f: F) -> Matcher<'a, V>
            where R: Matchable,
                  F: Fn(T, <R as Matchable>::Out) -> V {
        self.then(|tail, v1| {
            match R::scan(tail) {
                Some((t2, v2)) => Matcher { tail: t2, val: Some(f(v1, v2)) },
                None => Matcher { tail, val: None }
            }
        })
    }

    fn result(self) -> Option<T> {
        self.val
    }

    #[cfg(test)]
    fn result_ref(&self) -> &Option<T> {
        &self.val
    }
}

fn matcher<'a>(src: &'a str) -> Matcher<'a, ()> {
    Matcher { tail: src, val: Some(()) }
}
0

Dużo tego kodu. A jakbyś napisał jakiś bardziej minimalny przykład i wypisał konkretny błąd do konkretnej linijki?

wtedy kompilator marudzi, że nie może mi zagwarantować, że ta referencja nie przeżyje obiektu, na który pokazuje

Ale ten błąd dotyczy którego miejsca w kodzie i których dokładnie referencji?

0

No to minimalny przykład będzie taki:

        let mut it = matcher("foo=1 bar=2 zza=3")
                       .many(|m1|
                             m1.until_word());

Wtedy komunikat błędu będzie taki:

|| error: lifetime may not live long enough
src/main.rs|370 col 6 error| 
||     |
|| 369 |                        .many(|m1|
||     |                               --- return type of closure is Matcher<'_, &'2 str>
||     |                               |
||     |                               has type `Matcher<'1, ()>`
|| 370 |                              m1.until_word());
||     |                              ^^^^^^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`
|| 
|| error: could not compile `vidvid` due to previous error

0
elwis napisał(a):

a alokować miejsce na stercie ilekroć chcę pokazać kawałek tekstu z iteratora wydaje się być co najmniej nieładne i niefektywne. Tak wiec pewnie ten problem ma dobre rozwiązanie o którym nie wiem.

Nie rozumiem tu czegoś jak chcesz zwrócić wartość bez alokowania tego na heapie?

Normalnie z funkcji możesz zwrócić jeden adres w RAX rejestrze, ten adres to może być jakiś obiekt, który będzie enkapsulował więcej danych.
W C++ byś stworzył obiekt na heapie tego stringa, a potem za pomocą std::move konstruktora przeniósł prawa własności do obiektu i jego życia do innej funkcji.
W dół możesz przenieść samą referencję, gdyż rodzic funkcja będzie żyła dłużej od dziecka, a w górę funkcji musisz przenieść prawa własności obiektu, gdyż funkcja dziecka umrze, tzn. jego ramka stosu zostanie zniszczona.
I teraz funkcja, która odebrała obiekt nie musi się obawiać, że obiekt zginie, gdyż teraz jest jej i dopiero zostanie usunięty gdy skończy się funkcja, chyba że znowu komuś odda prawa własności do obiektu.

Nie wiem jak w rust jest to rozwiązanie, ale jeśli alokuje na stosie, to nie jest w stanie tych danych przekazać, musi przekopiować, przez heap idzie bez problemowo.
Rust mógł także nie tworzyć nowego stringa, a po prostu odwoływać się do tego statycznego kawałka tekstu, to akurat łatwo pójdzie sprawdzić w listingu assemblera.

Nieładne i nieefektywne?
Jak zwracasz przez heap to jest efektywne, bo tylko przekazujesz adres i zmieniasz właściciela obiektu, jeśli masz na stosie to musisz i tak wykonać przekopiowywanie, gdyż nie da się fizycznie tego inaczej przenieść.
I odwoływanie się do statycznego stringa w formie indeksów to jest nieładne, bo używasz jednego długiego stringa jako odniesienia do 3 innych, w C się takie coś robiło w strtok, co oczywiście nie powinno się robić na const static, ale pozwala zaoszczędzić na pamięci gdyż ją mutujemy.

0

A to nie chodzi o to, że wyrzucasz to z domknięcia i że te dwa lifetime'y ze sobą nie współgrają?

W końcu masz jakiś lifetime w tej strukturze i spoko, ale jak dajesz niezależne od struktury domknięcie, to generuje ono kolejny lifetime i kompilator nie wie, że to to samo?

Nie ogarniam tego kodu do końca i nie wiem, czy moje rozumowanie jest poprawne, ale tak intuicyjnie bym w tym kierunku popróbował.

0
tumor napisał(a):
elwis napisał(a):

a alokować miejsce na stercie ilekroć chcę pokazać kawałek tekstu z iteratora wydaje się być co najmniej nieładne i niefektywne. Tak wiec pewnie ten problem ma dobre rozwiązanie o którym nie wiem.

Nie rozumiem tu czegoś jak chcesz zwrócić wartość bez alokowania tego na heapie?

W tym przypadku to akurat nie ma sensu niczego alokować na stercie, bo ten string żyje sobie pewnie w sekcji .rodata i jest tam przez cały czas wykonywania programu. Stringi w Ruscie są reprezentowane przez wskaźnik+długość, więc można spokojnie mnożyć dowolne podciągi w oparciu o ten sam fragment pamięci. Gdyby nawet nie był, to przecież nie wyrzucam tej referencji nigdzie poza blok, w którym ten string jest zaalokowany, więc nie widzę problemu.

LukeJL napisał(a):

A to nie chodzi o to, że wyrzucasz to z domknięcia i że te dwa lifetime'y ze sobą nie współgrają?

W końcu masz jakiś lifetime w tej strukturze i spoko, ale jak dajesz niezależne od struktury domknięcie, to generuje ono kolejny lifetime i kompilator nie wie, że to to samo?

Nie ogarniam tego kodu do końca i nie wiem, czy moje rozumowanie jest poprawne, ale tak intuicyjnie bym w tym kierunku popróbował.

Myślę, że kompilator to właśnie sugeruje, tylko nie wiem czemu uważa, że wartość wyjściowa ma inny lifetime niż wejściowa, skoro postać funkcji until_word jednoznacznie stwierdza, że lifetime zostanie ten sam: fn until_word(self) -> Matcher<'a, &'a str> .

1

Też nie do końca tak jest, że zawsze dane są w .rodata, kompilator często wrzuca statyczny string za pomocą movabs [adres], constant, nawet kilka pod rząd tak jak się nie mieści w jednym rejestrze, ma to dwie zalety, nie musi oddzielnie cachować stringa, za jednym razem cachuje i instrukcje i stałą, często wysokich optymalizacjach kompilator stosuje ten trik.

Możne tak zrobić sobie miejsce na stosie i przekopiować za pomocą movabs qword [rsp], 'asdfghij', a potem po wyjściu z funkcji dane są tracone, może też tego użyć żeby do heapu wpisać.

Podejrzyj w listingu assemblera, jak przechowywany jest ten string, żeby dopuścić do kompilacji możesz nie zwracać go tymczasowo.

0

Myślę, że kompilator to właśnie sugeruje, tylko nie wiem czemu uważa, że wartość wyjściowa ma inny lifetime niż wejściowa, skoro postać funkcji until_word jednoznacznie stwierdza, że lifetime zostanie ten sam: fn until_word(self) -> Matcher<'a, &'a str> .

Ale dla struktury, w której jest to zdefiniowane. A jak robisz tak:

 .many(|m1|
   m1.until_word());

to 'a odnosi się do m1, a m1 jest tak definiowane:

    fn many<R,F>(self, f:F) -> MatcherLoop<'a,R,F>
            where F: Fn(Matcher<()>) -> Matcher<R> {

czyli jest tu jakiś przeskok i kompilator nie wie, że to jest to samo. Czyli stąd to drugie lifetime.

Ale tak ogólnie, to nie rozumiem tego kodu na poziomie koncepcyjnym. Do czego służy funkcja many? Co ma zwracać callback:

    .many(|m1|
          m1.until_word());

?
o co chodzi z Matcher<R, F>?

0
LukeJL napisał(a):

Ale dla struktury, w której jest to zdefiniowane. A jak robisz tak:

 .many(|m1|
   m1.until_word());

to 'a odnosi się do m1, a m1 jest tak definiowane:

    fn many<R,F>(self, f:F) -> MatcherLoop<'a,R,F>
            where F: Fn(Matcher<()>) -> Matcher<R> {

czyli jest tu jakiś przeskok i kompilator nie wie, że to jest to samo. Czyli stąd to drugie lifetime.

To jest fakt, że powinno być F: for <'b> Fn(Matcher<'b, ()>) -> Matcher<'b, R> i analogicznie w definicji MatcherLoop. To jednak nie naprawia problemu.

Ale tak ogólnie, to nie rozumiem tego kodu na poziomie koncepcyjnym. Do czego służy funkcja many? Co ma zwracać callback:

    .many(|m1|
          m1.until_word());

?
o co chodzi z Matcher<R, F>?

many zapewnia wielokrotne dopasowanie. Funkcja f:F dostaje matchera i robi dopasowanie, bo jak inaczej dobrze zamodelować listę działań? Tym bardziej, że chcę mieć kroki z typem (jak value::<u64>), chyba nie mogę tak zrobić z enumem. W związku z tym Many zwraca MatcherLoop<R,F>, który jest iteratorem, który dopasowuje, zwraca wartość i w następnym zaczyna dopasowanie od miejsca w którym skończył poprzednie. R jest typem dopasowania, a F jest typem funkcji, co jest konieczne ze względu na to, że każda lambda ma swój typ, więc jeśli chcę je wspierać to tak trzeba zrobić. Przynajmniej nie znam innego sposobu.

0
LukeJL napisał(a):

Ale dla struktury, w której jest to zdefiniowane. A jak robisz tak:

 .many(|m1|
   m1.until_word());

to 'a odnosi się do m1, a m1 jest tak definiowane:

    fn many<R,F>(self, f:F) -> MatcherLoop<'a,R,F>
            where F: Fn(Matcher<()>) -> Matcher<R> {

czyli jest tu jakiś przeskok i kompilator nie wie, że to jest to samo. Czyli stąd to drugie lifetime.

To jest fakt, że powinno być F: for <'b> Fn(Matcher<'b, ()>) -> Matcher<'b, R> i analogicznie w definicji MatcherLoop. To jednak nie naprawia problemu.

Ale tak ogólnie, to nie rozumiem tego kodu na poziomie koncepcyjnym. Do czego służy funkcja many? Co ma zwracać callback:

    .many(|m1|
          m1.until_word());

?
o co chodzi z Matcher<R, F>?

many zapewnia wielokrotne dopasowanie. Funkcja f:F dostaje matchera i robi dopasowanie, bo jak inaczej dobrze zamodelować listę działań? Tym bardziej, że chcę mieć kroki z typem (jak value::<u64>), chyba nie mogę tak zrobić z enumem. W związku z tym Many zwraca MatcherLoop<R,F>, który jest iteratorem, który dopasowuje, zwraca wartość i w następnym zaczyna dopasowanie od miejsca w którym skończył poprzednie. R jest typem dopasowania, a F jest typem funkcji, co jest konieczne ze względu na to, że każda lambda ma swój typ, więc jeśli chcę je wspierać to tak trzeba zrobić. Przynajmniej nie znam innego sposobu.

1

Znalazłem rozwiązanie. Problem tkwił w definicji MatcherLoop i funkcji Many. Było:

struct MatcherLoop<'a, R, F>
where
    F: Fn(Matcher<()>) -> Matcher<R>,

//...

impl<'a, R, F> Iterator for MatcherLoop<'a, R, F>
where
    F: Fn(Matcher<()>) -> Matcher<R>,

//...

fn many<R,F>(self, f:F) -> MatcherLoop<'a,R,F>
            where F: Fn(Matcher<()>) -> Matcher<R> {

należało wszędzie w definicjach typu funkcji konsekwentnie podawać lifetime objektu:

struct MatcherLoop<'a, R, F>
where
    F: Fn(Matcher<'a, ()>) -> Matcher<'a, R>,

//...

impl<'a, R, F> Iterator for MatcherLoop<'a, R, F>
where
    F: Fn(Matcher<'a, ()>) -> Matcher<'a, R>,

//...

fn many<R,F>(self, f:F) -> MatcherLoop<'a,R,F>
            where F: Fn(Matcher<'a, ()>) -> Matcher<'a, R> {

I wtedy wszystko gra, pozwala zwracać &str bez problemu.

1 użytkowników online, w tym zalogowanych: 0, gości: 1