Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemen baca ayat atau perenggan. #2

Open
Thaza-Kun opened this issue Apr 4, 2024 · 8 comments
Open

Implemen baca ayat atau perenggan. #2

Thaza-Kun opened this issue Apr 4, 2024 · 8 comments
Labels
enhancement New feature or request good first issue Good for newcomers

Comments

@Thaza-Kun
Copy link
Owner

Buat masa sekarang, struct Phonotactic hanya boleh baca satu frasa. Jarak atau mana-mana tanda baca tidak dikenali.

Ciri membaca ayat atau perenggan membolehkan Phonotactic mengenal pasti frasa-frasa yang mengikut tatabunyi dan yang tidak mengikut tatabunyi.

Cadangan API:

impl Phonotactic {
     pub fn parse_paragraph<'a>(&'a self, input: &'a String) -> Vec<IResult<&'a str, Phrase<&'a str>>>;
}

dengan contoh interaksi yang sebegini:

#[cfg(test)]
mod test {
    #[test]
    pub fn test_parse_paragraph() {
        let paragraph = "saya cakap English".into();
        let phonotactic = Phonotactic::new();
        let parsed: Vec<IResult<&'a str, Phrase<&'a str>>> = phonotactics.parse_paragraph(&paragraph);
       // The IResult and Phrase shown here is just a simplified representation!
        assert_eq!(
            parsed, 
            vec![
               IResult<"", Phrase { vec! ["sa", "ya"] }>,
               IResult<"", Phrase { vec! ["ca", "kap"] }>,
              // "English" is not parsed fully because it does not follow phonotactic rules.
               IResult<"h", Phrase { vec! ["Eng", "lis"] }>,
            ]
        )
    }
}

Bagaimana hasil akhirnya bergantung pada kesesuaian tapi ini yang saya bayangkan.

@Thaza-Kun Thaza-Kun added enhancement New feature or request good first issue Good for newcomers labels Apr 4, 2024
@irfanzainudin
Copy link

Saya baru sempat tengok isu ni dan saya cuba bayangkan solusi.

Solusi yang naif mungkin adalah untuk panggil fn parse_syllables<'a>(&'a self, input: &'a String) -> IResult<&'a str, Phrase<&'a str>> beberapa kali dalam "loop" dan simpan hasilnya dalam Vec<IResult<...>> kemudian return.

Boleh tuan jelaskan sama ada ni solusi yang boleh diterima atau fn parse_paragraph ialah fungsi yang lebih kompleks?

Saya rasa fungsi parse_syllables tidak baca tanda baca kan? Jadi mungkin itu akan jadi satu masalah.

@Thaza-Kun
Copy link
Owner Author

Iya, parse_syllables tak baca tanda baca. Jadi, barangkali dalam parse_paragraph, ada parser untuk tanda baca.

Secara naifnya, kita boleh pecahkan perenggan pakai <perenggan as String>.split(".") dan pecahkan lagi <ayat as String>.split_whitespace() jadi begini:

let mut tokens: Vec<String> = Vec::new()
let perenggan: String = "Nama saya ikan. Saya tinggal dalam air.";
for ayat in perenggan.split(".") {
    for kata in ayat.split_whitespace() {
        tokens.push(kata);
    }
}
assert_eq!(
    tokens, vec!["Nama", "saya", "ikan", "Saya", "tinggal", "dalam", "air"]
)

tapi itu akan abaikan tanda baca lain seperti "?", ",", dll.

Menurut, https://rustjobs.dev/blog/how-to-split-strings-in-rust/, String.split() boleh terima closure berbentuk |char| -> bool. Barangkali boleh buat begini menggunakan nom::combinator::recognize:

use nom::combinator::recognize;

let punctuation_parser: nom::Parser;
for ayat in perenggan.split(|c: char| recognize(punctuation_parser)(c).is_ok()) {
    // -- snip --
}

Kemudian, persoalan berikutnya ialah, adakah nak pulangkan balik tanda baca atau nak abaikan je? Kalau nak pulangkan, perlu perkenalkan enum baharu,

enum SentenceToken<'a> {
    Phrase(Phrase<&'a str>)
    Punctuation(Punctuation<&'a str>)
}

lalu hasil parse_paragraph ialah Vec<SentenceToken>. Jika ya, apa manfaatnya? Jika tidak, apa kesannya?

@irfanzainudin
Copy link

irfanzainudin commented Apr 23, 2024

Saya tersekat dengan beberapa isu tuan Thaza.

  • Boleh tuan terangkan tentang ParseResults punya komponen? Khususnya full dan partial, saya tak faham apa kegunaan mereka.
  • Patut ke saya adakan struct baru yang sama dengan ParseResults tapi untuk Vec?

Setakat ni, saya ada tambah beberapa fungsi dan struct baru:

// -- snip --

impl Phonotactic {
    // -- snip --

    pub fn parse_paragraph<'a>(
        &'a self,
        input: &'a String,
    ) -> Vec<IResult<&'a str, Phrase<&'a str>>> {
        let mut tokens: Vec<&str> = Vec::new();
        for ayat in input.split(".") {
            for kata in ayat.split_whitespace() {
                if kata.chars().any(|c| c.is_ascii_punctuation()) {
                    for token in kata.split("-") {
                        tokens.push(token);
                    }
                } else {
                    tokens.push(kata);
                }
            }
        }

        let def_as_str = self.definition.as_str();
        let mut parsed_tags: Vec<IResult<&'a str, Phrase<&'a str>>> = Vec::new();
        for unparsed_tag in tokens {
            let pt = def_as_str
                .clone()
                .parse_tags(unparsed_tag)
                .map(|(r, p)| (r, p.with_postprocessing(&self.definition)));
            parsed_tags.push(pt);
        }

        parsed_tags
    }
}

// -- snip --

struct VecInnerParseResult<'a> {
    vec: Vec<InnerParseResult<'a>>,
}

// -- snip --

impl<'a> VecInnerParseResult<'a> {
    pub fn render(&self, options: ParseResultOptions) -> ParseResults {
        let mut res = ParseResults::new(options);
        for iresult in &self.vec {
            if iresult.full {
                res.with_full(iresult.phrase.as_separated(&res.options.separator));
            } else {
                let mid_tail = if &iresult.rest.len() > &1 {
                    &iresult.rest[0..2]
                } else {
                    iresult.rest.as_str()
                };
                let tail_rest = if &iresult.rest.len() > &1 {
                    iresult.rest[2..iresult.rest.len()].to_string()
                } else {
                    "".into()
                };
                let head = iresult.phrase.as_contiguous();
                let mid = format!(
                    "{head}{tail}",
                    head = head.chars().last().unwrap_or(' '),
                    tail = mid_tail,
                );
                res.with_partial(
                    head[0..head.len().saturating_sub(1)].to_string(),
                    mid,
                    tail_rest,
                );
            }
        }
        res
    }
}

// -- snip --

impl<'a> From<Vec<IResult<&'a str, Phrase<&'a str>>>> for VecInnerParseResult<'a> {
    fn from(vec_iresult: Vec<IResult<&'a str, Phrase<&'a str>>>) -> Self {
        let mut vec_innerparseresult: Vec<InnerParseResult> = Vec::new();
        for value in vec_iresult {
            match value {
                Ok((rest, phrase)) => {
                    vec_innerparseresult.push(InnerParseResult {
                        full: rest.is_empty(),
                        rest: String::from(rest),
                        phrase: phrase,
                    });
                }
                Err(e) => {
                    alert(&format!("{}", e));
                    vec_innerparseresult.push(InnerParseResult {
                        full: false,
                        rest: "".into(),
                        phrase: Phrase { syllables: vec![] },
                    });
                }
            }
        }
        
        Self {
            vec: vec_innerparseresult,
        }
    }
}

#[wasm_bindgen]
impl Phonotactic {
    // -- snip --

    pub fn parse_perenggan(&mut self, input: String, options: ParseResultOptions) -> ParseResults {
        let text = input.to_lowercase();
        let s = self.parse_paragraph(&text);
        VecInnerParseResult::from(s).render(options)
    }

    // -- snip --
}

// -- snip --

Kod ni masih tak hasilkan ciri yang dikehendaki lagi, tapi saya tahu salahnya di dalam impl<'a> VecInnerParseResult<'a> tapi saya tak tahu bagaimana nak teruskan sebab saya tak faham ParseResults.

Saya boleh je adakan struct baru untuk ParseResults (mungkin VecParseResults, sama macam saya buat VecInnerParseResults) tapi saya tak pasti pendekatan ni sesuai atau tidak.

@Thaza-Kun
Copy link
Owner Author

Asalnya, struct ParseResult ni nak buat macam sejenis enum yang membezakan perkataan yang berjaya diparse semua dan yang tak berjaya diparse semua.

Cubaan Asal

Begini cubaan asal:

#[wasm_bindgen]
struct FullyParsed {...}

#[wasm_bindgen]
struct FullyParsed {...}

#[wasm_bindgen]
enum ParseResult {
    Full(FullyParsed),
    Partial(PartiallyParsed),
}

Masalahnya, wasm-bindgen hanya menyokong enum jenis-C (enum tidak menyimpan data), seperti yang dinyatakan dalam rustwasm/wasm-bindgen#2407:

Right now, only C-style enums are supported. A C-style enum like this

#[wasm_bindgen]
enum CStyleEnum { A, B = 42, C }

Jadi, cubaan guna enum seperti yang ditunjukkan di atas gagal, maka jadilah struct ParseResults seperti yang dilihat:

#[wasm_bindgen]
pub struct ParseResults {
    options: ParseResultOptions,
    // Success
    full: Option<String>,
    // Error
    partial: Option<(String, String, String)>,
}
#[wasm_bindgen]
impl ParseResults {
    #[wasm_bindgen(getter)]
    pub fn full(&self) -> Option<String> {
        self.full.clone()
    }

    #[wasm_bindgen(getter)]
    pub fn error(&self) -> bool {
        self.partial.is_some()
    }

    #[wasm_bindgen(getter)]
    pub fn head(&self) -> Option<String> {
        Some(self.partial.clone()?.0)
    }

    #[wasm_bindgen(getter)]
    pub fn mid(&self) -> Option<String> {
        Some(self.partial.clone()?.1)
    }

    #[wasm_bindgen(getter)]
    pub fn tail(&self) -> Option<String> {
        Some(self.partial.clone()?.2)
    }
}

Kalau perasan, di bahagian JS, penggunaannya lebih kurang begini:

let phonotactic = new Phonotactic();
let result = phonotactic.parse_syllables("sahabat");
if ( !result.error()) {
    let display = result.full()
} else {
    let display = result.head() + "<u>" + result.mid() + "</u>" + result.tail() 
}

Rancangan

Aku ada rancangan nak perbetulkan penggunaan ini. Mungkin akan guna Result sebab Result disokong oleh wasm_bindgen. Cumanya, itu akan memerlukan kita guna try ... catch dekat bahagian JS. Mungkin Result lebih baik.

Untuk tujuan baca perenggan, rasanya boleh buat Vec<ParseResults>. Jadi, di bahagian JS, penggunaannya akan lebih kurang begini:

let phonotactic = new Phonotactic();
let results = phonotactic.parse_paragraph(lorem_ipsum);
let display: String[];
for result in results {
  if ( !result.error()) {
    display.push(result.full())
  } else {
    display.push(result.head() + "<u>" + result.mid() + "</u>" + result.tail())
  }
}

Cumanya, dia menjadi masalah sebab isi ParseResult.option akan berulang banyak kali sebab option tersebut patutnya terpakai untuk semua item dalam Vec tersebut. Sama ada nak biarkan je (dan selesaikan kemudian), atau nak terus selesaikan, terpulang.

@irfanzainudin
Copy link

irfanzainudin commented Apr 24, 2024

Oh faham, pendapat saya, kalau tuan bercadang untuk gunakan wasm_bindgen untuk jangka masa lama, mungkin sesuai untuk tukar menggunakan Result.

Saya cuba pakai Result. Dengan ni, tuan boleh nilai sama ada pendekatan mana lagi sesuai; struct sendiri atau Result.

Tapi maksud tuan std::fmt::Result atau std::result::Result?

PS. Saya lupa nak cakap yang solusi yang saya kongsikan sekarang ni sangat asas (banyak ambil daripada kod tuan juga), sebab saya nak cuba buat "proof-of-concept" dulu (if that makes sense 😅). Saya cuba buat berperingkat (incrementally).

@Thaza-Kun
Copy link
Owner Author

Pakai std::result::Result<T,E> (Result paling asas) sebab kita boleh letak struct kita sendiri dalam E manakala std::fmt::Result dah disesuaikan untuk formatting (contohnya untuk Display dan Debug).

Untuk wasm_bindgen tu, aku kekalkan untuk kekalkan laman web. Untuk tujuan jangka masa panjang, kod teras (tanpa wasm_bindgen) sedang dipisahkan masuk crate sendiri dalam repo ini juga (rujuk #4).

P/S - Jangan risau, projek ini pun asalnya untuk POC juga dan memang dijangka akan ada penyelesaian berperingkat (contohnya struct ParseResult tadi sebagai penyelesaian sementara)

@Thaza-Kun
Copy link
Owner Author

Thaza-Kun commented Apr 24, 2024

Aku dah merge #4.

Aku ada senaraikan ringkasan perubahan di sini. Yang penting untuk isu ini mungkin nombor 1 (pindahkan logik ke onc::phonotactic::PhonotacticRule), dan 6.a (hubungan antara InnerParseResult dengan ParseResult).

Boleh juga lihat bahagian example untuk tahu macam mana frontend akan berinteraksi dengan backend.

Harap ia membantu.

@irfanzainudin
Copy link

Terima kasih tuan!

Saya sibuk beberapa minggu ni, jadi tak banyak berita baru sangat. Kalau ada apa-apa, saya bagitau kat sini.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request good first issue Good for newcomers
Projects
None yet
Development

No branches or pull requests

2 participants