veer66

veer66

สมัยที่เรียนหนังสือเขามักจะเอา Fibonacci number มาเป็นตัวอย่างกัน function ก็เป็นงี้ครับ

fib(0) = 0 fib(1) = 1 fib(n) = fib(n-1) + fib(n-2) สำหรับ n มากกว่า 1

ซึ่งเอามาแปลงเป็น Clojure ได้แบบตรงไปตรงมาเลย

(def fib
  (fn [n]
    (if (<= n 1) n
        (+ (fib (- n 1)) (fib (- n 2))))))

ผมลองรันบนเครื่องผมใช้เวลา 4.324 วินาที ซึ่งมันช้าเพราะไปเรียก fib ซ้ำไปซ้ำมา ดูง่าย ๆ fib(4) ก็ไปเรียก fib(3), fib(2) แล้ว fib(3) ก็ไปเรียก fib(2) อีกซ้ำ พอเลขมากก็ซ้ำไปเรื่อย ๆ

จริง ๆ มันมีวิธีหล่อ ๆ เยอะแยะที่จะแก้ปัญหานี้ให้เร็วขึ้น แต่ถ้าเราไม่ต้องหล่อมาแก้แบบดอกเดียวเสร็จก็ใช้ memoize เขียนแบบนี้

(def fib
  (memoize
   (fn [n]
     (if (<= n 1) n
         (+ (fib (- n 1))
            (fib (- n 2)))))))
(println (time (fib 42)))

แก้นิดเดียวเอา memoize มาครอบ function ไว้ มันจะจำ function ที่เคยเรียกไว้แล้วส่งผลกลับมาเลยโดยไม่ต้องไล่ยาว ผลคือโปรแกรมใช้เวลารันเหลือ 0.820 วินาที

จริง ๆ มันจบแล้วล่ะ แต่เขียนให้ดูอีก function แบบใช้ loop

(defn fib [n]
  (loop [i 1 acc0 0 acc1 1]
    (if (< i n)
      (recur (inc i) acc1 (+ acc0 acc1))
      acc1)))      
(println (time (fib 42)))

อันนี้ใช้เวลาแค่ 0.0976 วินาที แต่ก็จะเห็นได้ว่าโปรแกรมเปลี่ยนไปเยอะ เปลี่ยนวิธีคิดเยอะ memoize นี่มันแทบไม่ต้องคิดต้องเขียนอะไรมากเลย ใส่ไปแล้วได้ผลเลย

ขอบคุณ visibletrap@twitter.com ที่แนะนำ function time มาครับ

ผมมักจะไปสร้าง script ด้วย Python สั้น ๆ ไว้ใน $HOME/bin หรือ /usr/local/bin ก็ได้ แล้วก็ chmod 755 <ชื่อไฟล์> แล้วก็จะได้เรียกจาก shell ได้เลย

ผมเขียนไว้ 3 ตัวตามนี้

/usr/local/bin/deepcut

#!/usr/bin/env python
# * mode: python-mode *

import deepcut
import json
import sys

for line in sys.stdin:
    line = line.strip()
    print(json.dumps(deepcut.tokenize(line)))

/usr/local/bin/newmm

#!/usr/bin/env python
# * mode: python-mode *

from pythainlp.tokenize import word_tokenize

import json
import sys

for line in sys.stdin:
    line = line.strip()
    print(json.dumps(word_tokenize(line ,engine='newmm')))

/usr/local/bin/wordcutpy

#!/usr/bin/env python
# * mode: python-mode *

from wordcut import Wordcut
import json
import sys

wordcut = Wordcut.bigthai()

for line in sys.stdin:
    line = line.strip()
    print(json.dumps(wordcut.tokenize(line)))

เรียกใช้งาน

ใน shell จะเรียกเดี่ยว ๆ ก็ได้แบบ

$ newmm < file1.txt > file1.ndjson

แต่ส่วนมากก็จะเรียกผ่าน for-loop เถื่อน ๆ แบบนี้

$ for x in *.txt; do newmm < $x > $x.ndjson; done

พอได้ไฟล์มาก็เอาไปใช้กับภาษาอื่นต่อได้

/usr/local/bin/wcnt

#!/usr/bin/env ruby
# * mode: ruby-mode *

require "json"
require "csv"

h = Hash.new(0)
while gets
  JSON.parse($_.chomp)
    .each{|w| h[w.chomp] += 1}
end
h.to_a.each{|row| puts CSV::generate_line(row)}

เวลาเรียกก็ไป pipe แบบนี้เลย

cat file1.txt | newmm | wcnt

เดี๋ยวนี้คนดี ๆ เขาคงใช้ Jupyter กันหมดแล้ว ผมก็พยายามฝึกอยู่เหมือนกัน แต่ยังไม่คล่อง บางทีก็เลยเอาแบบนี้ไปก่อน

I failed to download a list of PDFs by Wget, cURL, and Slimerjs. So I used Firefox and shell script.

First, I disable Pdfjs to force Firefox to download PDFs.

I wrote a shell script for opening a URL with PDF file, and then I use xdotool to close the Firefox window. For xdotool command, thank Jon G – Megaphone Tech for his comment at Unix Exchange.

#!/bin/sh

for url in `sort -u eng_pdf_urls.txt | grep -i pdf`
do
    echo $url
    firefox -P exp1 "$url" &
    sleep 10
    echo CLOSE
    xdotool search "Mozilla Firefox" windowactivate --sync key --window 0 --clearmodifiers alt+F4
    sleep 10
done

I didn't use headless because I don't know how to close it properly. I tried timeout command but Firefox detected it as an improper shutdown.

I ported my program for showing concurrency from Erlang to Elixir. I guess this is still not Idiomatic Elixir. 😅

defmodule Con1 do
  def log(p, i) when rem(i, 3000) == 0 do
    IO.puts "#{p} #{i}"
  end
  def log(_, _) do
    :ok
  end

  def cnt(_, 0) do
    :ok
  end
  def cnt(p, i) do
    log(p, i)
    cnt(p, i - 1)
  end

  def par_cnt(0) do
    :ok
  end
  def par_cnt(n) do
    spawn fn() -> cnt(n, 3000000) end
    par_cnt(n - 1)
  end
end

Crystal intentionally looks similar to Ruby. However, Crystal is a static typing programming language. A Thai word segmentation program written in Crystal, which is a CPU bound program, was found almost as fast as one written in Go.

I wondered whether its concurrency support was like what I expected. So I did this experiment. By spawning, Crystal did not create any new thread because it doesn't support multithreading parallelism yet. To support concurrency, Crystal has to be able to switch fibers when a fiber performs non-blocking IO operation.

require "socket"

def load(id, chan)
  puts "ID=#{id}; START"
  (id..11).each do 
    socket = TCPSocket.new("www.ku.ac.th", 80)
    socket.close
  end
  puts "ID=#{id}; FINISH"
  chan.send nil
end

def main
  chan = Channel(Nil).new
  (1..10).each{|i| spawn(load(i,chan))}
  # Wait
  (1..10).each{chan.receive}
end

main 

In this program, a task with the id with a smaller number creates a TCP socket repeatedly more times than a task with a larger number id. For example task#1 establishes a TCP socket for 11 times and task#10 create a TCP socket for one time only. So even task#1 started long before task#10, task#10 should finish before task#1.

ID=1; START
ID=2; START
ID=3; START
ID=4; START
ID=5; START
ID=6; START
ID=7; START
ID=8; START
ID=9; START
ID=10; START
ID=10; FINISH
ID=9; FINISH
ID=8; FINISH
ID=7; FINISH
ID=6; FINISH
ID=5; FINISH
ID=4; FINISH
ID=3; FINISH
ID=2; FINISH
ID=1; FINISH

The result was that task#1 finished after task#10 although task#1 started before task#10, show that Crystal can switch tasks. And the important thing is the code looks like regular Ruby code, without async-await nor promise nor callback.

หลายครั้งผมแค่อยากได้ Ruby ที่มันเร็ว ไม่ต้องแนว ไม่ต้อง functional ก็ได้ ไม่ต้อง minimal ก็ได้

Crystal เป็นภาษาโปรแกรมแบบ static type ความเร็วแบบ thread เดียว ผมเคยเอามาลองตัดคำ ถ้าไม่ optimize อะไรเลย ความเร็วไล่ ๆ กับ Go

ประเด็น hot ช่วงนี้คือถ้าใช้แบบ concurrent จะเป็นอย่างไร Crystal ไม่ได้ใช้ multithreading แน่ ๆ เพราะมันยังทำไม่เสร็จ 🤣 แต่ว่าตอนนี้มีพวก event loop และ fiber แน่ ๆ ดูคร่าว ๆ แล้วน่าจะทำงานคล้าย ๆ Node.js ได้โดยไม่ต้องเขียน async-await

code ต่อไปนี้ผมเขียนมาลองว่าจะเป็นแบบที่คิดจริงหรือไม่

require "socket"

def load(id, chan)
  puts "ID=#{id}; BEFORE"
  (id..11).each do 
    socket = TCPSocket.new("www.ku.ac.th", 80)
    puts "ID=#{id}; Blocking?=#{socket.blocking}"
    socket.close
  end
  puts "ID=#{id}; AFTER"
  chan.send nil
end

def main
  chan = Channel(Nil).new
  (1..10).each{|i| spawn(load(i,chan))}
  # Wait
  (1..10).each{chan.receive}
end

main 

code มันก็จะคล้าย ๆ Ruby หน่อย ผมเขียนให้ ถ้า id ยิ่งน้อยยิ่งรันนาน เพื่อที่จะดูว่างานที่ spawn มาทีหลังมันมันแซงมาเสร็จก่อนได้เปล่า อีกอย่างก็คือ print มาให้ดูด้สนว่าใช้ non-blocking IO

ID=1; BEFORE
ID=2; BEFORE
ID=3; BEFORE
ID=4; BEFORE
ID=5; BEFORE
ID=6; BEFORE
ID=7; BEFORE
ID=8; BEFORE
ID=9; BEFORE
ID=10; BEFORE
ID=1; Blocking?=false
ID=2; Blocking?=false
ID=3; Blocking?=false
ID=4; Blocking?=false
ID=5; Blocking?=false
ID=6; Blocking?=false
ID=7; Blocking?=false
ID=8; Blocking?=false
ID=9; Blocking?=false
ID=10; Blocking?=false
ID=1; Blocking?=false
ID=2; Blocking?=false
ID=3; Blocking?=false
ID=4; Blocking?=false
ID=5; Blocking?=false
ID=7; Blocking?=false
ID=6; Blocking?=false
ID=8; Blocking?=false
ID=9; Blocking?=false
ID=2; Blocking?=false
ID=1; Blocking?=false
ID=10; Blocking?=false
ID=10; AFTER
ID=4; Blocking?=false
ID=5; Blocking?=false
ID=3; Blocking?=false
ID=7; Blocking?=false
ID=6; Blocking?=false
ID=8; Blocking?=false
ID=9; Blocking?=false
ID=9; AFTER
ID=2; Blocking?=false
ID=1; Blocking?=false
ID=4; Blocking?=false
ID=5; Blocking?=false
ID=3; Blocking?=false
ID=7; Blocking?=false
ID=6; Blocking?=false
ID=8; Blocking?=false
ID=8; AFTER
ID=2; Blocking?=false
ID=1; Blocking?=false
ID=4; Blocking?=false
ID=5; Blocking?=false
ID=3; Blocking?=false
ID=7; Blocking?=false
ID=7; AFTER
ID=1; Blocking?=false
ID=2; Blocking?=false
ID=6; Blocking?=false
ID=4; Blocking?=false
ID=5; Blocking?=false
ID=3; Blocking?=false
ID=1; Blocking?=false
ID=2; Blocking?=false
ID=6; Blocking?=false
ID=6; AFTER
ID=4; Blocking?=false
ID=5; Blocking?=false
ID=5; AFTER
ID=3; Blocking?=false
ID=1; Blocking?=false
ID=2; Blocking?=false
ID=4; Blocking?=false
ID=4; AFTER
ID=3; Blocking?=false
ID=1; Blocking?=false
ID=2; Blocking?=false
ID=3; Blocking?=false
ID=3; AFTER
ID=1; Blocking?=false
ID=2; Blocking?=false
ID=2; AFTER
ID=1; Blocking?=false
ID=1; AFTER

อันนี้ก็จะเห็นได้เลยว่า ID=1 ที่เริ่มรันก่อนมันเสร็จทีหลังเลย สรุปก็คือใน Crystal ใช้ non-blocking IO กับ event loop ได้โดยที่ programmer ไม่ต้องทำอะไรพิเศษเลย ก็แค่เขียน ๆ ไปธรรมดา ในกรณีที่เป็น web server ส่วน spawn และ channel ก็ไม่ต้องเขียนเองด้วย

Go, Erlang, Elixir ทำอะไรได้มากกว่านี้ แต่ Crystal มันเป็นภาษาที่แทบไม่ต้องปรับตัวเลย สำหรับคนที่เคยเขียน Ruby งาน CPU bound ก็น่าจะเร็วกว่า Erlang แลพ Elixir ด้วย

ผมจะใช้คำศัพท์ thread และ process ในบริบทของ Linux และ *BSD นะครับ ใน OS อื่น ๆ บางตัวเรียกไม่เหมือนกัน

nginx และ thread pool

  • nginx ใช้ event loop ด้วย ใช้หลาย thread ด้วยครับ 1
  • ผมเข้าใจว่าใช้ event loop เดียวรับ request แล้วไปปล่อย task เข้า thread อื่นอีกที แล้วส่งผลคิน event loop

node.js cluster

  • node.js cluster แตก process (แทนที่จะแตก thread) ครับ มี process หลักมา accept incoming connection แล้วส่งต่อให้ process ลูกครับ2
  • แต่ว่าไม่แน่ใจว่ามี event loop ของแต่ละ process อีกทีหรือเปล่าครับ ผมเดาว่าน่าจะมีแต่ก็ไม่เคยลองดูจริง ๆ ครับ

Tokio

  • Tokio อย่างน้อยจะต่างจาก node cluster ตรงที่ไม่ได้แตก process ครับ แต่ว่าใช้แตก thread ในแง่นี้น่าจะคล้าย nginx มากกว่า node cluster
  • ข้อมูลอีกอย่างคือมี event loop เท่าจำนวน thread ที่แตกออกมาครับ แต่ส่วนนี้ผมยังไม่ได้ดูว่าแบ่งงานกันอย่างไรครับ

CGI

ส่วนพวก CGI คือแตก process (ไม่ใช่ thread) ทุกครั้งที่มี request ครับ

PHP

  • php-fpm แตก process มารับ request
  • mod_php บน Apache ก็แตก process เหมือนกันครับ เพราะว่า php ต้องการแบ่งแยกระดับ process
  • พวกนี้ก็จะทำให้เร็วขึ้นโดยใช้ prefork ไว้ก่อนทำให้เร็วขึ้นได้
  • ใช้ process pool ไม่ได้ fork หมดเหมือน CGI แต่ก็เปลือง RAM อยู่ดีครับ
  • นอกจากนั้นเพื่อที่จะทำให้เร็วขึ้นมันมี opcache ที่ compile โปรแกรมเก็บไว้ก่อนรับ request อยู่แล้วครับ

Gunicorn และ Meinheld

gnuicorn ใช้ process มาเป็น worker โดยที่ worker ที่ใช้ meinheld ข้างในก็ไปใช้ picoev เอามาทำ event loop อีกที

ผมไม่ค่อยแม่นเรื่องนี้เท่าไหร่ถ้ามีอะไรมั่วไปก็ขออภัยด้วยครับ สามารถพูดคุยกันเพิ่มเติมได้ที่ veer66@mastodon.in.th

เขียนโปรแกรมบน web server ยุค 90 ส่วนมากก็ผ่าน CGI เอาครับ สมัยนั้นใช้ C Perl Sh เขียนกัน URL มันก็จะประมาณ http://localhost/cgi-bin/do_sth.pl งี้เลย web server มันก็จะเรียก do_sth.pl ให้อีกที ถ้าเป็นบน GNU/Linux ก็คือต้อง fork/spawn process ขึ้นมาใหม่เลย

ปลาย ๆ ยุค 90 ค่าย Java ก็มีตัวเลือกมาให้อีกแบบคือใช้ Servlet ซึ่งตัวนี้ใช้หลาย ๆ thread เอาแทนทีจะมา spawn process ใหม่ ซึ่งผมเดาว่าเวลา implement เขาก็น่าจะมี thread pool ตั้งแต่ตอนนั้น พูดอีกอย่างคือเอา thread สร้างมาแล้วมาใช้ซ้ำ

PHP ก็มาปลาย ๆ ยุค 90 เหมือนกัน แต่ผมเริ่มใช้ทีแรกก็เป็น mod_php ที่ฝังมาใน Apache Httpd แล้ว เวลาใช้จริง ๆ มันก็ fork process ใหม่อยู่ดี แต่ว่าก็มี prefork ไว้ก่อนได้ ถ้าผมเข้าใจไม่ผิดแต่ละ process ก็จะมี PHP runtime พร้อม extension ต่าง ๆ อยู่ใน memory ด้วย อันนี้น่าจะเป็นเหตุผลหลัก ๆ ที่หลาย ๆ ท่านบ่นกันว่ามันกิน RAM

ยุค 90 MS ก็มี ASP ออกมา แต่ผมไม่รู้เลยว่าทำงานอย่างไร ก็เลยข้ามไปแล้วกัน

หลังจากที่ไม่ค่อยจะมีอะไรน่าสนใจออกมาสักพัก กลาง ๆ ยุค 2000 ก็มี Ruby on Rails ออกมา ผลที่ตามมาคือมี web server ที่เอามาใช้รัน Rails ได้หลายตัว ซึ่งผมไม่รู้เลยว่าตอนนั้นเขาทำแบบไหนกัน 😅; ตอนนั้นผมใช้ TurboGears ก่อน 1.0 (บน Python) ส่วน Web Server ใช้ CherryPy ผมลองดู version เก่า ๆ ก็ใช้หลาย thread แล้วมี threadpool ด้วย

ราว ๆ ค.ศ. 2008 ผมก็เริ่มได้ยินพี่ป็อกพูดถึง Erlang ซึ่ง Erlang นี่ผมก็เพิ่งมาทราบทีหลังว่ามาในชุมสายโทรศัพท์ตั้งแต่ยุค 80 แล้ว ใน Erlang ใช้คำว่า process แทน thread แต่ว่าเอาจริง ๆ มันไม่ใช่ process ของ Linux หรือ BSD แบบของ Apache Httpd มันคล้าย green thread ของ Java ยุคแรก ๆ มากกว่า process ของ Linux แต่ว่าเรียกว่า process เพราะว่าไม่ได้ share memory แบบที่ thread ส่วนมากทำ Erlang process ใช้ memory อยู่ในวิสัยที่จะ spawn มาเป็นแสนตัวได้ สลับ ๆ ไปสลับมาแบบ preemptive ต่อให้ไม่มี I/O หรือไปสั่งให้สลับ scheduler มันก็จะสลับให้ แต่ถึงมันจะสร้าง process ได้เยอะแยะแต่ก็เหมือนว่าเขาจะไม่ได้สร้างกันทุกครั้งที่ request เข้ามา แต่ว่าไปใช้ gen_server ซึ่งถ้าผมเข้าใจไม่ผิดมันทำให้ reuse process เดิมได้

ยุคหลัง 2010 คนเริ่มเปลี่ยนมาใช้ node.js ซึ่งใช้ประโยชน์จาก kernel polling เช่น epoll และ kqueue เอามาทำ event loop พอมีงานที่ต้องรอ I/O ก็เอาออกไปรอก่อนแล้วสลับไปรับงานอื่นได้ แต่ว่า JavaScript แต่เดิมมันแตก thread ไม่ได้ node.js ก็แตก thread ไม่ได้เหมือนกัน เวลาจะใช้ประโยชน์จาก CPU หลาย ๆ core ก็เลยต้องแตก process ของ Linux หรือ OS อื่น ๆ เอาแล้วก็มี runtime ไปอยู่ทุก process เลย

พอเริ่มใช้ node.js ไม่เท่าไหร่ ก็มี Golang จาก Google ออกมา ซึ่งจะมองว่า Golang ก็คือแบบที่ใช้ multithread เลยก็ได้ พวกตัวที่เร็ว ๆ เช่น fasthttp ก็มี thread pool คล้าย ๆ กับ web server ก่อนหน้านี้ แต่ว่า thread ของ Golang หรือที่เรียกว่า Goroutine มันใช้ memory น้อยไม่ใช่ thread ของ OS ตรง ๆ ทำให้ spawn ออกมาเยอะ ๆ ได้ fasthttp อาจจะเป็นไปได้ว่า spawn ออกมาหลักแสนตัวได้เลย แล้วก็ปล่อย scheduler ของ Go runtime ไปจัดลง thread ของ OS เอาเอง จะเห็นว่าคล้าย ๆ Erlang อยู่บ้าง แต่ว่า Goroutine อย่างน้อยก็ช่วงนี้ยังไม่ได้สลับแบบ preemptive

วงการ Rails เองก็มีการปรับตัว Puma ที่เป็น default web server ทุกวันนี้ก็เป็นแบบ multithread โดยที่ค่า default คือให้เปิดมาได้เลย 5 thread แต่ใน production จะใช้มากกว่านี้ก็ได้ เทียบกับตัวเก่า ๆ Puma รับ concurrency ได้มากขึ้นใช้ RAM น้อยลง เท่าที่ผมเห็นก็คือมีทั้ง worker thread ที่มาประมวลผล โดยที่ก็จะวน ๆ ใช้จาก thread pool ไม่ใช่ว่าสร้างใหม่บ่อย ๆ มีอีก thread นึงคือไป accept request แล้วอ่านข้อมูลมารอใส่ queue ชื่อ todo ไว้ นอกจากนั้น parser ก็เขียนด้วย C ทำให้เร็วขึ้นไปอีก Puma นี่มันเลือกแตก process ผสมกับ Thread ได้ด้วย ผมเดาว่าเพราะ MRI-Ruby มันติด global lock ถ้าเจองานใช้ CPU มาก ๆ อาจจะใช้ได้ไม่ทั่วถึง แต่ถ้าไปใช้ JRuby แทนก็เป็นอันตกไป

Tokio เขียนบน Rust ที่มี mio ที่ไปหุ่ม epoll และ kqueue ได้เหมือน node.js ถ้าเทียบกันน่าจะอยู่ในระดับเดียวกับ libuv แต่ว่า Rust ไม่มีปัญหาเรื่องแตก thread อยู่แล้ว Tokio ก็เลยทำได้ทั้งแตก thread (multithread) แล้วก็มี event loop แบบ node.js ด้วย ในแต่ละ thread ก็มี event loop เองเลย

อีกวิธีหนึ่งของค่าย Rust คือทำ coroutine คล้าย ๆ Goroutine ขึ้นมาเลยใน project ชื่อ May แต่ทั้ง May และ Tokio นี่มันเป็นระดับล่างมาก ๆ ผมเข้าใจว่าคนทำเว็บส่วนมากคงไม่มาเรียกตรง ๆ น่าจะใช้ผ่าน Rocket หรือ Actix แทน

Rust นี่แตก thread กันเป็นว่าเล่นส่วนหนึ่งอาจจะเพราะว่ามันไม่มี GC ส่วนหนึ่งที่ยากเวลาแตก thread คือต้องทำ GC ให้ใช้งานสอดคล้องกับ scheduler ด้วย ผมเห็นสองภาษาที่โมเรื่องนี้อยู่คือ Ocaml และ Crystal ถ้าเสร็จแล้วก็น่าจะจัดอยู่กลุ่มเดียวกับ Golang ได้ แต่อาจจะเขียนง่ายกว่า

ใน shell script มัน pipe ต่อกันไปได้เรื่อย ๆ แบบนี้

cat input.txt | \
      prog1 x | \
      prog2 y | \
      prog3 z
  • cat อ่านไฟล์ input1.txt แล้วก็ส่งออกมาทาง stdout ต่อมาก็ส่งเข้า prog1 ทาง stdin
  • prog1 อ่านข้อมูลจาก stdin ที่ cat ส่งมา; แล้วประมวลผลแล้วก็ส่งออกไป stdout
  • prog2 อ่านข้อมูลจาก stdin ที่ prog1 ส่งมา; แล้วประมวลผลแล้วก็ส่งออกไป stdout
  • prog3 อ่านข้อมูลจาก stdin ที่ prog2 ส่งมา; แล้วประมวลผลแล้วก็ส่งออกไป stdout
  • โดยที่ prog1 prog2 prog3 ก็รับค่า x y z เข้ามาทาง argument ด้วย

โปรแกรมแบบนี้จะเขียนใหม่เป็น Ruby ก็จะได้ประมาณนี้ หรือจะใช้ Java ก็จะคล้าย ๆ กัน

p read_file("input.txt").prog1(:x)
    .prog2(:y)
    .prog3(:z)

พวก prog1 prog2 prog3 กลายเป็นชื่อ method ใน object ที่ดูจาก code ก็ไม่รู้ว่า object อะไร หรือกระทั่งตอนรันก็ไม่รู้ว่า prog1 ส่งข้อมูลอะไรออกมา

ถ้าเป็น shell script ก็ใส่ tee เข้าไปได้เลย

cat input.txt | \
      prog1 x | \
      prog2 y | \
      tee debug.txt | \
      prog3 z

แล้วก็ไปเปิดดูไฟล์ debug.txt ทีหลังได้ โดยที่โปรแกรมโดยรวมก็ยังทำงานปกติ

แต่ถ้าเป็น Ruby หรือ Java ก็จะกลายเป็นท่ายาก เพราะว่าใส่ tee เข้าไปใน object ก็ยากเพราะไม่รู้ว่า object ไหน class อะไร ใส่ไปแล้วจะมั่วหรือเปล่า

โปรแกรมมันก็จะออกมาประมาณนี้แทน

tmp = read_file("input.txt").prog1(:x)
        .prog2(:y)
write_file(tmp)
p tmp.prog3(:z)

ถ้าลองเปลี่ยนเป็น Clojure มันก็จะประมาณนี้

(-> (slurp "input.txt")
    (prog1 :x)
    (prog2 :y)
    (prog3 :z)
    println)

พวก prog1 prog2 prog3 ก็จะเป็นแค่ function ธรรมดาแทนที่จะเป็น method

นอกจากนั้นก็ยังเขียน tee ขึ้นมาง่าย ๆ ได้แบบนี้

(defn tee [data path]
    (spit path data)
    data)

แล้วก็เอา tee ไปแทรกได้แบบ shell script

(-> (slurp "input.txt")
    (prog1 :x)
    (prog2 :y)
    (tee "debug.txt")
    (prog3 :z)
    println)

โปรแกรมทั้งหมดผมไม่ได้ลองรันจริง ๆ นะครับ อาจจะเจ๊งได้ จาก blog นี้ผมรู้สึกว่าตอบคำถามตัวเองได้ว่าทำไมเวลาเขียน Clojure หรือ Common Lisp ในโปรแกรมที่ค่อนข้างซับซ้อนแล้วรู้สึกสะดวกกว่า Ruby เอาจริงๆ จะเขียน Ruby หรือ Java ให้คล้าย ๆ Clojure ก็คงทำได้ แต่เขียนไปก็จะถูกเรียกว่าไม่ idiomatic อันนี้ก็เป็นอีกเหตุผลนึงให้เปลี่ยนภาษาเพราะว่าภาษามันพ่วงวัฒนธรรมมาด้วย

จริง ๆ จะเขียนให้มันง่าย ๆ กว่านี้ก็ได้ เช่น ส่ง N ไปใน argument เลย แต่มันจะง่ายไป อยากจะดูว่าถ้าใช้ fun แล้วมันจะเรียกตัวเองได้ไหม ปกติก็คือไม่ได้ แต่ก็มีท่าแก้คือส่ง Fib เข้าไปเป็น F แล้วใน Fib เรียก F อีกที

(แก้ code วันที่ 18 มี.ค. 2556)

-module(rec3).
-export([fib/1]).

fib(N) ->
    Fib = fun (F, I, Acc1, Acc2) when I < N -> 
		  F(F, I + 1, Acc2, Acc1 + Acc2);
	      (_, _, _, Acc2) -> 
		  Acc2
	  end,
    Fib(Fib, 0, 0, 1).

ผมลองใส่ N ไป 100,000,000 มันก็ไม่ stack overflow นะครับ แต่ก็ไม่รู้ว่าผลถูกหรือเปล่า 😅