Open Thaza-Kun opened 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.
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?
Saya tersekat dengan beberapa isu tuan Thaza.
ParseResults
punya komponen? Khususnya full
dan partial
, saya tak faham apa kegunaan mereka.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.
Asalnya, struct ParseResult
ni nak buat macam sejenis enum yang membezakan perkataan yang berjaya diparse semua dan yang tak berjaya diparse semua.
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()
}
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.
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).
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)
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.
Terima kasih tuan!
Saya sibuk beberapa minggu ni, jadi tak banyak berita baru sangat. Kalau ada apa-apa, saya bagitau kat sini.
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:
dengan contoh interaksi yang sebegini:
Bagaimana hasil akhirnya bergantung pada kesesuaian tapi ini yang saya bayangkan.