Thaza-Kun / bunyi-melayu

Projek kecil menerapkan tatabunyi melayu menggunakan nom
https://thaza-kun.github.io/bunyi-melayu/
2 stars 1 forks source link

Implemen baca ayat atau perenggan. #2

Open Thaza-Kun opened 5 months ago

Thaza-Kun commented 5 months ago

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.

irfanzainudin commented 5 months ago

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 commented 5 months ago

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 commented 4 months ago

Saya tersekat dengan beberapa isu tuan Thaza.

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 commented 4 months ago

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 https://github.com/rustwasm/wasm-bindgen/issues/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 commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

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 commented 4 months ago

Terima kasih tuan!

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