May 28, 2014

อย่าเสียเวลาให้กับลูปที่เรียก System บ่อยๆ

ช่วงนี้มีวิเคราะห์ string บนไฟล์จำนวนมหาศาล (หลายแสนไฟล์) ตอนแรกก็เขียน Python ง่ายๆ ตามความเคยชิน
counter = 0
for file in glob('*.cpp'):
    if '//' in file.read():
        counter += 1
print(counter)
โค้ดนี้ทำงานได้ดีเลยแหละ ติดตรงที่ไฟล์ที่นำมาวิเคราะห์บางไฟล์ก็ใช้ encoding แปลกๆ แล้วเปิดอ่านไม่ได้ (ยังงงอยู่ว่า Python ไม่เช็คว่าเป็น ASCII หรือ UTF-8 ให้อัติโนมัติ?) ก็แก้ๆ โค้ดด้านบนนี้โดยใช้โมดูล codecs เข้าช่วย ไม่มีปัญหาอะไร

แต่แล้วก็คิดขึ้นมาได้ว่า งานประเภทนี้ถ้าทำใน Bash ไปเลยก็น่าจะแจ่มเหมือนกัน เพราะปรัชญาของ Unix คือให้โปรแกรมจัดการกับ I/O Stream แบบข้อความอยู่แล้ว เลยได้โค้ดออกมาเป็น
counter=0
for file in *.cpp
do
    grep -q '//' "$file" && : $((counter++))
done
echo $counter
ผลลัพท์ออกมาไม่ต่างกับ Python ด้านบน แต่ว่า Bash ทำงานช้ามากกกกก (ก.ไก่ 140 ตัว) ตอนแรกก็คิดว่า Bash มันน่าจะทำลูปไม่เก่งมั้ง เพราะเท่าที่อ่านโค้ดของโครงการ Linux ก็ไม่ค่อยเห็นเขียนลูปกันเท่าไหร่ แต่พอลองเขียนลูปเปล่าแสนรอบใน Bash มันก็ไม่เห็นจะช้า เลยคิดว่าน่าจะเป็นที่ grep มั้ง? แต่ก็ไม่อยากจะเชื่ออยู่ดีเพราะเคยอ่านงานของ Russ Cox ก็ไม่เห็นว่า grep มันจะช้าซักเท่าไหร่

อาจารย์ @juggapong ผ่านมาเห็นเลยยิงคำถามมาว่า มันช้าเพราะมัวแต่มุดเข้ามุดออก system หรือเปล่า?

ได้ยินดังนั้นเลยใช้ time จับเวลาดู โอ้ว user กับ sys ใช้เวลาพอๆ กันเลยแฮะ

ถึงตอนนี้ก็รู้แล้วว่าสาเหตุที่มันช้าเพราะ grep แต่ละครั้งเป็น process ใหม่เลย ต้องรอให้ system จองทรัพยากรเสร็จก่อนจึงจะใช้งานได้ แถมพอจบงานก็ทำลาย process นี้ทิ้งซะอีก ไม่ได้จัดสรรทรัพยากรอะไรกันระหว่างลูปเลย ... เทียบง่ายๆ คงเหมือนกับการที่มีเตารีดแสนอัน เสียบปลั๊กเตารีดตัวแรกรอร้อนแล้วจึงรีดผ้าหนึ่งชิ้น พอผ้าชิ้นนี้เรียบเนี๊ยบจนพอใจก็ถอดปลั๊กเตารีดซะงั้น แล้วไปเสียบปลั๊กเตารีดอีกตัวมารีดผ้าอีกชิ้นนั่นเอง

คิดได้ดังนี้ก็เปลี่ยนโค้ดไปเป็น (ใช้เตารีดตัวเดียว เสียบปลั๊กรอร้อนครั้งเดียว)
grep -l '//' *.cpp | wc -l
คราวนี้เร็วส์เสียยิ่งกว่า Python อีก :P

(แต่สุดท้ายก็คงกลับไปเขียน Python อยู่ดี เพราะมีปัจจัยที่ใช้วิเคราะห์ซับซ้อนกว่านี้ ใช้ Python แล้วรู้สึกถนัดมือกว่า)

May 25, 2014

ที่ทางของคำสั่ง : ใน Shell

เคยเขียน (มั้ง) มาทีละว่า ทุกคำสั่งใน linux มันมีที่ทางของมันหมด แม้จะเป็นคำสั่งเล็กๆ ที่ทำหน้าที่ง่อยๆ ก็ตามที ... แต่วันนี้จะมานำเสนอคำสั่ง : (colon ตัวเดียว) ซึ่งเป็นคำสั่งที่ไม่ทำอะไรเลย

อ้าว แล้วเมื่อมันไม่ทำหน้าที่อะไรเลย จะมีไว้ทำไมหละ?

งั้นต้องถามก่อนว่า เคยประสบปัญหานี้มั้ย
> # ตั้งตัวแปรไว้ใช้ดีกว่า
> foo="hahaha"
> # ทำงานอื่นไปเรื่อยๆๆๆๆ
> # เอ๊ะ ตัวแปร $foo โดนเปลี่ยนค่า (?) ไปเป็นอะไรแล้วนะ?
> $foo
hahaha: command not found
ด้านบนนี้เป็น user error เอง อยากดูค่าก็อย่าลืม echo สิ :P

อย่างไรก็ตาม อย่าลืมว่า Shell มันเป็น programming language ตัวหนึ่งเลย ความสามารถที่หลายๆ คนลืมคือเราสามารถทำการคำนวณใน Shell ได้
> i=0
> for f in *
> do
>     [ -s "$f" ] && i=$((i+1))
> done
> echo $i
โค้ดด้านบนนี้จะถามว่าไฟล์ในแฟ้มปัจจุบัน มีกี่ไฟล์ที่มีเนื้อหา (ขนาดไม่เป็น 0 byte) ซึ่ง counter จะเพิ่มขึ้นตรงหลังเครื่องหมาย && นั่นเอง

ในโลกความจริงเราไม่ได้เขียนโค้ดอย่างนี้ตลอด อาจมีบางครั้งที่ต้องเอาการคำนวณแยกไว้ในที่เดี่ยวๆ เช่น
> i=0
> for ...
> do
>     # ทำอะไรซักอย่าง
>     i=$((i+1))
> done
เขียนไปเขียนมาจะพบว่า i=$((i+1)) มันตลกสิ้นดี ยิ่งเมื่อรู้ว่า Shell เขียน i++ ได้! แต่อย่าลืมว่าถ้าเปลี่ยนไปเขียนตรงๆ ก็จะเจอปัญหาเดิม
> $((i++))
0: command not found
ทั่วไปแล้ว การเขียนในแนวนี้จะเป็นการสร้างตัวนับเพื่อบอกว่า process นี้ทำงานไปถึงไหนแล้ว ซึ่งมันสามารถเอาไปแทรกไว้กับคำสั่ง echo ได้
> echo "now doing item number: $((++i))"
แต่ถ้าไม่ต้องการให้มัน echo อะไรระหว่างทางหละ? นี่แหละคือจุดยืนของคำสั่ง :
> : $((i++))
ป.ล. Shell รุ่นเก่าๆ ไม่มีคำสั่ง true เลยต้องใช้คำสั่ง : แทน

Apr 13, 2014

Code Jam 2014 รอบคัดเลือก


ปีนี้ปัญหาเยอะนิดหน่อยเพราะวันแข่งตรงกับวันเดินทางออกต่างจังหวัดพอดี และเนื่องจาก #แล้วรถไฟความเร็วสูงกูหละ ทำให้เสียเวลาเดินทางสั้นๆ แค่ 300 กว่ากิโลเมตรไป 8 ชั่วโมงครึ่ง แถมพอถึงที่หมายแล้วก็หาเวลาส่วนตัวไม่ค่อยได้เลย กว่าจะได้ลงมือจริงๆ ก็ตีหนึ่งแล้ว (มีเวลา 8 ชั่วโมงจาก 27 ชั่วโมง) ผลงานที่ได้เทียบกับ effort ที่สามารถลงให้มันไปถือว่าน่าผิดหวังเล็กน้อย :\

อย่างไรก็ตาม ปีนี้ก็บรรลุเป้าข้อนึงคือใช้ภาษาอย่างน้อย 3 ภาษาเขียนแก้โจทย์ ก็ได้แก่ Python, Haskell และ CoffeeScript (อันที่จริง CoffeeScript นี้แปลจาก Python มาตรงๆ นาทีเดียวเสร็จเลยนะ :P) ปีต่อไปพยายามตั้งเป้าไว้ที่ Racket/Scheme กับ matrix processing language ซักตัวครับ

มาดูข้อแรกข้อ A ที่ตามธรรมเนียมจะง่ายที่สุดกัน ยิ่งใช้ Python ยิ่งง่ายใหญ่เลยเพราะมี set intersection ให้ใช้ เราก็แค่หาว่า intersect ของตัวเลขที่นักมายากลถามมานั้นคืออะไร ถ้ามีตัวเดียวก็คือตัวนั้นแหละ ถ้ามีมากกว่าหนึ่งแปลว่านักมายากลแม่งกาก แต่ถ้าไม่มีเลยนี่ผู้ชมโกงแล้วหละ

def foo(answer_1, matrix_1, answer_2, matrix_2):
    candidate_1 = matrix_1[answer_1-1]
    candidate_2 = matrix_2[answer_2-1]
    final = set(candidate_1) & set(candidate_2)
    if len(final) == 0:
        return 'Volunteer cheated!'
    elif len(final) > 1:
        return 'Bad magician!'
    else:
        return final.pop()

for case in range(int(input())):
    answer_1 = int(input())
    matrix_1 = [[int(n) for n in input().split()] for _ in range(4)]
    answer_2 = int(input())
    matrix_2 = [[int(n) for n in input().split()] for _ in range(4)]
    answer = foo(answer_1, matrix_1, answer_2, matrix_2)
    print('Case #{}: {}'.format(case+1, answer))



ข้อ B ถัดมาเริ่มจะต้องสังเกตละ แต่เนื่องจากผมตั้งใจเขียนข้อนี้ในสไตล์แบบ clean code พอสมควร (ไม่มี comment, ตั้งชื่อตัวแปรให้รู้เรื่อง) ดังนี้ข้อนี้ขออธิบายด้วย code อย่างเดียวนะครับ

def optimal_cookie(new_farm_cost, prodct_cookie, finale_cookie):
    income_cookie = 2.0
    optimal_spent = 0
    while True:
        new_farm_time = new_farm_cost / income_cookie
        expect_cookie = prodct_cookie + income_cookie
        wait_win_time = finale_cookie / income_cookie
        upgd_win_time = finale_cookie / expect_cookie + new_farm_time
        if wait_win_time > upgd_win_time:
            income_cookie = expect_cookie
            optimal_spent += new_farm_time
        else:
            optimal_spent += wait_win_time
            break
    return optimal_spent

for case in range(int(input())):
    answer = optimal_cookie(*[float(n) for n in input().split()])
    print('Case #{}: {:.7f}'.format(case+1, answer))

หมายเหตุว่าข้อนี้ถ้าไปดูคำตอบผมในเว็บ จะเห็นคำตอบสำหรับข้อเล็กเขียนด้วย Haskell ส่วนข้อใหญ่เขียนด้วย CoffeeScript ครับ แต่ในการทดลองนี้จะใช้อันไหนทดสอบที่ระดับใดก็ได้



กระโดดข้ามมาข้อ D ก่อน ข้อนี้ยากตรงที่เราต้อง proof จนได้ strategy ที่ดีที่สุด เราอาจสังเกตว่า ในเกม war ธรรมดามีวิธีเล่นที่ดีที่สุดดังนี้
  • ถ้า Naomi เล่นกล่องที่หนักกว่าทุกกล่องของ Ken ทางเลือกที่ดีที่สุดสำหรับ Ken คือเล่นกล่องที่เบาที่สุด เพื่อให้กล่องที่เหลือๆ มีประสิทธิภาพในการสู้สูงที่สุด
  • ถ้า Naomi เล่นกล่องที่เบากว่ากล่องบางกล่องของ Ken แล้ว Ken จะตอบโต้ด้วยกล่องที่หนักกว่ากล่องของ Naomi แต่เป็นกล่องที่เบาที่สุดที่ยังจะหนักกว่ากล่องของ Naomi เพราะการเล่นแบบนี้ทำให้ได้คะแนน และยังเก็บกล่องหนักๆ ไว้ใช้สู้ในอนาคตได้อีก
พอเกมเปลี่ยนมาเป็น deceitful war ทาง Naomi ก็ใช้ข้อได้เปรียบจากการล่วงรู้น้ำหนักกล่องของ Ken ทั้งหมดมาเล่น โดยยึดกลยุทธ์ดังนี้
  • Naomi สามารถเล่นกล่องที่เบาที่สุดของตน แต่หลอกว่ากล่องนี้มีน้ำหนักน้อยกว่ากล่องที่หนักที่สุดของ Ken เนื่องจาก Ken ยังยึดกฎเดิม เขาจะตอบโต้ด้วยกล่องที่หนักที่สุด (กินคะแนน)
  • Naomi สามารถเล่นกล่องที่เบาที่สุดของตน แต่หลอกว่ากล่องนี้มีน้ำหนักมากกว่ากล่องที่หนักที่สุดของ Ken เช่นกันเพราะ Ken ยึดกฎเดิม เขาจะตอบโต้ด้วยกล่องที่เบาที่สุด (ทิ้งของกากสุด)
เทคนิคสำคัญคือรอจังหวะที่แต่ละกล่องของ Naomi เมื่อเรียงน้ำหนักและนำไปจับคู่กับ Ken แล้ว ทุกกล่องมีน้ำหนักมากกว่า เมื่อนั้น Naomi จะใช้กลยุทธ์ล่างทันที แต่ถ้ายังไม่มีโอกาสก็จะยึดกลยุทธ์แรกไปก่อน เพื่อกำจัดกล่องหนักๆ ของ Ken ทิ้งไป

from collections import deque

def normal_war(ns, ks):
    c = 0
    while ns:
        n = ns.pop()
        if ks[-1] < n:
            ks.pop(0)
            c += 1
        else:
            i = 0
            while n > ks[i]:
                i += 1
            ks.pop(i)
    return c

def deceitful_war(ns, ks):
    while any(n < k for n, k in zip(ns, ks)):
        ns.popleft() and ks.pop()
    return len(ns)

for case in range(int(input())):
    _ = input()
    ns = sorted(float(n) for n in input().split())
    ks = sorted(float(n) for n in input().split())
    dw_answer = deceitful_war(deque(ns), deque(ks))
    nw_answer = normal_war(ns[:], ks[:])
    print('Case #{}: {} {}'.format(case+1, dw_answer, nw_answer))

ข้อ C ถามว่าในอุดมคติแล้ว เราสามารถกดปุ่มเดียวผ่าน minesweeper ได้มั้ย ซึ่งข้อนี้มีจุดน่าสนใจอยู่ที่การแตกเคสต่างๆ ออกมาให้ครอบคลุม ดังนี้
  • ถ้ากระดานมีความกว้างแค่ 1 หน่วย ยังไงก็กดครั้งเดียวผ่านแน่ เพราะเราก็ให้ระเบิดทั้งหมดไปกองด้านนึง อีกด้านก็ไม่มีอะไร
  • ถ้ากระดานมีความกว้าง 2 หน่วย เราจะได้ว่าต้องมีพื้นที่ว่าง 1 ช่อง (สำหรับกดครั้งเดียวผ่านเลย) หรือที่ว่างต้องเป็นเลขคู่ที่มากกว่า 2 (ถ้ามี 2 ช่องพอดีแม้ช่องนั้นจะอยู่ติดกันก็ต้องกด 2 ครั้ง)
  • กระดานแบบอื่นๆ ทั้วไป พยายามกั้นช่องว่างให้ตัวเองเป็นจัตุรัสก่อน แล้วค่อยๆ เวนคืนที่ว่างเพิ่มเติม ดังนี้
    • ที่ว่าง 1 ช่องเป็นจัตุรัสขนาด 1x1 ง่ายสุด คือกดครั้งเดียวผ่านเลย อย่างไรก็ตาม ที่ว่างแบบนี้ทำให้เราไม่สามารถมีที่ว่าง 2, 3 ได้
    • ที่ว่าง 4 ช่องเป็นจัตุรัสขนาด 2x2 สามารถมีติ่งเพิ่มออกมาอีก 2, 4 ช่องได้ คือจะได้ ที่ว่างรวมเป็น 6, 8 ช่อง ส่วนติ่งที่เพิ่มแค่ 1, 3 ช่องเพิ่มไม่ได้
    • ที่ว่าง 9 ช่องเป็นจัตุรัสขนาด 3x3 และใหญ่กว่า คราวนี้จะมีติ่งเป็นเศษแค่ 1 ช่องก็ได้ ทำโดยไปยืมพื้นที่ว่างหัวมุมมาจับคู่เข้ากับติ่งนี้ซะ
    • สรุปว่าท่านี้ไม่รองรับการเหลือที่ว่างแค่ 2, 3, 5, 7 ครับ
    • ท่านี้ต้องพลิกแพลงนิดหน่อยด้วย ตรงที่ถ้าเกิดขนาดจัตุรัสใหญ่เกินด้านใดด้านหนึ่ง ต้องเปลี่ยนไปใช้โหมดเติมเต็มทีละบรรทัดแทน
อ่านข้อความอย่างเดียวอาจคิดไม่ออก ดูตัวอย่างการ fill แบบต่างๆ บนตาราง 4x4 นี้น่าจะง่ายกว่า
Case #0:      Case #1:      Case #2:      Case #3:
c...          c...          c...          c...
....          ....          ....          ....
....          ....          ....          ...*
....          ...*          ..**          ..**

Case #4:      Case #5:      Case #6:      Case #7:
c..*          c..*          c..*          c..*
...*          ...*          ...*          ...*
...*          ...*          ..**          ...*
...*          ..**          ..**          ****

Case #8:      Case #9:      Case #10:     Case #11:
c.**          Impossible    c.**          Impossible
..**                        ..**
..**                        ..**
..**                        ****

Case #12:     Case #13:     Case #14:     Case #15:
c.**          Impossible    Impossible    c***
..**                                      ****
****                                      ****
****                                      ****
ส่วน code ก็หน้าตาประมาณ
def transpose(matrix):
    return [list(line) for line in zip(*matrix)]

def fill_line(r, c, left):
    width, length = sorted([r, c])
    matrix = [['*' for _ in range(length)] for _ in range(width)]
    for i in range(width):
        matrix[i][:left//width] = ['.' for _ in range(left//width)]
    matrix[0][0] = 'c'
    return matrix if r == len(matrix) else transpose(matrix)

def fill_rect(r, c, left):
    width, length = sorted([r, c])
    sq_length = int(left**0.5)
    matrix = [['*' for _ in range(width)] for _ in range(length)]
    if sq_length > width:
        i = 0
        while left - width > 0:
            matrix[i] = ['.' for _ in range(width)]
            left -= width
            i += 1
        if left == 1:
            matrix[i-1][-1:] = ['*']
            matrix[i][:2] = ['.' for _ in range(2)]
        else:
            matrix[i][:left] = ['.' for _ in range(left)]
    else:
        i = 0
        while i < sq_length:
            matrix[i][:sq_length] = ['.' for _ in range(sq_length)]
            left -= sq_length
            i += 1
        if left > sq_length:
            if left == sq_length + 1:
                matrix[i][:sq_length-1] = ['.' for _ in range(sq_length-1)]
                left = 2
            else:
                matrix[i][:sq_length] = ['.' for _ in range(sq_length)]
                left -= sq_length
            if i == len(matrix)-1:
                matrix = transpose(matrix)
            else:
                i += 1
            matrix[i][:left] = ['.' for _ in range(left)]
        elif left > 0:
            if left == 1:
                matrix[i-1][sq_length-1:sq_length] = ['*']
                left = 2
            matrix[i][:left] = ['.' for _ in range(left)]
    matrix[0][0] = 'c'
    return matrix if r == len(matrix) else transpose(matrix)

def mine_fill(r, c, m):
    width = min(r, c)
    left = r * c - m
    if width == 1:
        if left == 0:
            return None
        return fill_line(r, c, left)
    if width == 2:
        if left in [0, 2] or left % 2 == 1 and left != 1:
            return None
        return fill_line(r, c, left)
    else:
        if left in [0, 2, 3, 5, 7]:
            return None
        return fill_rect(r, c, left)

for case in range(int(input())):
    r, c, m = [int(n) for n in input().split()]
    answer = mine_fill(r, c, m)
    print('Case #{}:'.format(case+1))
    if answer is None:
        print('Impossible')
    else:
        for line in answer:
            print(''.join(line))
ข้อนี้เสียดายมากที่หา bug ไม่เจอ มาเจออีกทีตอนแข่งจบไปแล้ว ไม่งั้นรอบนี้คงได้ทำคะแนนเต็มให้เป็นเกียรติเป็นศรี \(TwT)/

Mar 31, 2014

PHP Closure: เริ่มต้นชีวิตใหม่ปิดกั้นตัวเองยอมลืมทุกสิ่งอย่าง

ภาษาทั่วไปเช่น Python เราสามารถทำ closure เช่นนี้ได้
read_only = 42

def f(x):
    return read_only + x

print(f(10)) # 52
หรือกระทั่ง
ls = [4, 8, 15, 16, 23, 42]

def g(x):
    ls.append(x)

g(99)
print(ls) # [4, 8, 15, 16, 23, 42, 99]
ความประหลาดและน่ารำคาญใน PHP คือเราไม่สามารถเขียนแค่นี้เพื่อทำ closure ง่ายๆ ตามข้างบนได้ เพราะเมื่อเราสร้างฟังก์ชันขึ้นมาแล้ว ฟังก์ชันนั้นจะไม่รู้จักตัวแปรใดๆ เลย (ยกเว้นพวก superglobals อย่าง $_GET) เราต้องเพิ่ม keyword global เข้าไปอีก เช่น
$ls = array(4, 8, 15, 16, 23, 42);

function g($x) {
    global $ls;
    $ls[] = $x;
}
ซึ่งท่านี้ก็ยังมีปัญหาอีกว่าถ้าตัวแปรที่ต้องการไม่อยู่ใน global scope (เช่นไปอยู่ใน local scope ของฟังก์ชันที่สร้างฟังก์ชันนี้อีกที) PHP ก็จะหาตัวแปรนั้นไม่เจอ

ทางแก้ที่ดูแล้วเป็น functional มากที่สุด คือใช้ keyword use เช่นนี้
$ls = array(4, 8, 15, 16, 23, 42);

$g = function($x) use($ls) {
    $ls[] = $x;
    return $ls;
};
แน่นอนว่าเมื่อทำแบบ functional แล้ว ตัวแปร $ls เก่าจะไม่เปลี่ยนค่า เพราะเมื่อมันถูกเรียกผ่าน use นั่นหมายถึงการคัดลอกค่าตัวแปรมาทั้งหมด แล้วตั้งชื่อตัวแปรให้เหมือนกันใน scope ต่างกัน แต่ถ้าอยากให้ตัวแปรเดิมเปลี่ยนค่าก็ยังสามารถใช้เทคนิคเดิมได้คือ
function($x) use(&$ls) { ... }
อย่างไรก็ตาม ท่านี้ยังมีปัญหาตรงที่การประกาศฟังก์ชันต้องทำแบบ anonymous (แล้วค่อยเอาตัวแปรไปรับ) แถมถ้าเราจะอ้างค่าใน scope อื่นเป็นจำนวนมาก ที่หัวฟังก์ชันจะเขียนได้รุงรังอย่าบอกใคร



ข้อดีเดียวที่นึกออกจากการบังคับใช้ global หรือ use สำหรับเรียกตัวแปรนอก scope คือ PHP อนุญาตให้ไม่ต้อง init ตัวแปรก็ได้ (ถ้าไม่มีการ init มาก่อน มันจะถือว่าเป็นค่าว่างตามการใช้งานนั้นๆ) ทำให้เราสามารถเขียนอะไรเช่นนี้ได้
function query_to_array() {
    $res = mysql_query('SELECT * FROM blah_blah_blah');
    foreach ($res as $row) {
        $ls[] = $row['foo_blah'];
    }
    return $ls;
}
เราอาจมองว่าท่านี้สวยตรงที่ไม่ต้อง init ตัวแปร $ls ที่รู้ๆ กันอยู่แล้วว่าต้องเป็น array ว่างแน่ๆ แต่ถ้าเกิดว่า query ข้างบนให้ผลลัพท์เป็นเซ็ตว่าง ตอน return เอาไปใช้ต่อจะเกิด bug เพราะ PHP ไม่สามารถบอก type ของ $ls ได้ สุดท้ายก็ต้องกลับไปประกาศตัวแปรไว้ที่จุดเริ่มต้นฟังก์ชันอยู่ดี หรือไม่งั้นก็เปลี่ยน return เป็น
return $ls ?: array();



ส่วนข้อเสียของการไม่ยอมให้อ้างตัวแปรนอก scope ได้นั้น นอกจากความหงุดหงิดแล้ว ก็มาจากวิวัฒนาการของภาษาสมัยนี้ที่พยายามทำให้เป็น OOP มากขึ้น ทุกวันนี้มันคงไม่แปลกที่จะเขียน
$db = new PDO('mysql: ...');
$res = $db->prepare('SELECT * FROM blah_blah_blah WHERE answer = ?');
$res->execute(array(42));
ในความจริงแล้ว ขั้นตอน prepare/execute มักถูกเขียนในส่วนอื่นๆ ไม่เอาไว้ติดกันเช่นนี้ ยิ่งไปกว่านั้นมันมักโดน refactor ไว้ในฟังก์ชันเพื่อจัดระเบียบให้อ่านง่ายด้วย ในเมื่อตัวแปร $db ที่ควรเป็น global ดันไม่สามารถเรียกใช้ได้ง่ายๆ ใครมันจะอยากเขียนแบบ OOP กันหละ?

ทางออกโดยทั่วไปก็คือสร้าง model ที่เป็น interface สำหรับ query ทั้งหมดให้ทำผ่านตัวมัน แล้วตอนสร้าง model ก็ bind ตัวแปร $db เข้าไป ก็คือเราสามารถเข้าถึง database ได้โดย $this->db ซึ่งท่านี้ก็ยังรุงรังเหมือนเดิม แถมตอนเรียกใช้ก็ยังต้องพิมพ์ยาวขึ้นอีกด้วย

ท่าที่ผมชอบมากกว่าเป็นของ Laravel ที่สร้าง class เชื่อมต่อ database นั้นๆ ไว้ให้เลย ทำให้เวลาจะทำ query ก็เพียงแค่
$res = DB::select('SELECT * FROM blah_blah_blah WHERE answer = ?', array(42));
เพราะว่า class และฟังก์ชันใน PHP สามารถเรียกใช้จาก scope ไหนๆ ก็ได้ครับ



ข้อดีนิดเดียว (แถมยังไม่แน่ว่ามันเป็นข้อดีจริงๆ หรือเปล่า) แต่ข้อเสียบานเลย รู้งี้แล้วยังเขียน PHP กันอยู่อีกรึ :P

Feb 14, 2014

ประสบการณ์สร้างเกมจริงจังครั้งแรก

ไอเดียมันเริ่มมาจากทวีตนี้


(ว่าแต่ใครมันจุดกระแส #ถีบเนยสด ฟระ Orz)

เลยจัดการซะคืนนั้นเลย ... ถ้าไล่ดูตาม commit log จะเห็นว่าใช้เวลาไป 2 เดือนพอดีจนปิดโปรเจคได้ ก็นับว่าใช้เวลาเยอะโขอยู่กับโปรเจคขำๆ

ทำให้รู้เลยว่า ส่วนที่ยากที่สุดของการทำเกมนั้น ไม่ใช่การ coding ไม่ใช่การ debug แต่เป็นการออกแบบ game play ให้น่าสนใจ ผู้เล่นต้องรู้สึกว่าไม่ยากเกินความสามารถ ในขณะเดียวกัน เมื่อเล่นจบแล้วก็ยังกลับมาเล่นซ้ำๆ ได้อีกโดยไม่เบื่อ

ซึ่งเวลาส่วนใหญ่ที่หมดไปก็เพราะเจ้าเนี่ยแหละ จริงๆ ถ้าดูตาม log อย่างละเอียดแล้วลองคำนวณเวลาที่ใช้ จะพบว่าโปรเจคนี้ทำเสร็จภายใน 42 ชั่วโมง (บวกลบไม่เกิน 10%) ถ้ามีไอเดียเจ๋งๆ เกี่ยวกับ game play เตรียมไว้อยู่แล้ว

แต่ก่อนที่จะไปดูการออกแบบเกมนี้ ลองเรียนรู้จากเกมอื่นๆ ก่อน



Flappy Bird

  • infinity - เล่นได้เรื่อยๆ จนกว่าจะพลาดหรือเบื่อ
  • social - อวดคะแนนแข่งเพื่อน
  • reflex - ตอบสนองทันทีต่อสิ่งใหม่
(เกาะกระแสซักหน่อยครับ) เกมนี้ใช้ระบบที่เรียบง่ายมากๆ คือพานกบินลอดท่อไปเรื่อยๆ ลอดผ่านได้ 1 ท่อก็เพิ่ม 1 คะแนน อย่างไรก็ตามเกมประเภทนี้จะสุ่มด่านใหม่มาให้เสมอ ผู้เล่นแต่ละคน (หรือแม้แต่คนเดียวกันเมื่อเล่นรอบใหม่) ก็จะพบกับด่านที่ไม่เหมือนเดิม การเพิ่มคะแนนแบบไม่เกี่ยวข้องกับความยากของด่านจึงเหมาะสม เพราะจะกดดันให้เล่นเกมซ้ำๆ เผื่อฟลุ๊คเจอด่านง่ายแล้วได้คะแนนเยอะโดยออกแรงไม่มากไปกว่าเดิมนัก ซึ่งการเล่นซ้ำๆ (ด้วยความโมโหว่าทำไมคะแนนไม่ขึ้นซักที 555) จะย้อนกลับมาช่วยให้เราเรียนรู้ระบบเกมและฝึกประสาทตอบสนอง ส่งผลให้ผู้เล่นได้คะแนนง่ายขึ้นเรื่อยๆ ตามเวลาที่ฝึก ยิ่งผสมกับการนับคะแนนตรงไปตรงมา (1 ท่อ = 1 คะแนน) ยิ่งทำให้เรากะถูกว่าต้องใช้ความพยายามเพิ่มอีกแค่ไหนเพื่อจะทำคะแนนให้ชนะเพื่อนได้

Super Hexagon

  • goal set - เกมตั้งจุดมุ่งหมายให้แล้ว
  • social - แต่จะแข่งกับเพื่อนก็ได้
  • reflex - ตอบสนองทันทีต่อสิ่งใหม่
แนวคิดของเกมนี้ก็ง่ายเช่นเดียวกัน คือพาเจ้าตัวสามเหลี่ยมวิ่งวนคอยหลบไม่ให้โดนกำแพงอัดแบน และใช้ "เวลาที่อยู่รอด" เป็นคะแนนนั่นเอง งานนี้ไม่ต้องคิดมากว่าจะเลือกท่าหลบให้ให้สวยงามได้คะแนนโบนัสเพิ่มหรือเปล่า เช่นเดียวกับ Flappy Bird คือต้องฝึกเยอะๆ ฝึกไปเรื่อยๆ และแม้ว่าจะกำหนดเวลาผ่านด่านไว้ที่ 60 วินาที แต่ถ้าจะทำคะแนนไว้อวดเพื่อน ก็สามารถเล่นต่อหลัง 60 วินาทีนั้นไปได้เรื่อยๆ ครับ

Jubeat

  • goal set - เกมตั้งจุดมุ่งหมายให้แล้ว
  • social - แต่จะแข่งกับเพื่อนก็ได้
  • rhythm - เกมเข้าจังหวะ
Jubeat เป็นเกมแนวดนตรีที่จะให้กดปุ่ม 16 ปุ่ม (บน grid ขนาด 4x4) ตามจังหวะของแต่ละปุ่ม แต่ละเพลงมีรูปแบบที่ตายตัว แถมเกมยัง cap แต้มไว้ที่หนึ่งล้านแต้มเสมอไม่ว่าจะเป็นเพลงยากหรือง่าย ดังนั้นรูปแบบการฝึกจะไม่เหมือน 2 เกมข้างบนเท่าไหร่ เช่นฝึกซ้ำบ่อยๆ เฉพาะรูปแบบที่ยาก ไปจนถึงการจำรูปแบบจังหวะของเพลงทั้งเพลง เกมแบบนี้ต้องให้คนรักในความ perfect จริงๆ มาเล่น ไม่งั้นคงรู้สึกเหมือนฟินไม่สุด

The Typing of the Dead

  • goal set - เกมตั้งจุดมุ่งหมายให้แล้ว
  • co-op - ร่วมมือกันเล่นได้
  • reflex - ตอบสนองทันทีต่อสิ่งใหม่
จริงๆ เกมนี้จะนับเป็นแนวกึ่ง rhythm กึ่ง reflex ก็ได้ เพราะเราไม่ได้สนใจแค่ตัวอักษรที่โผล่มาให้พิมพ์เพียงอย่างเดียว ในบางจังหวะเราสามารถจำว่าต้องยิงซอมบี้ตัวไหนก่อน เพื่อในกลับมาเล่นด่านเดิมด้วยคะแนนที่สูงขึ้นได้ จุดเด่นอีกอย่างในเกมแบบนี้คือความสามารถในการร่วมมือกันเล่น ที่ผู้เล่นต้องสมดุลระหว่างการยิงรัวๆ กันโดนซอมบี้กัดตาย กับการเหลือซอมบี้ไว้ให้เพื่อนร่วมทีมยิงเพื่อให้คะแนนไม่ห่างกันนักด้วย (หรือใครเป็นพวกซาดิสม์ ชอบทำคะแนนให้ห่างกันเยอะๆ ก็ไม่ว่ากัน 55+)

Diablo II

  • goal set - เกมตั้งจุดมุ่งหมายให้แล้ว
  • co-op - ร่วมมือกันเล่นได้
  • leveling - ตัวละคร/ด่านมีพัฒนาการ
ผมจำได้เลยว่าตอนอยู่ม.ต้นเล่น Diablo II ก็รู้สึกหนุกดีนะ เดินผ่านด่านไปเรื่อยๆ ไม่คิดอะไรมาก ถ้าตรงไหนดูยากก็กลับไปเล่นด่านเก่าอัพเลเวลซักหน่อย เพื่อที่จะได้ใช้ท่าใหม่ๆ ตีมอนได้แรงขึ้น เล่นไปเล่นมาก็รู้สึกว่า เอ๊ะทำไมเหมือนฟาร์มเลเวลได้ช้าลง ค่าประสบการณ์สำหรับเลเวลถัดไปที่เพิ่มขึ้นเร็วเป็นเอกซ์โพเนนเชียลก็ยังไม่น่าทำให้รู้สึกได้ขนาดนี้ จนเมื่อได้อ่านสูตรการคิดค่าประสบการณ์ที่ได้จากมอนตัวนึงแล้วถึงบางอ้อ ความต่างของเลเวลมอนกับผู้เล่นก็มีผลต่อค่าประสบการณ์ด้วย นั่นทำให้ผมรู้สึกประทับใจในรายละเอียดและความเชื่อมโยงกันของค่าต่างๆ ภายในเกม ไม่ใช่การที่ developer สักแต่คิดว่าใส่ factor นี้เข้ามาแล้วเกมมันน่าจะสนุกขึ้น ซึ่งในความเป็นจริงเกมมันอาจจะสนุกขึ้น 10 นาที แต่หลังจากนั้นก็กลายเป็นหายนะเพราะเสียสมดุลไปเรียบร้อย



จริงๆ ตอนเอาไอเดียมาแปลงเป็น game play ก็ไม่ได้ไล่คิดถึงเกมอื่นเยอะขนาดนี้หรอก (เอ หรือมันจะเป็น unconscious?) แต่อยากได้แบบ infinity+leveling และคิดว่าต้องปรับแน่ๆ ดังนี้

  • เกมฝึกพิมพ์ภาษาไทยยากมาก ทั้ง font เล็ก ทั้งสระบนล่าง ... ทำภาษาอังกฤษง่ายกว่า
  • 1 นาทีจะว่านานก็นานเกินไปสำหรับเกมฝึกพิมพ์ ยิ่งเจอแต่คำซ้ำๆ นี่ยิ่งน่าเบื่อ
  • แต่ 1 นาทีก็สั้นเกินไปถ้า random คำมาได้ไม่ซ้ำกันเลย
  • เลยคิดว่าทำเป็นระบบหัวใจดีกว่า พิมพ์ไม่ทันก็โดนโจมตี หัวใจหมดเมื่อไหร่ก็จบเกม
  • ถึงแม้ส่วนกริยาจะไม่ซ้ำจากการ random แล้ว แต่ให้พิมพ์ชื่อ (@neizod) บ่อยๆ ก็เบื่อได้เช่นกัน เลยทำระบบเก็บคำกริยาหลายๆ คำไว้ก่อน แล้วค่อยพิมพ์ชื่อเมื่อต้องการจบประโยค
  • อนึ่ง ในความเป็นจริงเกมก็ไม่ได้มี word list ใหญ่ขนาดนั้น เลยหาแผนมารับมือเวลาเจอคำซ้ำไม่ให้รู้สึกเบื่อ คือเมื่อพิมพ์ศัพท์ซ้ำจะทำการดึงคำนั้นออกจากประโยคที่กำลังสร้างซะเลย

ตอนนี้ก็ได้ game play หลักมาครบแล้ว ซึ่งเอาจริงๆ prototype พวกนี้ให้ทำ 3-4 วันก็เสร็จ

แต่ก่อนหน้านั้นก็มีสิ่งที่สำคัญไม่แพ้กัน คือการเลือก engine สำหรับโปรเจคนี้ครับ

  • เลือกเป็นเว็บเพราะทำ GUI ได้ง่ายและเร็วสุด (จริงๆ คือเป็นอยู่แค่เว็บ 555+)
  • ตอนแรกก็ว่าจะใช้ canvas เพราะทุกอย่างสามารถยัดเป็น JavaScript ล้วนเลยได้ แต่ก็จะไม่ได้ฝึก CSS อีกทั้งเกมนี้ยังเป็นแบบ text-base ไม่ใช่เกมตัวละครลุยด่าน เลยคิดว่าเล่นกับ DOM ดีกว่า
  • จะเล่นกับ DOM ทั้งที ก็ต้อง jQuery และ position: absolute สิครัช
  • ถ้า JavaScript เขียนสั้นๆ ก็พอไหว แต่มาเขียนซับซ้อนๆ แบบนี้ CoffeeScript เถอะครัฟ จะได้ไม่เป็นภาระ @plynoi
  • ไม่ได้ใช้ framework อื่นใดช่วยอีกเพราะไม่ค่อยรู้จัก + learning curve ท่าทางจะสูงน่าดู รอลุยโปรเจคนี้เสร็จพลังวัตรน่าจะกระโดดข้ามไปอีกขั้น
  • ไม่อยากแตะ database เท่าไหร่เพราะต้องใช้ host แยก ไม่งั้นก็ไปขอ AppEngine ที่ตอนนี้เก็บครบโควต้าแล้ว
  • เลยกะว่าเอาง่าย เล่นเกมเสร็จก็ทวีตคะแนนซะเลย ใครจะโกงก็ช่างมัน (แต่ประสบการณ์ก็บอกว่าไม่ค่อยมีใครโกงหรอก เท่าที่สังเกตจาก Temple Run)
  • พอไม่แตะ database ทุกอย่างเป็น static หมด ก็หา host ง่ายขึ้นจม แต่มีที่เดียวที่ผุดมาในใจคือ GitHub Pages

พอรู้ spec env ครบก็สบายใจไปกว่าครึ่งแล้ว

จุดต่อมาคือระบบคิดคะแนนต่างๆ (ที่อู้ไปเป็นเดือนเพราะคิดส่วนนี้ไม่ออก) การจะออกแบบตรงนี้ได้ถ้ารู้จักแนวโน้มกราฟแบบต่างๆ จะได้เปรียบพอสมควร ผลสุดท้ายผมออกแบบดังนี้

  • ค่าหลักของเกมคือคะแนน ซึ่งควรคิดตามจำนวนตัวอักษรต่อคำที่พิมพ์เข้าไป เช่น kick ได้ 4 คะแนน เพราะมี 4 ตัวอักษร
  • อย่างไรก็ตาม การคิดคะแนนที่ 1 ตัวอักษร = 1 คะแนน อาจทำให้เบื่อได้ และยังไม่ได้ใช้ประโยชน์จากการบังคับพิมพ์ชื่อ (@neizod) เท่าไหร่ด้วย
  • คิดว่าอีกปัจจัยที่ควรจะมีผลต่อคะแนนคือเลเวล (เหมือนเกมอื่นๆ) ยิ่งผู้เล่นเลเวลสูงก็น่าจะได้คะแนนเยอะ เลยเอาคะแนนที่ได้ไปคูณด้วยเลเวลเมื่อพิมพ์จบประโยค
  • แต่ก็ไม่อยากให้ผู้เล่นเก็บคำศัพท์ไว้เป็น 20 คำแล้วค่อยจบประโยคเพื่อคูณคะแนนตูมเดียว เลยให้แค่คำแรกของประโยคที่ได้คูณเลเวลเต็มๆ คำถัดๆ มาก็คูณกับค่าเลเวลที่ลดลงเรื่อยๆ จนคำท้ายๆ ไม่ได้ประโยชน์จากการเก็บดองคำศัพท์ไว้ไม่ยอมสร้างเป็นประโยคซักที
  • ส่วนการเพิ่มเลเวลนั้นก็คิดจากคะแนน ตรงนี้ไม่อยากให้คะแนนต่อเลเวลเป็นกราฟเส้นตรง เพราะจะทำให้ตัวคูณเพิ่มเร็วมาก และเนื่อจากไม่ค่อยชอบกราฟกำลังสอง เลยเลือกค่าคะแนนสำหรับเพิ่มเลเวลต่อไปเป็นแบบเอกซ์โพเนนเชียลซะ
  • หัวใจลดได้ก็ย่อมเพิ่มได้ ง่ายสุดไม่คิดอะไรแล้ว เลเวลเพิ่มเมื่อไหร่ หัวใจเพิ่มเมื่อนั้น (เชื่อว่าระบบการเพิ่มเลเวลถูกออกแบบมาเป็นอย่างดี 555+)
  • แต่แน่นอนว่าเพิ่มเลเวล = เพิ่มความยาก ในที่นี้ก็คือเพิ่มจำนวนคำศัพท์บนหน้าจอให้ออกมาเท่ากับเลเวล และเพิ่มความเร็วให้มันทีละนิดๆ

ถ้าแม่น math หน่อย ระบบพวกนี้ implement เข้าไปไม่นานครับ ดีไม่ดีเร็วกว่าออก prototype ข้างบนอีก ปัญหาคือจะทำได้เร็วๆ นี่ต้องไอเดียบรรเจิดมาก ควรหา paper ด้านจิตวิทยาและการออกแบบเกมอ่านตุนไว้เป็นอย่างยิ่ง :p

พอ core ทั้งหลายเสร็จแล้ว ก็แต่งสวยครับ

  • จริงๆ ก็อยากให้เจ้าแมวสดโดนชก/ถีบหน้าน่วมนะ แต่มันทำยากซะเหลือเกิน เลยเลือกๆ อารมณ์มาตรฐานที่เว็บมันมีให้มาให้แทน ได้มา 8-9 แบบก็น่าจะพอ
  • พื้นหลังนี้ก็เลือกนานเหมือนกัน คิดไว้ว่าอยากได้โทนสว่างๆ ก็เลือกเหลืองอ่อน เลยได้น้ำเงินอ่อนติดมาด้วยในตอนแรก (สีคู่ตรงข้ามกันพอดี) ทำไปทำมาได้สีแดงตอนจบเกมมาด้วย เลยต้องเลื่อนสีน้ำเงินให้ออกไปฟ้าๆ หน่อย โทนสีจะได้เป็นรูปสามเหลี่ยม
  • ตอนแรกไม่ได้คิดว่าจะเอาเจ้าแมวสดมาไว้ด้านล่าง แต่จะเอาไว้เป็น background เลย ซึ่งคิดไปคิดมาก็คงไม่เหมาะเพราะรบกวนสมาธิผู้เล่นเกินไป เลยย้ายมาอยู่มุมล่างขวา ด้านที่คำศัพท์ต่างๆ พรั่งพรูออกมานั่นเอง
  • ทำให้พวก status ผู้เล่นอย่างคะแนนสะสม/เลเวล/หัวใจที่ตอนแรกวางไว้ว่าจะให้กระจัดกระจายตามขอบต่างๆ ของจอ ถูกจับมาอัดรวมกันไว้เป็นกล่องทางด้านซ้ายในสไตล์เดียวกัน
  • ที่เหลือก็จับปุ่มโน่นนี่นั่นยัดเข้ามาไม่ให้มันดูโล่ง
  • สุดท้ายคือ tutorial เกม หัวใจสำคัญที่ขาดไม่ได้แม้เกมจะง่ายแค่ไหน ใครจะไม่ดูก็ช่างเขาแต่เราจะพรีเซนต์!

หลังจาก implement เสร็จหมดก็ได้ฤกษ์ปล่อยเกม ตามไปเล่น (ด้วยความรัก) ได้ที่ neizod.github.io/kick เลยครับ \(;w ;)/

Feb 7, 2014

พื้นที่สำหรับความผิดพลาด

วันก่อนเห็นทวีตนี้



จะบอกว่าไม่เห็นด้วยก็คงจะพูดได้ไม่เต็มปาก เพราะการตีความ "อำนาจ" ในการ "ทำอะไรก็ได้" ของเด็กและผู้ใหญ่นั้นต่างกันไกลลิบ

เมื่อเป็นเด็กเราต้องการความสามารถในการทำอะไรก็ได้ โดยไม่ต้องกังวลถึงทรัพยากร ไม่ว่าจะเป็นความรู้ความสามารถ หรือทรัพย์สินเงินทอง

หลายคนโชคดีหน่อย เกิดมาในตระกูลเศรษฐี ก็มีของเล่นได้หลากหลายชิ้นกว่าเพื่อนๆ

หลายคนขยันหน่อย มีความสามารถโดดเด่น ก็มีชื่อเสียงได้รับการยอมรับจากเพื่อนๆ

แต่ส่วนมากที่ไม่ได้โชคดีอย่างนั้น ก็ย่อมได้แต่ไผ่ฝันในสิ่งที่อยากทำ โดยคิดว่าพอโตเป็นผู้ใหญ่แล้วอุปสรรคต่างๆ คงหายไป ไม่ว่าจะด้วยความรู้ความสามารถที่มากขึ้นตามกาลเวลา หรือด้วยทุนทรัพย์ที่สามารถหามาได้อย่างไม่ยากเย็น

โดยที่ไม่ได้คิดถึงสิ่งที่ต้องแลกมา คือพื้นที่สำหรับความผิดพลาด

ตอนเด็กที่เราเล่นบอลกับเพื่อน จะเตะบอลหลุดนอกกรอบซักกี่ครั้งก็ได้ แต่ถ้าโตมาเป็นนักฟุตบอลอาชีพแล้ว แค่เตะบอลหลุดนอกกรอบเพียงครั้งก็อาจทำให้อนาคตในเส้นทางนี้ดับลงได้

...

จริงๆ จะเทียบอย่างนี้ทั้งหมดก็คงไม่ถูกเท่าไหร่ เพราะถ้าตอนเด็กดันเตะบอลหลุดนอกกรอบในนัดที่มีแมวมองมาคัดตัว ก็อาจทำให้เราพลาดโอกาสที่จะอยู่บนเส้นทางนักฟุตบอลอาชีพได้เช่นกัน

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

เพราะเราไม่รู้ว่าจะได้มีโอกาสผิดพลาดโดยไม่เจ็บตัวอีกเมื่อไหร่

Feb 1, 2014

อำนาจ

Power tends to corrupt, and absolute power corrupts absolutely. Great men are almost always bad men.

-- Lord Acton

ถ้าเริ่มรู้สึกว่าตัวเองมีอำนาจมากเหลือล้น สามารถตัดสินใจแทนคนอื่นได้ เมื่อจะลงมือทำอะไรก็ไม่มีใครขัดใจ พึงระวังไว้ให้ดี

หึหึหึ

Jan 5, 2014

0!=1

สัญลักษณ์คณิตศาสตร์ที่น่าฉงนปนตกใจมากที่สุดคงหนีไม่พ้น factorial (!) และสิ่งที่น่าตกใจยิ่งกว่าคือคำตอบที่ว่า 0!=1 เมื่อถามต่อว่าทำไมก็จะได้รับคำตอบว่ามันเป็นนิยาม ซึ่งจริงๆ ก็ถูกต้องแล้วแหละ แต่หลายคนคงอยากให้มันมีความหมายที่ลึกซึ้งกว่านั้น

เมื่อลองพิจณานิยามพื้นฐานสุดของ factorial การจะได้มาซึ่ง n! นั้นคือการหาผลคูณจาก 1 ขึ้นไปเรื่อยๆ จนถึง n การหา 1! ได้จึงไม่แปลก เพราะมันคือผลคูณจาก 1 ไปจนถึง 1 (ซึ่งก็คือไม่ต้องคูณเลยซักตัว) แต่ถ้าคิดตามนิยามนี้ 0! ก็ไม่มีตัวตนเพราะว่า 1 มีค่ามากกว่า 0 (แล้วเราจะคูณขึ้นไปเรื่อยๆ ได้ยังไง!?) ตอนนี้ถ้าเราอยากให้ 0! มีค่าก็ต้องหาวิธีคำนวณ/นิยายามที่มีเหตุผลขึ้นมาเพื่อให้คนอื่นยอมรับค่านั้นๆ ... ซึ่งแน่นอนว่ามันไม่ใช่การเปลี่ยนนิยามข้างต้นเป็น n! คือผลคูณจาก 0 ขึ้นไปเรื่อยๆ จนถึง n แน่ :P



ความพยายามแรกที่จะหาคำตอบนี้ ก็คือการกล่าวถึง factorial ในรูปของ n!=n(n-1)! จะเห็นว่าสูตรนี้เป็น recursive การจะหาผลลัพท์ได้นั้น ต้องมองย้อนกลับไป 1 ขั้นเสมอ ในทำนองเดียวกัน ถ้าเรารู้ผลลัพท์ของขั้นตอนปัจจุบัน เราอาจจะสามารถย้อนกลับไปคำนวณผลลัพท์สำหรับขั้นตอนก่อนหน้าได้ ดังนั้น
5! = 120 = 5 * 4!
4! =  24 = 4 * 3!
3! =   6 = 3 * 2!
2! =   2 = 2 * 1!
1! =   1 = 1 * 0!
เลขตัวเดียวที่คูณหนึ่งแล้วยังได้หนึ่งก็คือหนึ่ง ดังนั้น 0!=1 อย่างไม่ต้องสงสัย...

ใครตอบมาอย่างนี้บอกได้เลยว่าเป็นนักคณิตศาสตร์เกรด C เพราะถ้าเราลองทำต่อไปจะได้ว่า
0! = 1 = 0 * (-1)!
แน่นอนว่าสมการนี้หาคำตอบไม่ได้ (เลขใดๆ คูณศูนย์ย่อมได้ศูนย์) แม้ว่าสมการข้างต้นจะทำงานได้ดีไม่มีผิดถ้าหาก n เป็นเลขจำนวนเต็มบวก แต่ก็ทำให้เกิดข้อสงสัยว่า ในเมื่อ 0! ไม่สามารถคำนวนมาได้จากฝั่ง 0!=0*(-1)! แล้วเราจะแน่ใจได้หรือ ว่าสมการข้างต้นนี้สามารถใช้ได้ตั้งแต่ n=1 จริงๆ (เพราะตอนนี้เรามั่นใจแค่ว่ามันใช้ได้ตั้งแต่ n=2)



ความพยายามต่อมาคือ gamma function ที่ให้สมการอินทิเกรตยากๆ มาอันนึง (ซึ่งแก้ออกได้ด้วยการทำอินทิเกรตแยกส่วน แล้วลดรูปกลับมาให้อยู่ในรูปเดิม) สมการนี้เมื่อหาค่าทุกจุดแล้วนำมาพล็อตกราฟ จะได้ภาพการลากเส้นโค้งเชื่อมจุดต่างๆ ของ factorial นั่นเอง กราฟนี้บอกเราว่า 0!=1 และ factorial ของจำนวนเต็มลบใดๆ หาค่าไม่ได้ ซึ่งไม่ขัดแย้งกับความพยายามก่อนหน้า


แม้ว่าความพยายามนี้จะเลิศเลอเพอร์เฟคแค่ไหนก็ตาม (ทั้งสมการยากๆ ที่ทำให้ดูเป็นมือโปร ทั้งการขยายขอบเขต factorial ให้ไปอยู่บนจำนวนจริงได้) แต่มันก็เกิดขึ้นหลังจากที่เรานิยามได้แล้วว่า 0!=1 ครับ



ความพยายามสุดท้าย คือการกลับไปยังต้นกำเนิดของการที่เราอยากนิยาม factorial มาใช้เพื่อความสะดวกในการเขียนและคำนวณ นั่นก็คือเรื่องของโอกาสและความน่าจะเป็น เช่นตัวอย่างที่ว่า ถ้ามีนักเรียน 5 คน จะจัดให้ยืนเรียงต่อแถวกันได้ทั้งหมดกี่แบบ เราก็จะตอบได้ว่า คนแรกเลือกที่ยืนได้ 5 ที่ คูณกับคนที่สองเลือกยืนได้ 4 ที่ (เพราะคนแรกจองไปที่นึงแล้ว) คูณกับคนต่อมามีที่ยืนแค่ 3 ที่ (สองคนแรกจองไปสองที่) ... เมื่อคิดอย่างนี้ต่อไปเรื่อยๆ เราจะเห็นได้ว่ามันย้อนกลับไปหานิยามตั้งต้นของ factorial นั่นเอง

แล้วถ้านักเรียนเหลือแค่คนเดียวหละ? เราก็จะได้ว่ามีวิธีเดียวที่จะยืน คือยืนอยู่คนเดียวให้เปล่าเปลี่ยวหัวใจเล่น ไม่มีใครมายืนนำหน้าหรือต่อแถวข้างหลัง

ส่วนที่ยากก็คือว่าถ้าไม่มีนักเรียนเหลือซักคนเลย?? คำตอบนี้อาจดูแปลก เพราะมันคือการลอกคำตอบข้างบนมาตอบในทำนองเดียวกันว่ามีวิธีเดียวที่จะยืน คือไม่ต้องมีนักเรียนยืนเลยซักคนนั่นเอง

ความพยายามนี้แม้แลดูเป็นปรัชญาอันสูงส่งเข้าใจยาก (ที่พยายามบอกเป็นนัยเชิงปฏิทรรศน์ว่า ความว่างเปล่าแท้จริงแล้วก็ยังไม่ว่าง) มันนับว่าเป็นการอธิบายที่ถูกต้องตรงจุดที่สุดในบรรดาความพยายามอธิบายที่ผ่านๆ มาเลยทีเดียว



ส่วนเรื่องจริงนั้นก็อย่างที่บอกแต่แรกสุดแล้ว ว่า 0!=1 มันเป็นนิยามนั่นแหละ

เหตุผลก็ไม่พิศดารอะไรเลย แค่การนิยามแบบนี้แล้วจะทำให้เขียนสมการง่ายขึ้นเท่านั้น ลองคิดดูว่าถ้าเราไม่นิยามแบบนี้จะเกิดอะไรขึ้น
  • สัมประสิทธิ์ทวินาม หรือที่หลายๆ คนคุ้นเคยในรูปของสามเหลี่ยมปาสคาลมากกว่า มันมีสูตรว่า
    (x+y)^n = sigma C(n,k) x^(n-k)y^k for k from 0 to n
    
    ซึ่งถ้าไม่ได้นิยาม factorial ตามข้างต้น ก็ต้องเปลี่ยนสูตรนี้ใหม่เป็น
    (x+y)^n = x^k + (sigma C(n,k) x^(n-k)y^k for k from 1 to n-1) + y^k
    
  • นิยาม permutation กับ combination ต้องกลับไปเขียนข้อยกเว้นไว้ที่
    • C(n,0) (เลือกของ 0 ชิ้นจากของ n ชิ้นได้กี่วิธี) ซึ่งก็ยังเข้าใจได้เพราะการไม่เลือกของซักชิ้นเลยอาจเป็นข้อถกเถียง
    • C(n,n) (เลือกของ n ชิ้นจากของ n ชิ้นได้กี่วิธี) อันนี้เริ่มแปลกๆ แล้ว ทำไมเลือกของทุกชิ้นต้องมีข้อยกเว้นด้วยหละ?
  • การหาค่า e จากการกระจายเทย์เลอร์ จากเดิมที่เราสามารถสรุปสั้นๆ อย่างสวยงามได้ว่ามันคือ
    e = sigma 1/k! for k from 0 to infinity
    
    ก็จะกลายเป็นสมการหน้าเกลียดๆ ที่ไม่สามารถอธิบายได้ว่าเลข 1 ตัวแรกนั้นโผล่มาจากไหน/โผล่มาทำไม เช่นนี้
    e = 1 + (sigma 1/k! for k from 1 to infinity)
    
เพราะคณิตศาสตร์คือความสวยงามครับ :)

Jan 1, 2014

ปี 2013 ของฉัน

ปีที่แล้วไม่เป็นชิ้นเป็นอันที่สุด โปรเจกต์ทำเล่นหลายอันปิดไม่ลง แล้วระหว่างทำก็ดันคิดโปรเจกต์ใหม่ๆ ออกอยู่ร่ำไป โฮฮฮฮ

  • ทำ Debian Package สำเร็จจนได้ หลังจากงมมานาน ฝากไว้ที่ ppa:neizod/ppa ทำขำๆ ไม่รู้จะได้กลับไปดูแลหรือเปล่า :\
  • เรียนต่อมอเกษตร ตั้งใจมาฝากตัวเป็นลูกศิษย์อาจารย์ @jittat สอบเข้าไม่ยากเท่าไหร่ สอบจบต้องรอดูกันต่อไป 55+
  • เป็นวิทยากรสอน Python ท้าทายดี ขนาดแค่กลุ่มเล็กที่เหมือนจะดูแลได้ทั่วถึง ยังรู้สึกว่าทำได้ไม่ค่อยดีเท่าไหร่เลย
  • ซื้อ ThinkPad เอง เก็บตังค์จากข้อข้างบนนั่นแหละ
  • ลดน้ำหนักได้ 7 Kg ถือว่าน้อยถ้าเทียบทั้งปี แต่ก็ถือเป็นการเริ่มต้นที่ดี (มั้ง) กับคนที่เคยมีแต่น้ำหนักขึ้นมาโดยตลอด Orz
  • เขียนบล๊อกเดือนละครั้ง แม้เฉลี่ยแล้วจะเกิน แต่ก็ถือว่าไม่สำเร็จเพราะบางเดือนก็หายไปดื้อๆ ขี้เกียจตัวเดียวเลย TwT

achievement ไม่ค่อยแปลกเท่าไหร่ กินพิซซ่าข้ามปี 555+

Nov 10, 2013

Division ใน Relational Algebra

การหาร (DIVISION) ใน relational algebra อธิบายง่ายๆ คือการเอาตารางมา join กันแล้วนับว่าแต่ละ pk ของตารางตัวตั้ง มี row เท่ากับตารางตัวหารหรือไม่ ถ้าครบก็ตัด column ที่ปรากฏในตารางตัวหารทิ้งแล้วยุบรวม row ซ้ำด้วย pk เช่นเรามีข้อมูล
table: students
------+------+-------
 name | subj | grade 
------+------+-------
 bar  | code | A
 foo  | code | C+
 foo  | trek | A
 qux  | code | B+
 qux  | math | A
 qux  | trek | A
------+------+-------

table: colleges
-----------+------
  campus   | subj 
-----------+------
 leetademy | code
 leetademy | trek
 hack-cool | code
 hack-cool | math
-----------+------
คำสั่ง SQL ของการหารก็จะมีหน้าตาประมาณ
SELECT *
FROM   students
DIVISION (
    SELECT subj
    FROM   colleges
    WHERE  campus = 'leetademy'
) AS leetademy_courses
ซึ่งให้ผลลัพท์ออกมาคือ
------
 name 
------
 foo
 qux
------



ปัญหาของการหารคือ DBA แทบทั้งหมดไม่มีคำสั่งนี้ให้ใช้ (ต่างจากการลบ (MINUS) ที่ยังมีบ้าง แม้บางเจ้าจะเปลี่ยนชื่อเป็น EXCEPT ก็ตาม) เพราะมันเป็นท่าที่เปลืองทรัพยากรมาก แถมไม่ได้ใช้กันบ่อยๆ เสียด้วย ...ซึ่งจะว่าไปแล้วมันก็ดูไม่ใช่ปัญหาเลยนี่หน่า

อย่างไรก็ตามปัญหาจริงๆ คือมันเป็นท่าที่อาจารย์ database ส่วนใหญ่ชอบเอาไปออกสอบ ทำให้เราต้องหาท่าอ้อมๆ ที่ให้ผลลัพท์แบบเดียวกันมาใช้แทน

การจะเข้าใจมันได้ ถ้าคิดแบบ programming ก็แค่ดูว่า set ที่เป็นตัวตั้งหารนั้นเป็น superset ของ set ที่ถูกหารหรือเปล่า
students_set = defaultdict(set)
for name, subj, grade in students:
    students_set[name].add(subj)

colleges_set = defaultdict(set)
for campus, subj in colleges:
    colleges_set[campus].add(subj)

output = set()
for name, subj_set in students_set.items():
    if subj_set >= colleges_set['leetademy']:
        output.add(name)
print(output)
แต่การทำงานบน database เรามักเขียนพวกนี้ทั้งหมดให้เป็น one-liner เพื่อที่จะรับประกันว่าระหว่างการ query จะไม่เกิด race condition กับ query อื่นๆ สิ่งที่ต้องทำก่อนอื่นใดคือพยายามกำจัดการสร้าง set ของข้อมูลล่วงหน้าซะ

ในที่นี้ ขั้นแรกเราจะกำจัดการสร้าง colleges_set โดยวน loop ใน colleges ทุกตัวดูว่าแต่ละ subj มีใน students_set หรือเปล่า
def learnt_all(name):
    for campus, subj in colleges:
        if campus == 'leetademy':
            if subj not in students_set[name]:
                return False
    return True

output = set()
for name, _, _ in students:
    if learnt_all(name):
        output.add(name)
print(output)
ขั้นต่อมาก็กำจัด students_set โดย loop เข้า students ดูว่านักเรียนที่ชื่อ name ได้เรียนวิชา subj แล้วหรือยัง
def learnt(name, subj):
    for student_name, student_subj, _ in students:
        if student_name == name and student_subj == subj:
            return False
    return True

def learnt_all(name):
    for campus, subj in colleges:
        if campus == 'leetademy':
            if learnt(name, subj):
                return False
    return True
    
output = set()
for name, _, _ in students:
    if learnt_all(name):
        output.add(name)
print(output)
เนื่องจากในฟังก์ชัน learnt กับ learnt_all จะคืนค่า False ทันทีที่เข้าเงื่อนไขครบ แต่จะคืนค่า True หลังจากจบ loop เท่านั้น เราสามารถเขียนตรงนี้ใหม่ด้วย generator แล้ว wrap ด้านนอกด้วย not any(...) เช่นนี้
def inverse_learnt(name, subj):
    for student_name, student_subj, _ in students:
        if student_name == name and student_subj == subj:
            yield True

def inverse_learnt_all(name):
    for campus, subj in colleges:
        if campus == 'leetademy':
            if not any(inverse_learnt(name, subj)):
                yield True

output = set()
for name, _, _ in students:
    if not any(inverse_learnt_all(name)):
        output.add(name)
print(output)
พอทุกอย่างเป็น single-statement และ generator คราวนี้ก็ง่ายที่จะทำ one-liner แล้ว ... จับมาเขียนสวยๆ ได้แบบนี้
print({
    name
    for name, _, _ in students
    if not any(
        True
        for campus, subj in colleges
        if campus == 'leetademy'
        and not any(
            True
            for student_name, student_subj, _ in students
            if student_name == name
            and student_subj == subj
        )
    )
})
เห็น code นี้แล้วคงเดาได้ไม่ยากว่า พอเอาเขียนเป็น SQL คงหนีไม่พ้น
SELECT DISTINCT name
FROM   students AS s
WHERE  NOT EXISTS (
    SELECT *
    FROM   colleges AS c
    WHERE  campus = 'leetademy'
    AND    NOT EXISTS (
        SELECT *
        FROM   students AS ss
        WHERE  ss.name = s.name
        AND    ss.subj = c.subj
    )
)

อ้างอิง