Monday, December 23, 2013

Fizz Buzz in Haskell without If

เห็นน้องรูฟจัดกิจกรรม Kata เขียน FizzBuzz แบบไม่ให้ if แล้วคันมือบ้าง ก็เลยขุดเอา Haskell มาลองใช้

แนวคิดก็คือ เรารู้ว่าชุดของ fizzbuzz มันจะซ้ำเมื่อวนซ้ำเดิมทุกๆ 15 ค่า เราก็เลยสร้าง function ที่ทำหน้าที่แปลงตัวเลข เป็นข้อความ ขึ้นมา 15 ตัว แล้วใช้ function cycle ทำหน้าผลิตซ้ำมันให้กลายเป็น infinite array

fizz n     = "fizz"
buzz n     = "buzz"
fizzbuzz n = "fizzbuzz"
as_is n    = show n
 
fn_table = cycle [as_is, as_is, fizz, as_is, buzz, fizz, as_is, as_is, fizz, buzz, as_is, fizz, as_is, as_is, fizzbuzz] 

ตาราง fizz buzz ก็เกิดจากการเรียกใช้ function ด้วย ตัวเลข index ของ ตำแหน่งของมัน


table = zipWith ($) fn_table [1..]

ลองเรียก 100 ลำดับแรกขึ้นมาตรวจสอบ

take 100 table 
["1","2","fizz","4","buzz","fizz","7","8","fizz","buzz","11","fizz","13","14","fizzbuzz","16","17","fizz","19","buzz","fizz","22","23","fizz","buzz","26","fizz","28","29","fizzbuzz","31","32","fizz","34","buzz","fizz","37","38","fizz","buzz","41","fizz","43","44","fizzbuzz","46","47","fizz","49","buzz","fizz","52","53","fizz","buzz","56","fizz","58","59","fizzbuzz","61","62","fizz","64","buzz","fizz","67","68","fizz","buzz","71","fizz","73","74","fizzbuzz","76","77","fizz","79","buzz","fizz","82","83","fizz","buzz","86","fizz","88","89","fizzbuzz","91","92","fizz","94","buzz","fizz","97","98","fizz","buzz"]


น้องรูฟเห็น code แล้ว บอกว่า ทำไมไม่มี test หล่ะพี่ เราก็นึกในใจ code สั้นขนาดนี้ยังต้องมี test อีกหรือ แต่เพื่อให้เข้ากระแสจึงต้องไปค้นคว้าสักหน่อยว่า ถ้าต้องเขียน test ใน haskell เราจะใช้ท่าไหนดี
ใน haskell เขาไม่นิยมเขียน unit test แบบทั่วๆไป เขาบอกว่ามัน primitive มากที่ต้องเขียน test เพื่อ assert เงื่อนไขแบบ manual ทำไมเราไม่เขียนแค่ specification ของ function เรา แล้วเดี๋ยว haskell จะ random data เข้ามาทำทดสอบให้เราเอง

อันนี้คือ spec ของ fizzbuzz ของเรา

show_at n = table !! (n - 1)
test' n = fn n == show_at n
        where fn n
                | (n `rem` 15) == 0 = "fizzbuzz"
                | (n `rem` 5)  == 0 = "buzz"
                | (n `rem` 3)  == 0 = "fizz"
                | otherwise = show n

show_at ก็คือ ค่าที่ได้จากตาราง fizz buzz ที่เรา generate ขึ้นมาจาก code ข้างบน
ส่วน fn ก็คือ spec ที่เรากำหนดขึ้นมาทดสอบ

ลองทดสอบ run ดู

*Main> quickCheck test' 
*** Failed! (after 1 test and 1 shrink): Exception: Prelude.(!!): negative index 0

จะเห็นว่า Fail ตั้งแต่ค่าแรกสุด เลย นั่นคือค่า fizz buzz กรณี n = 0
แต่เราไม่อยากปวดหัวกับ fizzbuzz ที่ n <= 0 เราก็เลย กำหนด test ขึ้นมาใหม่ ให้ใช้ index ค่าระหว่าง 1 ถึง 10000 เท่านั้น

 test n = forAll (elements [1..10000]) $ \n -> test' n 

ลองทดสอบ run ดู จะเห็นว่า quickCheck จะ random ค่าระหว่าง 1 ถึง 10000 ขึ้นมา 100 ค่า แล้วทำการทดสอบให้เรา

*Main> quickCheck test 
+++ OK, passed 100 tests.

Related link from Roti