Things you don’t know about Protocol Buffers

ช่วงนี้นั่งงม Protocol Buffers ลึกๆ แล้วพบว่า documentation มันไม่ค่อยมีเขียนเท่าไร หลายๆ คนก็คงน่าจะเคยใช้อย่างมากก็ gRPC เลยอยากมาเล่าให้ฟังหน่อยว่า Protobuf ทำอะไรได้อีกบ้าง

บทความนี้จะพูดถึงเฉพาะ Protobuf 3 เท่านั้น ส่วน 2 ผมไม่ได้ใช้นานแล้ว

REST API

ลองทำ API แล้วคืนเป็น Protobuf ดู พบว่าทำงานง่ายขึ้นมาก

  • Protobuf มี schema ชัดเจน และ validate มาแล้ว ไม่ต้องนั่งเขียน JSON Schema บน Swagger อีกรอบ (ตรงไม่ตรงก็ไม่รู้อีกต่างหาก)
  • ไม่เสียเวลาเขียน struct หรือ type definition มา parse บน client side ใช้ codegen ออกมาจาก proto definition ได้เลย
  • ยังใช้ Django + Django REST Framework Serializer ได้อยู่

แต่ข้อเสียคือ

  • HTTP Library ส่วนมาก parse JSON จาก response ได้เลย พอเป็น Protobuf แล้วต้องเขียน logic ในการ parse เอง
  • บน JavaScript frontend ถ้าใช้ Protobuf API ต้อง ship parser ไปด้วยทำให้เปลือง bundle size

JSON

จากข้อข้างบน ถ้าอยากจะ ship Protobuf แต่ไม่อยากเปลือง bundle size วิธีหนึ่งที่ทำได้คือ Protobuf จะมี JSON representation เราก็อาจจะให้รับส่งเป็น JSON แทนได้

วิธีนี้คิดว่าได้ข้อดีข้างบนมาหมดทุกข้อเลย

วิธี encode protobuf เป็น JSON อย่า parse ออกมาแล้วโยนใส่ JSON encoder ปกติ แต่ Protobuf จะมี function ให้ เช่น google.protobuf.json_format.MessageToJson ใน Python หรือ google.golang.org/protobuf/encoding/protojson.Marshal ใน Go เวลาถอดรหัสก็ต้องใช้ function ของ protobuf เช่นกัน

JSON encoding ของ Protobuf จะไม่เหมือนกับ data structure ตรงๆ นิดนึงคือ

  • Key จะกลายเป็น lowerCamelCase เสมอ เช่น field ชื่อ user_name จะกลายเป็น userName
  • ถ้าไม่ได้แก้ settings อะไร default value ของแต่ละ field จะหายไป เช่นถ้ามี field string username = 1; อยู่มีค่าเป็น string เปล่า มันจะไม่ออกมาใน JSON (ถ้าอยากให้ parse สะดวกอาจจะต้องแก้ config ให้มันส่งออกมาด้วย)
  • enum จะแสดงเป็นชื่อ enum field แต่จะแก้ settings ให้ส่งเป็นตัวเลขแทนก็ได้ (default value ของ enum คือ member ตัวแรก)
  • bytes กลายเป็น base64 string

Fun fact: เวลาแปลง protobuf เป็น JSON ใน Go ไม่ได้ใช้ encoding/json แต่มันจะ build string ออกมาเลย (แต่ตอนถอดกลับใช้ยัง encoding/json นะ)

Null value

จากข้อข้างบนอาจจะสงสัยว่าทำไม default value ไม่ถูก encode ออกมา

คำตอบคือ Protobuf ไม่มี null value และนี่น่าจะเป็นสิ่งที่ผมพลาดเยอะที่สุด

กรณีที่เราเซต field ใดๆ เป็น null นั้น protobuf จะถือว่าใช้ default value ของ field นั้นๆ ได้แก่

  • string คือ string เปล่า
  • bool คือ false
  • ตัวเลขต่างๆ คือ 0
  • enum คือ สมาชิกตัวแรกของ enum
  • repeated คือ empty list
  • เฉพาะ field ที่มี type เป็น message เท่านั้นจะมี null value ได้

ที่เป็นแบบนี้เพราะ Protobuf จะไม่ encode field ที่มีค่าเป็น default ส่งไป

Well Known Type

Docs Protobuf เอา WKT ไปแอบลึกมากจนอาจจะไม่เคยรู้เลยว่ามีสิ่งนี้ด้วย

Well known type คือ message ต่างๆ ที่ติดมากับ Protobuf ได้แก่

  • google.protobuf.Duration เก็บระยะเวลา
  • google.protobuf.Timestamp เก็บเวลา
  • google.protobuf.Empty ไม่เก็บอะไรเลย

ดังนั้นเวลาจะ encode เวลาควรจะใช้ WKT เสมอ เนื่องจากว่าใน library ภาษาต่างๆ มักจะมี function ที่ถอดรหัสเข้าออกเป็น native value ของภาษานั้นๆ ให้อยู่แล้ว เช่น ToDatetime ใน Python หรือ AsTime ใน Go วิธีนี้ทำให้เราไม่ต้องนั่งจำว่า time บน service นี้ encode เป็นอะไร (ISO8601? POSIX? Custom format?)

เวลาใช้ WKT ต้อง import มาจาก google/protobuf/timestamp.proto (หรือไฟล์อื่นๆ ตามที่จะใช้) ซึ่งไม่มี document ไว้…

Wrapped type

จากข้อข้างบนเราบอกว่า Protobuf ไม่มี null value แต่ message เป็น null ได้ ดังนั้นถ้าอยากจะส่ง nullable เราก็แค่ยก field นั้นไปทำเป็น message ใหม่แล้วเซตเป็น null

ซึ่ง Protobuf ก็คิดมาแล้ว ใน WKT จะมี type ต่างๆ ที่ครอบมาให้แล้ว

  • google.protobuf.BoolValue
  • google.protobuf.BytesValue
  • google.protobuf.DoubleValue
  • google.protobuf.FloatValue
  • google.protobuf.Int32Value
  • google.protobuf.Int64Value
  • google.protobuf.StringValue
  • google.protobuf.UInt32Value
  • google.protobuf.UInt64Value

ทั้งหมดจะมี field เดียวคือ value และเมื่อ encode เป็น JSON แล้วมันจะหายไปกลายเป็นค่าที่เก็บไว้ตรงๆ (นี่แหละที่ถ้าทำ type เองจะทำไม่ได้)

FileDescriptorSet

เวลาใช้งาน protobuf เราจะใช้ protoc เพื่อทำ code generation ซึ่งต้องลง protoc-gen-* ตามภาษาที่ใช้งานด้วย

แล้ว protoc generate อะไรได้มั้ย?

คำตอบคือมัน generate FileDescriptorSet ได้

FileDescriptorSet เป็น well known type อันนึง มันคือการ .proto ให้เป็น protobuf แถมเราสามารถบอกให้ protoc include ไฟล์ทั้งหมดที่เรา include ต่อๆ กันมาได้ด้วย ทำให้ FileDescriptorSet นั้นจบในตัว

FileDescriptorSet นี้ไม่อยู่ใน protobuf documentation ด้วยนะ!!

Any

WKT อีก type หนึ่งที่น่าสนใจคือ Any ซึ่งน่าเสียดายที่ถึงมันจะอยู่ใน documentation แต่ library ต่างๆ ยังไม่พร้อมใช้เท่าไรนัก

Any ใช้เก็บ message อะไรก็ได้ โดยมันจะมี 2 field คือ

  • type_url เก็บ URL ของ type นั้น เช่น type.googleapis.com/com.mycompany.TypeName
  • value เป็น bytes คือข้อมูลที่ต้องการเก็บ encode เป็น protobuf

เวลา decode แล้ว library ที่รองรับจะสามารถอ่าน type ต้นฉบับได้เลยโดยไม่ต้อง decode เอง นอกไปจากนี้จะสังเกตว่า type_url นั้นเป็น URL เวลาเจอ type แปลกประหลาด Protobuf client library จะเข้าไปใน URL นั้นเพื่อโหลด type definition ให้อัตโนมัติ ซึ่งฟีเจอร์นี้โม้ไว้เฉยๆ ยังไม่ได้ทำ

เวลา encode Any เป็น JSON มันจะกลายเป็น {"@type": "type.googleapis.com/com.mycompany.TypeName"} แล้วเอาค่าใน value ที่เก็บไว้เข้ามา merge เลยไม่ได้ทำเป็น field ย่อย ใครที่ใช้ Stackdriver Logging น่าจะเห็น output ที่มี @type field บ่อย

จากด้านบน ถ้าเราทำ message ที่มี Any กับ FileDescriptorSet ฝังรวมกันเราจะได้ Self describing message แต่ปัจจุบันเนื่องจาก library ไม่รองรับกันเท่าไรจึง decode ยากมากๆ บางภาษาอาจจะ decode แล้วพังเลย

Reflection

แล้ว FileDescriptorSet ทำอะไรได้อีก? Protobuf ในบางภาษาจะมี reflection API ทำให้เราสามารถ create Protobuf message ได้ใน runtime (ภาษา dynamic เช่น JavaScript อาจจะไม่ต้องรองรับเพราะไม่ต้องใช้ codegen) เช่น dynamicpb

Lots of potential

เขียนมาทั้งหมดนี้แล้วจะเห็นธีมคล้ายๆ กันว่าถึง Protobuf จะออกมา 12 ปีแล้ว และ Proto3 ออกมา 4 ปีแล้วแต่ Protobuf ยังดูมี potential อีกมากที่ยังพัฒนาไม่เสร็จ

NSC รอบนักเรียน #2

จากปีที่แล้ว มาพูดถึงโครงงานปีนี้บ้าง

Inequality

ปีที่แล้วเรื่องที่สังเกตคือว่า โรงเรียนต่างจังหวัดมีโอกาสเข้าถึงปัญหามากกว่าโรงเรียนในกรุงเทพ ปรากฏว่าไม่รู้ว่าปีนี้โรงเรียนในกรุงเทพอ่านบล็อกเก่ามาหรืออย่างไร แต่เค้าแก้ปัญหาได้น่าสนใจมาก

ผู้เข้าแข่งขันที่ส่งมาหลายรอบน่าจะทราบว่า Social Impact เป็นคะแนนก้อนใหญ่มาก (ซึ่งแจ้งในเกณฑ์การตัดสินอยู่แล้ว) สิ่งที่โรงเรียนในกรุงเทพทำในปีนี้แทบทุกกลุ่มคือไปหากลุ่มเป้าหมายจริง หรือผู้เชี่ยวชาญในด้านนั้น (Domain expert) แล้วถ่ายคลิปมาด้วย กรรมการจะได้ไม่ต้องถามว่าอันนี้เคยเอาไปใช้งานจริงแล้วหรือยัง

ในขณะที่โรงเรียนต่างจังหวัดมีอยู่แค่ 3 – 4 โครงการที่ได้คุยกับ Domain expert บางกลุ่มสอบถามไปก็พบว่าขาดแคลน Domain expert ในพื้นที่ด้วยซ้ำ

Video

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

  1. หลายกลุ่มนั่งอ่านที่มาและความสำคัญให้ฟัง ซึ่งยาว เสียเวลา และไม่ได้ใช้วิดีโอเป็นประโยชน์ (ควรจะเขียน script ใหม่ให้เหมาะสมกับสื่อที่ใช้นำเสนอ)
  2. Demo ให้คะแนนยาก เช่นมีคะแนน Look and feel แต่ไม่ได้ถ่าย interface มาให้ดูเลยก็ต้องขอดูตอนซักถาม
  3. ถ้าตัดต่อดีเกินไปจะรู้สึกว่าเป็น visual effect ให้คะแนนไม่ได้เพราะไม่รู้ว่าชิ้นงานที่ใช้ได้จริงมากน้อยแค่ไหน โดยเฉพาะเมื่อกรรมการทดลองเองไม่ได้ ชิ้นงานอาจจะ simulate scenario ทั้งหมดที่แสดงในคลิปให้เหมือนใช้งานได้ไม่ติดปัญหา (นี่คือรายการแข่งซอฟต์แวร์ เราตัดสินจากชิ้นงานที่เสร็จไม่ใช่ไอเดีย) ก็เป็นหน้าที่ของกรรมการที่จะต้องใช้ซักถามสืบให้พอทราบว่าชิ้นงานจริงใช้งานได้แค่ไหน
  4. เนื่องจากทำ video แล้วไม่จำเป็นต้องถ่ายคลิปตัวเองยืนพูดหน้า slide บางคนทำแบบนี้มาแล้วข้อความไม่ชัด หรือเสียงไม่ชัด

สรุปแล้วคิดว่ากลางๆ สุดคือเอา slide ขึ้นเต็มจอ พากย์เสียงทับแล้วตัด demo เข้ามาแทรก (ด้วย screen record จริง ไม่ใช่เอากล้องจับ) น่าจะทำง่ายและดูง่ายที่สุด อาจจะน่าเบื่อแต่ไม่น่าจะเสียคะแนน

ปีหน้าคาดหวังว่าถ้ามีวิดีโออีก มันจะคุณภาพดีขึ้น เพราะปีนี้ฉุกละหุกมาก แต่ไม่มีเลยจะดีกว่า

Scope

ปีนี้กรรมการกังวลกันว่าผู้เข้าแข่งขันเห็นคะแนนแล้วจะสงสัยกันมั้ยว่าทำไมคนนี้ได้อันดับเท่านี้ ทั้งๆ ที่ Scope อาจจะเล็กกว่าของเค้า

ในความเห็นผมว่าเลือก Scope เล็กดีกว่า Scope ใหญ่ มันทำให้โฟกัสชิ้นงานได้มากขึ้น งานมันเลยจะรู้สึกว่าคุณภาพดี มากกว่างานที่ Scope ใหญ่แล้วเสร็จฟีเจอร์อย่างละนิดอย่างละหน่อย แต่ก็ต้อง balance กับ social impact ด้วยว่าถ้า scope งานเล็กแล้วประโยชน์น้อย อาจจะได้รางวัลแต่อาจจะไม่ถึงที่ 2

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

IoT

ปีนี้รู้สึกว่า IoT ถอยหลังลงคลองอย่างชัดเจนมาก ด้วยการมาของ Blynk และ Kidbright

สมัยผมลงแข่งจำได้ว่ามีกลุ่มหนึ่งทำระบบปิดไฟโดยใช้ตู้ Consumer Unit ของ Scheider ที่เสียบ Ethernet ได้ แล้วทำหน้าเว็บเป็น PHP เพื่อควบคุม มันเลยค่อนข้างเป็นโครงงาน software เพราะ hardware ไม่ได้ทำเอง

แต่สมัยนี้โครงงานที่เน้น hardware มีมากขึ้นเรื่อยๆ โดยเฉพาะยุค IoT แต่คิดว่าในมุมเทคนิคแล้วมันน่าจะเกิดจากที่การเขียน IoT ง่ายขึ้นเรื่อยๆ โดยเฉพาะ interface สำเร็จรูปที่ทำให้คนทำเองลดลงมาก หลายๆ ทีมเลือกใช้ Blynk หรือ LINE Notify ซึ่งพวกนี้เราไม่นับเป็นคะแนน interface เลยเพราะใช้ของสำเร็จมาแล้ว

ปัญหาที่ตามมาคือ ในหลายๆ use case ที่เลือกมาใช้แล้วจำเป็นต้องพึ่งบริการ online แต่จุดที่ใช้งานไม่มี internet ต้องติดตั้ง internet มือถือเพิ่มเข้าไปอีก (และด้วยว่าโครงงานไม่ได้ถูกใช้งานจริง เลยไม่เห็นใครคิดแก้ปัญหามาก่อนที่กรรมการจะถาม)

จริงอยู่ว่าการใช้ interface สำเร็จจะลดเวลาทำสิ่งที่เคยมีคนทำไปแล้ว (don’t reinvent the wheel) แต่ยังไม่เห็นมีใครจะใช้เวลาที่ได้คืนมาเอาไปทำอย่างอื่นที่เป็นนวัตกรรมใหม่ๆ โครงการเปิดปิดไฟที่ทำมาเป็นสิบปีแล้วก็ยังมีฟีเจอร์เท่าเดิมแค่เปลี่ยน use case ไปเรื่อยๆ แถมโค้ดน้อยลงเรื่อยๆ จนไม่ต้องเขียน server เองแล้ว

ที่น่ากลัวคือ IoT กำลังจะเข้าถึงมือ end user ด้วย Xiaomi และ Sonoff ที่ทำราคาถูกมาก ปีหน้าถ้ายังมีใครทำเปิดปิดพัดลมตามอุณหภูมิอยู่อีกจะถามแล้วว่าต่างกับการใช้ Sonoff กับ Xiaomi Temperature Sensor อย่างไร (แถมมันยังมี Plant Monitor ปักดินได้ด้วยนะ)

สิ่งที่คาดหวังในอนาคตคือโครงการใหม่ๆ ที่ไม่ใช่แค่เอา sensor ที่ให้มาต่อออก internet มีแจ้งเตือนแล้วก็เปิดปิดไฟได้อัตโนมัติ เช่น…

  • ระบบสำหรับทำให้ end user กำหนด automation ได้เองเหมือน IFTTT
  • data source แบบใหม่ที่ไม่ใช่เซนเซอร์ในกล่อง (ผมเคยลองทำระบบตรวจจับคนในห้องประชุมจะใช้ที่บริษัท มันก็พอได้บ้างนะ แต่สำหรับรอบนักเรียนอาจจะต้อง reframe ปัญหาใหม่)
  • เงื่อนไขการทำงานที่กลับด้านกัน คือ action เกิดบน internet แล้วส่งผลต่อโลกจริง เช่น Printer ที่พิมพ์ Instagram อัตโนมัติ
  • ไอเดีย embedded อื่นๆ ที่ไม่ต้องออก internet เช่น Network monitor หรืองานที่ใช้ peer-to-peer ระหว่างอุปกรณ์ (ปีนี้ก็มีคนทำนะ)

แต่ถ้าเลี่ยงได้น่าจะมีหัวข้อที่ได้ที่ 1 อีกเยอะที่ง่ายกว่าด้านบนนี้