Showing posts with label PHP. Show all posts
Showing posts with label PHP. Show all posts

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

Apr 7, 2013

Bit Flag

ในภาษาสมัยใหม่ เราคงคุ้นเคยกับการเขียนฟังก์ชั่น (และเรียกใช้) แบบนี้

def foo(n, m, a=False, b=False, c=False):
    if a:
        n += 42
    if b:
        n *= 7
    if c:
        n **= 2
    return n % m

...

foo(5, 99)
foo(5, 99, a=True)
foo(5, 99, c=True)
foo(5, 99, c=True, b=True, a=True)

โชคร้ายที่ระบบ keyword argument ไม่ได้เป็นแบบนี้ทุกภาษา อย่างเช่นใน PHP ถ้าจะเปิดตัว flag c เพียงตัวเดียว ก็ยังคงต้องบอกว่า a, b ถูกปิดอยู่ด้วย (ต้องบอกสถานะของ flag ทุกตัวที่อยู่ก่อนหน้า c)

ทางออกง่ายๆ แต่ทำให้ syntax ดูรุงรังหน่อยคือการให้ argument ส่วนที่เป็น flag ใช้ datatype แบบ hash table เช่นนี้

function foo($n, $m, $options=array()) {
    if (array_key_exists('a', $options) && $options['a'])
        $n += 42;
    if (array_key_exists('b', $options) && $options['b'])
        $n *= 7;
    if (array_key_exists('c', $options) && $options['c'])
        $n *= $n;
    return $n % $m;
}   

...

foo(5, 99);
foo(5, 99, array('a' => true));
foo(5, 99, array('c' => true));
foo(5, 99, array('c' => true, 'b' => true, 'a' => true));

ทางออกที่คลาสสิก (แต่ปวดหัว) กว่าคือการใช้ bit flag แบบนี้

const int A = 0x01;
const int B = 0x02;
const int C = 0x04;

int foo(int n, int m, int options) {
    if (options & A)
        n += 42;
    if (options & B)
        n *= 7;
    if (options & C)
        n *= n;
    return n % m;
}

...

foo(5, 99, 0);
foo(5, 99, A);
foo(5, 99, C);
foo(5, 99, C|B|A);

หลายคนอาจบอกว่าการใช้ bit flag นั้นมีข้อดีเหนือกว่า keyword argument ตรงที่สามารถ store สถานะของ flag เก็บไว้ใน data ตัวหนึ่ง (เพื่อที่จะนำไปใช้ซ้ำในที่อื่น) ซึ่งข้อได้เปรียบนี้ไม่เป็นความจริงเลยสำหรับภาษาที่แตก array, hash table ลงไปเป็น argument ได้

args = [5, 99]
flags = {'b': True, 'c': True}

foo(*args, **flags) == foo(5, 99, b=True, c=True)

ก็เลือกใช้ความสามารถเหล่านี้ตามความสะดวกของแต่ละภาษานะครับ อย่าไปฝืนมันมากถ้าตัวภาษามันไม่ได้ออกแบบให้ทำเรื่องยากๆ บางเรื่องไว้แต่แรก

Aug 20, 2012

เขียน Loop ยังไงดี?

ในการเขียนโปรแกรม เรามักต้องพบปะกับท่านี้เสมอๆ
text = input()
while text != 'exit':
    # do something with text
    # ...
    text = input()
ดูเผินๆ ก็ไม่น่ามีปัญหาอะไร แต่นี่จะทำให้โปรแกรมเมอร์สำคัญผิดกับ input ครั้งแรก หรือไม่งั้นก็มองโค้ดผ่านๆ แล้วนึกว่า input ด้านนอกนั้นจะไม่เกี่ยวกับกิจกรรมที่เกิดขึ้นใน while loop ที่ตามมาเลย (ยิ่งถ้าต้องมีการ pre-process input ก่อนที่จะเอาไป check while อีก) ทางแก้ง่ายๆ คือเปลี่ยนไปเขียนแบบนี้แทน
while True:
    text = input()
    if text == 'exit':
        break
    # do something with text
    # ...


อีกเรื่องที่ไม่ค่อยเกี่ยวกันเท่าไหร่คือ การรับข้อมูลจาก database มาแสดงผล ส่วนใหญ่จะทำท่านี้
$result = mysql_query($sql);
while ($row = mysql_fetch_row($result)) {
    echo $row[0], $row[1], $row[2];
}
ในทางปฎิบัติมันก็ใช้ได้เลย แต่ถ้ามาคิดๆ ดูแล้ว ข้อมูลที่ query ออกมาจาก database ได้นั้นควรจะมีจำนวนจำกัดเสมอ (ไม่มี database ไหนที่เก็บข้อมูลได้เป็น infinity เป็นแน่??) นั่นหมายความว่าทางทฤษฎีแล้ว มันไม่ควรเขียนด้วย while loop แต่ควรเขียนด้วย for loop (อาจเป็น for-each loop) เช่นนี้
// codeigniter active record
$query = $this->db->get($sql);
foreach ($query->result() as $row) {
    echo $row->title, $row->author, $row->content;
}
ท่านี้จะทำให้มีปัญหาด้าน memory (เพราะต้อง query ข้อมูลทั้งหมดออกมาเก็บไว้ใน array ก่อน แล้วจึงค่อยวนเข้าไปอ่านข้อมูลใน array นั้น) ซึ่งแก้ได้ที่ระดับ library โดยเปลี่ยนไปใช้ generator แทน array



ท้ายสุด กลับไปที่ while loop แบบแรก โดยมีเงื่อนไขเพิ่มเติมตรงที่อยากรู้ว่าเป็นการทำงานรอบที่เท่าไหร่ อาจเขียนเพิ่มเป็น
i = 0
while True:
    text = input()
    if text == 'exit':
        break
    # do something with text
    # ...
    print('loop no:', i, '; input is:', text)
    i += 1
จะเห็นว่ามี i = 0 เป็น initial statement อยู่นอก scope ของ while ตรงนี้จริงๆ ไม่มีปัญหาอะไรเลย แต่ถ้าอยากให้มันสวยขึ้นด้วยการจัดระเบียบ block สามารถใช้ประโยชน์ของ infinity generator ได้
from itertools import count

for i in count():
    text = input()
    if text == 'exit':
        break
    # do something with text
    # ...
    print('loop no:', i, '; input is:', text)
หรือยิ่งไปกว่านั้น ทำการเปลี่ยนรูปแบบการเขียนเพื่อให้ไม่ต้องตรวจสอบสำหรับ break ออกจาก loop แต่ไปตรวจตั้งแต่หัว loop เลยว่าหลุดออกจาก loop นี้หรือยัง อย่างนี้
from itertools import count

for i, text in zip(count(), iter(input, 'exit')):
    # do something with text
    # ...
    print('loop no:', i, '; input is:', text)
แนวคิดจะเปลี่ยนไปนิดนึงด้วย ตรงที่เราไม่ได้ใช้ while loop แล้ว (loop แต่ละครั้งไม่เชื่อมโยงกัน) เปลี่ยนมาเป็น for loop (แต่ละครั้งมีความเกี่ยวเนื่องกัน อย่างน้อยๆ ก็ index ที่ไล่กันไปเรื่อยๆ) นั่นเอง ซึ่งส่วนตัวแล้วผมชอบอย่างหลังมากกว่า