veer66

veer66

(Posted on 2022-01-28)

I agree that using JavaScript increases the chance of participation. I released a few versions of Thai word breakers in different programming languages. One on node.js is the most popular. 8 people contributed to the JS-based project compared to 2-3 people in other programming languages. However, JS has a downside too. In 2017, @iporsut and I made an experiment to compare Thai word breakers that we created. JS version running time is 15X of the Rust version. Even by comparing with another dynamic language, the Julia version is faster than the one in JS.

I created a website using node.js in 2014, and it is still running. The performance is good. However, I have a few regrets.
* We had a very hard time by install this project on other team members who use Windows 10 because we didn't know how to build a Bcrypt library. * Recently, I have to fix the project without adding any new feature because Express.js was changed, MongoDB was changed, and some packages that I used were abandoned. * It was a small project so I wanted to keep the session storage in RAM, but I can't since I ran 4 node.js processes. Now the project requires Redis as session storage, which causes more troubles for team members, who don't familiar with GNU/Linux, Docker, or WSL.

โม้ว่าทำอะไรเกี่ยวกับ Rust บ้าง 2021

(Posted on 2021-12-01)

ทีแรกผมจะเขียนเตรียมไว้พูดในงาน Rustacean Bangkok 2.0.0 แต่ว่าวันงานไม่ว่าง ก็เลยเอามารวมเป็น blog ไว้ก่อนแล้วกัน

ผมจะเล่าว่าผมทำอะไรกับภาษา Rust บ้างซึ่งก็คงไม่ครบถ้วน จะเขียนไปตามที่นึกได้

2014

ผมเริ่มเขียน Rust ครั้งแรกค.ศ. 2014 อย่างน้อยก็ที่ใส่ gist ไว้ code

#[deriving(Encodable, Decodable, Show, Clone)]
pub struct Node {
    pub snode: ~[~[Range]],
    pub children: Option<~[Option<Node>]>,
}

เป็นช่วงก่อนที่จะออก Rust 1.0 หน้าตาก็ต่างจากเดี๋ยวนี้เยอะเหมือนกัน ส่วนมากเอามาทำงานที่เอาไว้เก็บ string-tree alignment แต่รายละเอียดเรื่องนี้ก็ข้ามไปดีกว่า

2015

ค.ศ. 2015 ผมนึกถึงโปรแกรมพื้นฐานที่ใช้บ่อย ๆ โปรแกรมตัดคำ ถ้าอยากให้ความถูกต้องสูงใช้ deepcut เลย ถ้าอยากให้ความถูกต้องสูงแต่เร็วขึ้นหน่อยใช้ attacut

แต่ถ้าเอาเร็ว libthai chamkho icu

chamkho ผมแบ่งขั้นตอนการตัดคำเป็น 3 ขั้น

  1. สร้าง directed acyclic graph (DAG) ของวิธีตัดคำที่เป็นไปได้ทั้งหมด
  2. หา shortest path บน DAG จากข้อ 1
  3. ถอด path จากข้อสองมาเป็นตำแหน่งของ string ที่ต้องตัด

ส่วนที่ปรับไปมาคือข้อ 1 ผมแยกแบบนี้

1.1 สร้าง edge จากคำในพจนานุกรมที่ตรงกับ sub-string 1.2 สร้าง edge 1.3 ตัด edge ที่ขัดกับกด cluster ซึ่งเป็น substring ที่ตัดไม่ได้ออก ข้อนี้เพิ่มมาในค.ศ. 2021

มีแค่นี้เลย เล่าย้อนกลับไปหน่อยว่าปลายค.ศ. 2002 ผมเริ่มทำโปรแกรมตัดคำออกมาเขียนด้วย C

เพราะผมลองใช้ NLTK ที่เขียนด้วย Python ทั้งตัวแล้วมันทำงานช้าเกิน

แต่ผมก็คิดว่าแอปก็ยังควรจะภาษาที่เขียนง่าย ๆ แบบ Python หรือ Ruby หรือ Lisp อยู่ดี

แต่ข้อจำกัดของตัวคำสมัยนั้นคือ

  1. หลายตัวเขียนด้วย C++ ผมเขียนไม่เป็นหลังจากเรียนอยู่นาน

  2. ไม่ค่อยเหมาะเอามา bind กับ Ruby และอื่น ๆ

  3. แก้ word list และกฎยาก

thaiwordseg ก็เลยใส่ word list ใน file กฎใช้ regex และ bind กับ Ruby มาให้เลย ดูโครงการได้ที่ sourceforge ยังมี package ของโครงการนี้อยู่บน SUSE Linux ด้วยครับ

ปลายค.ศ. 2015 ผมไปพูดเรื่อง Rust ใน Barcamp Bangkhen รอบนึง พูดประมาณในเอกสารนี้

2016

ค.ศ. 2016 พูดเรื่อง Rust ในกลุ่ม Mozilla ไทย มีหลงเหลือ slide อยู่บ้าง

ค.ศ. 2016 ตั้งกลุ่ม Facebook สำหรับชาว Rust ไทยขึ้นมา ถ้าจำไม่ผิดตั้งตามกลุ่ม Clojure ไทยเพราะเห็นว่ามีกลุ่มขึ้นมาก็ดี ทุกวันนี้กลุ่ม RustLangTH ก็ได้คุณ Nui Narongwet และคุณ Wasawat Somno ช่วยกันดูแลเป็นหลักเลย กราบขอบพระคุณครับ

2017

ค.ศ. 2017 ร่วมกับคุณ @iporsut ทดสอบ Rust ที่เอามาตัดคำโดยใช้ word list และยังไม่ได้ใช้ regex สรุปได้เลยว่าเอา Rust เร็วกว่า Go Java คือตัวที่เขียนด้วย Rust ใช้เวลาทำงานเพียง 60% ของ Go ยังไม่ต้องพูดถึงตัวอื่นที่ว่าช้ากว่านั้น

ส่วนพวก Python JavaScript Clojure คือช้ามาก ห่างกันเป็นเท่าตัว ดูรายละเอียดเพิ่มเติม

ค.ศ. 2017 พบปะชาว Rust ไทยไปอีกรอบนึง ตาม blog นี้

ค.ศ. 2017 ตั้งกลุ่ม Rust ไทยบน Telegram ทั้งกลุ่มใน Telegram ทั้ง Facebook ผมส่งมอบสิทธิ admin ไปหมดแล้ว สมมุติว่าตอนนี้ผมตายไปก็ไม่ผลกระทบแน่นอน

ค.ศ.2017 ผมแก้ bug บน servo พอมีบันทึกไว้ใน blog ของ servo ก็พบว่าเป็น bug ที่ผิดง่าย ๆ ใส่ตัวแปรสลับกัน

Rust จะประกาศ type ให้ต่างกันจน compiler ตรวจได้ว่าคนใส่สลับกันได้

แต่ก็แบบที่ให้เวลาใช้งานจริง ๆ ก็ไม่ได้ประกาศ type แบบที่ว่า i32 u32 เต็มไปหมด ใส่สลับกัน compiler ก็ตรวจไม่ได้อยู่ดี

ระหว่างผมเขียน Python กับ Rust รู้สึกว่าเขียน Rust ก็เกิด bug เยอะแยะอยู่ดี อาจจะว่าผมโง่ก็ได้ แต่มันเป็นตัวแปรควบคุมว่าผมก็โง่เหมือนเดิมเวลาเขียน Python หรือ Rust ก็ตาม 😛

2018

ค.ศ. 2018 ส่วนมากก็จะปรับปรุงโปรแกรมเก่า เริ่มเขียน HTTP server มาหุ้มตัวตัดคำบ้าง โดยใช้ Hyper ตอนนี้น่าจะรันไม่ได้แล้ว หน้าตาแบบข้างล่าง

impl Service for WordcutServer {
    type Request = Request;
    type Response = Response;
    type Error = hyper::Error;
    type Future = WebFuture;

    
    fn call(&self, req: Request) -> Self::Future {
        match (req.method(), req.path()) {
            (&Post, "/wordseg") => wordseg_handler(req),
            (&Post, "/dag") => dag_handler(req),
            _ => not_found(req)
        }
    }
}

2019

ค.ศ.2019 ส่วนมากจะเป็น code พวก string-tree เหมือนห้าปีก่อน แต่ style ก็เปลี่ยนไปเยอะ หน้าตาประมาณข้างล่าง

quick_error! {
    #[derive(Debug)]
    pub enum TextunitLoadingError {
        CannotLoadToks(lang: LangKey, err: Box<Error>) { }
        CannotLoadLines(lang: LangKey, err: Box<Error>) { }
        CannotLoadLinks(err: Box<Error>) { }
        CannotAlignToks(lang: LangKey, line_no: usize, err: Box<Error>) { }
    }
}

#[derive(Debug)]
pub struct Textunit {
    pub bi_text: BiText,
    pub bi_rtoks: BiRToks,
    pub links: Vec<Link>,
}

2020

ค.ศ. 2020 ผม port extension สำหรับ full-text search ภาษาไทยบน PostgreSQL ของคุณ zdk (Di Warachet S.) มาเป็น Rust

ผมไม่ทราบว่าคุณ zdk คิดอย่างไร แต่ผมมองว่าการลงพวก ElasticSearch เพื่อที่จะใช้ full text search พื้น ๆ หรือแม้แต่เอามา query JSON document บนเว็บที่คนใช้พร้อมกันไม่ถึง 100% คน มันเพิ่มงาน กิน RAM กิน SSD ขึ้นอีกเยอะ ใช้ PostgreSQL แทนได้น่าจะดีกว่า ตัวที่เขียนด้วย Rust ผมตั้งชื่อให้ว่าชำฆ้อพีจี

ค.ศ. 2020 ผมเขียน JSON parser สำหรับ GNU Guile ชื่อ guile-json-parse ด้วย

2021

ค.ศ. 2021 ผมแก้ให้โปรแกรมตัดคำมี regular expression แบบ newmm และ nlpo3 แต่พบว่าทุกอย่างที่เคย optimize มาสูญสลายไปเกือบหมดเลย เพราะว่า regex แบบ fancy ใน Rust มันก็ไม่ได้เร็วกว่า regex engine ของ Python ที่เขียนด้วย C 😰

ถ้าจะคงความเร็วไว้อาจจะต้องเขียนกฎให้เป็นภาษา Rust เลยแบบที่ chamkho เคยทำมา หรือทำตาม libthai ที่มีกฎคล้าย ๆ newmm แต่เขียนด้วย C วิธีนี้เร็วแน่ แต่ว่าแก้ทีเหนื่อย จะเพิ่มภาษาใหม่หนักแรง

แต่ Rust มีทางออกที่สามคือใช้ regex-automata ของ Andrew Gallant ซึ่งตัวนี้ตามชื่อแปลง regex เป็น automata แบบที่ควรจะเป็น แต่ก็จะรับ regex แบบขยายของ newmm ไม่ได้ก็เลยต้องโม regex บ้าง

ทำให้เสร็จแล้ว newmm รันบน xeon ใช้เวลารัน 9.65 เท่าของ chamkho ผมว่าควรจะเขียนถึงเรื่อง profiling ด้วยแค่จะแยกไปอีกโพสต์

อีกภาษาที่หลัง ๆ ทำงานเร็วขึ้นมาก และสำหรับผมเขียนง่ายกว่า Rust เยอะมาก ๆ คือ Julia แต่ว่าก็ติดอยู่ 3 เรื่องคือ

  1. ไม่มี regex-automata บน Julia
  2. runtime ของ Julia เปิดช้า
  3. ทำ lib ลอกเลียนภาษา C ลำบาก ทำให้เอาไปใช้กับ cffi ใน Common Lisp หรือ Ruby ยาก

Common Lisp ก็เป็นอีกภาษาหนึ่งที่ optimize มาก ๆ แล้วก็เร็วสูสี Julia แต่ก็ติดประเด็นเดียวกันทั้งหมด

ทำให้ผมสรุปแบบนี้เลยว่า Rust นอกจากภาษาและ compiler แล้วยังเป็น community ที่ lib ดี ๆ ออกมาเยอะมาก หลายตัวหาในภาษาอื่นยาก

ค.ศ. 2021 อีกอย่างที่คือ เพื่อที่จะไป bind กับภาษาอื่นง่าย ๆ chamkho มี wrapper ทำให้กลายเป็น library ภาษา C แบบปลอม ๆ ชื่อ wordcutw

พอทำแบบนี้เวลาจะ bind กับ Ruby หรือภาษาอื่น ๆ ก็ไม่ต้องคิดอะไรใหม่เลยใช้ FFI ได้เลย ข้อดีของการใช้ FFI หรือ cffi คือมันจะไม่ยึดติดกับ implementation ตัวใดตัวหนึ่ง เช่น Common Lisp อาจจะใช้ implementation เป็น SBCL หรือ Clozure CL ก็ได้ แต่ถ้าใช้ cffi มา bind กับ wordcutw ก็จะใช้ได้ทั้ง SBCL และ Clozure CL โดยไม่ต้องแก้ code เลย

ค.ศ. 2021 ผมคิดว่าน่าจะไป chat กันแบบ protocol ที่เปิดและ decentralize บ้างก็เลยเปิด กลุ่ม Rust ไทยบน Matrix

ค.ศ. 2021 อีกอย่างที่ทำคือเทียบความเร็ว Apache Arrow กับ Pandas ผลคือถ้าเขียน Pandas ดี ๆ ก็เร็วได้ แต่ใช้ Arrow เร็วกว่า ยิ่งใช้ Rust ก็เร็วใหญ่ และไม่ต้องระวังเรื่องแปลงไปมาระหว่าง type ของ Rust และ Python ดูรายละเอียดเพิ่มเติม

ค.ศ. 2021 ก็มีเรื่องอื่น ๆ คือเขียน parser สำหรับ Apertium stream format โดยใช้ Nom ทำให้เขียน parser ง่ายขึ้นมาก; port โปรแกรม Attacut มาบน Rust ให้ชื่อว่า Khatson ปรากฎว่าไม่เร็วขึ้นเลยแต่ทำให้รู้ว่าใช้ tch ใน Rust ที่หุ้ม PyTorch ไว้ให้ เขียนพวก deep learning แบบเดี๋ยวกับใช้ PyTorch สะดวกมาก และสำหรับผมงงน้อยกว่าใช้ PyTorch ตรง ๆ

สถานการณ์ของ Rust ในไทย

สภานการณ์ปัจจุบัน ตอนนี้ผมคิดว่า Rust ติดลมบนไปแล้ว บริษัทขวัญใจมหาชนก็เป็นสมาชิก Rust Foundation ทั้ง AWS Facebook Google Huawei Microsoft Mozilla; celeb ก็เขียน Rust กันแล้ว สถานการณ์ในไทยก็ฝืนกระแสโลกไม่ไหวแน่ ๆ

ส่วนที่เป็นรูปธรรมมากคือคุณ Jojo จากช่อง KubeOps Skills ทำวิดีโอภาษาไทยขึ้นมาสอนเขียน Rust เลย ผมว่าอันนี้ดีมาก ๆ และตามที่เกริ่นไปคุณ Natechewin Suthison จะจัดงาน Rustacean Bangkok 2.0.0 วันที่ 13 ธันวาคม พ.ศ.2564 นี ทุกท่านกดดูรายละเอียดตาม link ไป Facebook ได้เลย

(Posted on 2021-11-15)

Install pre-requisited tools

sudo zypper install make gcc automake autoconf libtool aclocal wget
sudo zypper si -d emacs

Download Emacs source tarball

wget https://ftp.gnu.org/pub/gnu/emacs/emacs-27.2.tar.gz

Extract and change directory

tar xzvf emacs-27.2.tar.gz
cd emacs-27.2

Generate makefiles

./configure --prefix=/opt > log

Make

make -j `nproc`

Install

sudo make install

Add path

Add the line below to ~/.bashrc by nano ~/.bashrc

export PATH="/opt/bin:$PATH"

An incomplete WebSocket client based on only socket, ssl, and uuid in Python

A few days ago, I couldn't get a WebSocket library working with another library on Python 3.10. So to avoid those dependencies, I implemented my WebSocket client on a low-level socket API. I implemented one in Common Lisp first. Then I translated it to Python. My WebSocket client is very far away from being completed, but at least it can run.

The first part is import. I imported only three things that are socket, ssl, and uuid.

import socket
import ssl
import uuid

The second part is based on HTTP headers for telling the server to switch to the WebSocket mode.

def upgrade(s, conn_info):
    with s.makefile(mode = 'rw', encoding = "ISO-8859-1") as f:
        f.write(f'GET {conn_info["path"]} HTTP/1.1\r\n')
        f.write(f'Host: {conn_info["host"]}\r\n')
        f.write("Connection: Upgrade\r\n")
        f.write("Upgrade: websocket\r\n")
        f.write(f'Sec-Websocket-Key: {str(uuid.uuid1())}\r\n')
        f.write("Sec-WebSocket-Version: 13\r\n")
        f.write("\r\n")
        f.flush()
        
        # reading response
        for line in f:
            if line == "\n":
                break

The third part is reading a content length. I assume that the server keep sending text contents. In read_payload_len(), s.recv(1) read a byte from the socket s. & 0x7F is for masking only 7 bits. If length is 127, the length is in the next 4 bytes instead. If length is 126, the length is in the next 2 bytes. Otherwise the function just returns the length.

def read_payload_len(s):
    match s.recv(1)[0] & 0x7F:
        case 127:
            return read_extra_len(s, 4)
        case 126:
            return read_extra_len(s, 2)
        case l:
            return l

The read_extra_len reads bytes and turn them to integer.

def read_extra_len(s, num_of_bytes):
    buf = s.recv(num_of_bytes)
    len = 0
    for i in range(num_of_bytes):
        len += buf[i] << (8 * (num_of_bytes - i - 1))
    return len

In the fourth part, we read a web socket frame. The program determine a frame type from opcode, which in the last 4 bits of the header. I should implement PING-PONG part but I didn't. According to RFC6455, which I forgot to mention before, opcode == 0x1 means the frame is a text frame. So the program reads payload length and reads the payload.

def read_frame(s):
    header0 = s.recv(1)[0]
    opcode = header0 & 0x0F
    match opcode:
        case 0x1:
            payload_len = read_payload_len(s)
            print(s.recv(payload_len))
        case 0x9:
            print("PING")
        case 0xA:
            print("PONG")

The last part is for opening connections and SSL/TLS wrapper. The function created a socket and wrapped it with TLS/SSL wrapper. The sending upgrade message to ask the server to switch to Websocket mode and the keep reading frames.

def connect(conn_info):
    ctx = ssl.create_default_context()
    with socket.create_connection((conn_info["host"], conn_info["port"])) as s:
        with ctx.wrap_socket(s, server_hostname = conn_info["host"]) as ss:
            upgrade(ss, conn_info)
            while True:
                read_frame(ss)

In the final part, I cannot find any public Websocket endpoint besides ones from cryptocurrency exchanges. So I put Bitkub API.

connect({"host": "api.bitkub.com",
         "port": 443,
         "path": "/websocket-api/market.trade.thb_btc"})

And it works, but if you are unlucky, you will get PING instead.

> python3.10 http_ex.py 
b'{"amt":0.00004661,"bid":86911896,"rat":2140000,"sid":82187323,"stream":"market.trade.thb_btc","sym":"THB_BTC","ts":1636729677,"txn":"BTCSELL0011078574"}\n{"amt":0.23306074,"bid":86912246,"rat":2140000,"sid":82187325,"stream":"market.trade.thb_btc","sym":"THB_BTC","ts":1636729677,"txn":"BTCSELL0011078576"}'
b'{"amt":0.00466121,"bid":86912014,"rat":2140000,"sid":82187324,"stream":"market.trade.thb_btc","sym":"THB_BTC","ts":1636729677,"txn":"BTCSELL0011078575"}'

Download and unarchive

Download Python-3.10.0.tar.xz; tar xJvf Python-3.10.0.tar.xz; cd Python-3.10.0

Configure

I got this error message by running ./configure

./configure: line 10530: PKG_PROG_PKG_CONFIG: command not found

So I ran this:

docker run --rm --pull=always -v $(pwd):/src quay.io/tiran/cpython_autoconf:latest

Still configure cannot find g++, so I added CXX=c++ in front of configure.

So this was how I used configure:

CXX=c++ ./configure --enable-optimizations --prefix=/opt 2> elog > log

make

This step has no problem.

make -j8
sudo make install

Running python

pip3.10 and python3.10 cannot run since it can't find readline.

So I ran this.

ln -s /opt/lib64/python3.10/lib-dynload /opt/lib/python3.10/lib-dynload

Then python3.10 works now.

P.S. Thank every one who provide answers on issue trackers and forums. I should have kept URLs to those. Sorry that I didn't.

Actix has a supervisor that helps to restart an actor under its supervision. However, sometimes it doesn't. 😹

AFAIK something has to send a Die (message) to an actor when something went wrong. The actor has to implement a Die message handler. In the handler, it calls ctx.stop (context stop). When “Actor::stopping(...) == Running::Stop”, its poll function will return Poll::Ready. Then its supervisor can perform restarting.

In my case, all of these didn't work because of two reasons.

  1. Actor didn't stop properly. It stopped without calling the stopped function. So I suppose it has no chance to handle the Die message.

  2. Actor kept using the CPU when it went wrong. So the poll function was blocked. Then the actor cannot handle the Die message.

I fixed the first case by calling tokio::time::sleep, and the second case by breaking for the loop.

PS I'm not sure about what I wrote. Please tell me if I missed or misunderstood some things or even everything.

New Rustacians usually ask why Rust has many types of strings. I don't know the original intention. Anyways, it has some advantages as follow:

  1. It minimizes the language core. Rust core has only str. String, OsString, CString are in the standard library, which is not a part of the language core.

  2. Programmer can control performance. For example, &str refers to a byte slice so it should be fast, while String is a wrapper of Vec. String should have more performance penalty from heap memory allocation.

  3. OsString and CString improve interoperability with other programming languages and operating systems. Rust can manipulate C-style strings directly without converting them to native Rust strings. OsString is also similar.

String conversation has a performance penalty. Also sometimes string conversation is not obvious. Some systems don't even use Unicode or other standards.

The alternative of having too many types is treating everything as a byte slice. However, type checking doesn't work well with too few types.

I enjoy using key-value data in dynamic languages. For example, in Python, I can create key-value data for storing the metadata of a document as shown below. I don't discuss why I don't use struct, class, named tuple in this post.

doc_metadata = {"title": "The Rust Programming Language",  
                             "type": "book", 
                             "number-of-pages": 584, 
                             "authors": ["Steve Klabnik", 
                                                "Carol Nichols", 
                                                "contributions"]}

I can code read/write a value easily, for example:

# Write
doc_metadata["type"] = "text book"

# Read
print(doc_metadata["type"])

In Perl and Ruby, we can use Hash, which is almost the same thing as Dict in Python. In JavaScript, we can use an object.

Common Lisp is different. We can use a hash table, but it is not as convenient as Dict in Python.

(let ((doc-metadata (make-hash-table)))
  (setf (gethash :title doc-metadata) "The Rust Programming Language")
  (setf (gethash :type doc-metadata) :BOOK)
  (setf (gethash :number-of-pages doc-metadata) 584)
  (setf (gethash :authors doc-metadata) '("Steve Klabnik"
                                                                      "Carol Nichols" 
                                                                      "contributions")))

Besides construction, printing a hash table is not so convenient. Maybe one can create a function or macro to make creating/printing a hash table convenient. I still felt that I abused Common Lisp.

My code is usually too buggy when I keep mutating the same variable. So I prefer using an immutable data structure to prevent me from messing things up. Moreover, my key-value data usually do not have more than five keys. So I don't strictly need to use an efficient data structure, namely, hash table or binary search tree. So I use alist (assosiation list). I can construct a list like below:

(setq doc-metadata '((:title . "The Rust Programming Language")
                     (:type . :BOOK)
                     (:number-of-pages . 542) 
                     (:authors . '("Steve Klabnik"
                                   "Carol Nichols" 
                                   "contributions"))))

IMO, it looks concise and convenient. We can retrieve key-value pair with a specific key using the assoc function, which I suppose it does linear search. Linear search can be slow. However, my alist doesn't have a lot of keys.

Instead of replacing a value with another value, I can add a new key-value pair with an existing key, for example:

(setq another-doc-metadata (acons :type :TEXT-BOOK doc-metadata))

By retrieving the value of :type using assoc, we get the new value because assoc function retrieves the first key found in alist, for example:

(cdr (assoc :type another-doc-metadata))
;; OUTPUT => :TEXT-BOOK

However, with function calls instead of number/string literal, alist doesn't look concise anymore, for example:

(list (cons :title (get-title x y z))
       (cons :type (get-type x))
       (cons :number-of-pages (get-number-of-pages a b c)) 
       (cons :authors (get-authors c d)))

plist looks much more concise, for example:

(setq doc-metadata (list :title (get-title x y z)
       :type (get-type x)
       :number-of-pages (get-number-of-pages a b c) 
       :authors (get-authors c d)))

I can retrieve a value corresponding to a key easily by getf function. For example:

(getf doc-metadata :type)

A new value can be replaced the old value by setf, example:

(setf (getf doc-mentadata :type) :TEXT-BOOK)

setf is different from acons since acons doesn't mutate the existing list, setf does. Therefore plist is not exactly what I'm looking for.

Maybe the best way is using an Alexandria function for converting plist ot alist as Michał “phoe” Herda suggested.

I wanted to create a parser for Apertium Stream. In 2014, I used Whittle in Ruby. If this year were 2001, I would use Lex/Yacc. Anyway, this year is 2021. I wanted to create this parser in Rust. I tried to find what is similar to Lex/Yacc. I found Rust-Peg. I found a link to Nom from Rust-Peg's document. My first impression was Nom example is easy to read. At least, its document claimed Nom is fast.

Apertium Stream format is quite complex, and I didn't know exactly how to use Nom. So I started from an easy case. My simplified Apertium stream is a list of lexical units. A lexical unit looks like this:

^surface_form$

Btw, I didn't test my source code on this post. If you want a runnable example, please check https://github.com/veer66/reinars.

I created a function to match a lexical unit first. It looks like this:

fn parse_lexical_unit(input: &str) -> IResult<&str, &str> {
    let mut parse = delimited(tag("^"), is_not("^$"), tag("$"));
    parse(input)
}

By running parselexicalunit(“^cat$”), it returns Ok((“”, “cat”)).

I hopefully improve by returning a Lexical Unit struct instead of &str.

#[derive(Debug)]
struct LexicalUnit {
    surface_form: String
}

fn parse_lexical_unit(input: &str) -> IResult<&str, LexicalUnit> {
    let mut parse = delimited(tag("^"), is_not("^$"), tag("$"));
    parse(input).map(|(i,o)| (i, LexicalUnit { surface_form: String::from(o) }))
}

“delimited” helps me to match ^ at the beginning and $ at the end. I wanted to capture whatever, which is not ^ or $. So I use is_not(“^$”). Can it be more straightforward?

When I ran parselexicalunit(“^cat$”), I get Ok((“”, LexicalUnit { surface_form: “cat” })) instead. 😃

Then I created a function for parsing the simplified stream.

fn parse_stream(input: &str) -> IResult<&str, Vec<LexicalUnit>> {
    let mut parse = separated_list0(space1, parse_lexical_unit);
    parse(input)
}

In the parsestream function, I use parselexicalunit, which I created before, in separatedlist0. separatedlist0 is for capturing the list, which in this case, the list is the list of lexical units parsed by parselexical_unit; and space1, which is one or more spaces, separate the list.

By running parse_stream(“^I$ ^eat$ ^rice$”), I get:

Ok(("", [LexicalUnit { surface_form: "I" }, 
             LexicalUnit { surface_form: "eat" }, 
             LexicalUnit { surface_form: "rice" }]))

I think this is enough for showing examples. The rest of the parser is the combination of alt, escaped_transform tuple, etc. By doing all these, I feel that this is easier than using Lex/Yacc or even Whittle at least for this task.

ความเห็นเกี่ยวกับ Haskell

ผมเพิ่งเคยลองเขียน Haskell นิด ๆ หน่อย ๆ เรียกว่ายังรู้เรื่องก็ได้แต่อยากบันทึกความคิดของตัวเองไว้ จะผิดถูกอย่างไรก็เรียนเชิญอภิปรายได้เลยครับ

ผมมองว่าคุณของ Haskell มันทำให้ programmer เห็นได้ชัดว่า function ไหนไปยุ่งกับ IO หรือ database และ function ไหนไม่ยุ่ง

ถ้าเขียน Python ดูเผิน ๆ ไม่ไล่ดูเนื้อ function ทั้งสองแบบมันก็หน้าตาเหมือนกัน

พอเห็นได้ว่ามันต่างกัน ก็อาจจะคิดขึ้นมาได้ว่าควรเขียน function ที่อิสระจาก IO จาก database ให้มาก เพราะพวกนี้เขียนง่าย test ง่ายทั้ง auto ทั้ง manual ทำ hot reload ก็ง่าย

แต่คนที่สำเหนียกไม่ได้ก็มี เช่น (ผมเอง) ทำไปทำมามี monad กระจายไปทุก function