Let’s get familiar with “Mapping & Flattening” operator in RxJS #1

Vorrawut Judasri
odds.team
Published in
5 min readSep 1, 2019

ในบทความนี้จะเชิญชวนให้ทุกท่านที่สนใจและอยากทำความรู้จักและคุ้นเคยกับตัวละครทั้ง 4 ตัวด้านบน ซึ่งได้แก่ switchMap, mergeMap, concatMap และ exhaustMap ที่เป็น Operator ที่ทำหน้าที่ในการ Mapping และ Flattening ใน RxJS โดยมีโอกาสได้ไปแชร์ความรู้ให้กับเพื่อนๆ พี่ๆ ในงาน ReactiveX Bangkok Meetup first เมื่อสิ้นเดือนที่แล้ว เนื้อหาทั้งหมดนี้ได้แรงบันดาลใจและข้อมูลส่วนใหญ่มาจาก Shai Reznik และอีกสองคนที่ขึ้นไปพูดในงาน ng-conf 2018 ซึ่งได้อธิบายอย่างเข้าใจและเห็นภาพชัดเจน เลยเกิดความคิดที่จะนำข้อมูลดังกล่าวมาเรียบเรียงและส่งต่อให้คนอื่นๆ

มาทำความรู้จัก Mapping หรือ map ใน RxJS กันก่อนว่าคืออะไร ?

“Transform a collection of items into a collection of different items”

map operator คือ การที่เรา Apply ฟังก์ชันๆ หนึ่งเข้าไปกับค่า value ทุกตัวที่ถูกส่งออกมาจาก Observable และเมื่อเสร็จแล้วก็จะ emit ค่าออกมาให้ หรือเรียกกระบวนการนี้ว่า “การเปลี่ยนแปลง collection ของชุดข้อมูลให้เป็นชุดข้อมูลที่มีขนาดเท่าเดิมแต่เป็นชุดข้อมูลใหม่”

ตัวอย่างของ map

ในตัวอย่างข้างบนจะมีการประกาศสร้าง Observable ที่มีค่า value เป็น 10,20 และ 35 ตามลำดับ หลังจากนั้นก็จะทำการ map ค่า value ด้วยฟังก์ชันการคูณ 2 เข้าไป โดยผลลัพท์ที่ออกมาจะทำให้ค่ากลายเป็น 20 ,40 และ 70 ที่ผ่านการคูณ 2 มาแล้วนั่นเอง

มาลองดูตัวอย่างของ map ที่อธิบายโดยใช้ภาพกันดูบ้างนะครับ

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

การอธิบาย map ด้วยการทำแซนวิส

ทีนี้ผมก็ได้นำวัตถุดิบเหล่านี้ไปให้ Chef Gordon ชื่อดังทำการหั่นให้ผมหน่อย โดยเรียกผ่าน method ที่ชื่อว่า slice() และได้ผลลัพธ์เป็น ขนมปัง แตงกว่า มะเขือเทศ ผักกาดแก้ว และหอมหัวใหญ่ที่หั่นเรียบร้อยแล้วตามภาพด้านขวามือ

ทีนี้มาลองดูการใช้ map function ผ่านทาง javascript กันดู

map ในรูปแบบของ javascript

ในภาพจะเห็นได้ว่าเรามีการประกาศตัวแปร ingredient ให้มีค่าเท่ากับลิสต์ของวัตถุดิบที่จะใช้ หลังจากนั้นเรามีการเรียก map โดยผ่านฟังก์ชัน slice และได้ผลลัพธ์ออกมาเป็นของที่ผ่านการหั่นเรียบร้อยแล้ว

ลองมาดูในฝั่งของ RxJS กันบ้างที่แทบไม่ต่างกับ javascript ซึ่งทำงานในลักษณะเดียวกันเลย

map ในรูปแบบของ RxJS

ในภาพจะเห็นได้ว่าเรามีการประกาศตัวแปร ingredient ให้เป็น Observable ที่มีค่าด้านในเป็นวัตถุดิบที่จะใช้ หลังจากนั้นเรามีการเรียก map โดยผ่านฟังก์ชัน slice และเมื่อเราทำการเรียก subscribe() ก็จะได้ผลลัพธ์ออกมาเป็นลิสต์ที่ด้านในมีวัถตุดิบที่ผ่านการหั่นเรียบร้อยแล้ว

มาลองดูอีกสักตัวอย่างกันนะครับ

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

การอธิบาย map ด้วย function grill()

ในส่วนของภาพทางด้านขวาคือ ผลลัพธ์ที่ผ่านกระบวนการย่างเรียบร้อยแล้ว โดยฝีมือของเชพ จะเห็นได้ว่าเราได้ของที่มีขนาดเท่าเดิมแต่สภาพหรือลักษณะได้เปลี่ยนแปลงไปเรียบร้อยแล้ว

มาดูกระบวนการ map เมื่อผ่านการ girll() ด้วย javascript กันดูนะครับ

map ด้วย function grill() ในรูปแบบ javascript

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

มาลองดูกระบวนการ map เมื่อผ่านการ girll() ด้วย RxJSกันต่อนะครับ

map ด้วย function grill() ในรูปแบบของ RxJS

หลังจากที่เราทำการประกาศให้ meat เป็น Observable ที่มีค่าเป็นเนื้อประเภทต่างๆ ที่จะเอาไปย่าง หลังจากที่ทำการ map ด้วย function grill แล้วผลลัพธ์ที่ได้ก็คือ เนื้อประเภทต่างๆ

คราวนี้ว่าด้วยเรื่องของ Marble diagram ของ map กันบ้าง

การทำความเข้าใจในการทำงานของ Rx จะหยิบใช้เครื่องมือที่ชื่อว่า Marble diagram เข้ามาอธิบาย เพื่อให้เราเข้าใจถึงวิธีการทำงานตามช่วงของเวลา

เริ่มต้นที่การให้เส้นลูกศรสีขาวที่ชี้ไปด้านขวา คือเส้นของคาบหรือช่วงของเวลาที่ใช้อธิบายเกี่ยวกับ input ที่เข้ามา โดยเริ่มต้นจากด้านซ้ายสุดไปทางขวาสุด โดยเมื่อเจอเส้นกั้นนั้นจะหมายถึงการสิ้นสุด process

Marble Diagram อธิบายการทำงานของ map

ตามรูปด้านบนเราจะได้ว่า observable ดังกล่าวมีข้อมูลเข้ามาเป็น 10 , 20 และ 35 ตามลำดับ โดยในช่วงเวลาที่เข้ามา value นั้นๆ จะทำการ map เข้ากับ function *2 หลังจากนั้นผลลัพธ์ที่ได้จะอยู่ในเส้นลูกศรสีขาวด้านล่างตามช่วงของเวลา ก็คือ 20 , 40 และ 70 ตามลำดับ

ต่อไปเราลองประยุกต์ Marble diagram ของ map เข้ากับตัวอย่างที่เราได้ยกไปก่อนหน้ากันบ้าง

โดยกำหนดให้ค่า input ที่รับเข้ามานั้นคือ วัตถุดิบที่เราต้องการนำไปย่าง โดยเข้ามาตามช่วงของเวลา

Marble Diagram อธิบายการทำงานของ map กับ function grill()

หลังจากที่วัตถุดิบเหล่านี้เข้ามาแล้วจะต้องผ่านไปยังกระบวนการย่างก่อน แล้วค่อยรีเทิร์นค่าของผลลัพธ์ออกมาให้กับเราตามเส้นด้านล่าง

“จะเห็นได้ว่าการใช้งานฟังก์ชัน map กับ RxJS มันไม่ใช่เรื่องยากเลย แต่มันเริ่มยากขึ้นเมื่อเราต้องการให้มันคืนค่ากลับมาเป็น Observable”

คำถามถัดมาก็คือ “ทำไมเราต้องอยากคืนค่าเป็น Observable กันละ ?”

ลองมาดูตัวอย่างถัดไปกันดูนะครับ

ในตัวอย่างนี้คือการทำ mapping value ของ playerObservable เข้ากับข้อความ ‘is awesome!’ โดยคาดหวังให้ผลลัพธ์ของค่าที่ออกมาจะมีการรวมกับข้อความนั้นเสมอ

playerObservable ที่มีการ map ค่าร่วมกับ text ‘is awesome’

โดยหลังจากที่เราทำการ map เรียบร้อยแล้วและทำการ subscribe เราก็จะได้ค่า Output ออกมาเป็น “Miracle is awesome!” และ “Topson is awesome!” ตามลำดับ

คราวนี้ลองเปลี่ยนให้ค่าของข้อความ “is awesome คือ ผลลัพธ์ที่ได้จากการเรียกผ่าน API” โดยกำหนดให้ค่าที่รับมามีค่าตามนี้

Mock API getMessage (จำลองการคืนค่าจากเรียกผ่าน API)

โดยเมื่อเรียก http.getMessage(name) ผลลัพธ์ที่ได้จะอยู่ในรูปของ Observable ที่เป็นชนิด string ที่ด้านในมีการต่อค่าของตัวแปร name เข้ากับข้อความที่รับมา ซึ่งก็คือ “is awesome!” กับ “is cool!”

หลังจากนั้นเราจะมาทำการปรับเปลี่ยนตัวอย่างเดิมก่อนหน้า ใหัมีการเรียก API ที่สร้างไว้

playerObservable ที่มีการ map ค่าร่วมกับ text ที่รับมาจากหลังบ้านและส่งมาเป็น Observable

โดยการเรียก map function ในลักษณะนี้จะเป็นการ return ค่าของ observable มาอีกที ทำให้ค่าของผลลัพธ์ที่ได้จากการ map อยู่ใน observable ซึ่งเป็นสิ่งที่เราไม่ได้ต้องการให้เป็นแบบนี้ โดยเราคาดหวังว่าเมื่อ map แล้วเราจะได้เห็น Output ในลักษณะเช่น “Miracle is awesome!” หรือ “Topson is cool!” ต่างหาก

แล้วเราจะทำอย่างไรให้ได้ค่าแบบนั้นออกมากัน ?

สิ่งที่จะมาช่วยเราได้คือ การทำ Flattening Observable นั้น

โดยความหมายของมันก็คือ “การเรียก Subscribe() ใน Subscribe() ของตัวหลักอีกทีหนึ่ง”

รูปแบบของการเรียก subscribe() ใน subscribe จะมีหน้าตาเป็นแบบนี้

โดยเมื่อเราได้ค่าที่ต้องการฟังผ่าน subscribe() เราจะต้องทำการเรียก value ดังกล่าวเพื่อ subscribe อีกที ถึงจะได้ค่า Output value ออกมาตามที่เราอยากได้ ลักษณะการ subscribe() ซ้อนกันแบบนี้จะเรียกว่าการ “Flattening”

ถ้ายังนึกภาพไม่ออกว่าเป็นยังไง หรือทำไปทำไม งั้นลองมาเริ่มด้วยการ Flatten Array กัน

มาลองดูตัวอย่างของการ flatten Array กันดู

โดยเมื่อเรามีอาร์เรย์ที่ด้านในมีอาร์เรย์ 1,2 และ 3 ตามลำดับกับ อาร์เรย์ A, B และ C ตามรูปด้านซ้ายมือ

การ Flatten Array

หลังจากที่เราทำการ Flatten มันก็จะทำให้อาร์เรย์ของเรากลายมาเป็นอาร์เรย์ชั้นเดียวที่มีค่า 1, 2, 3, A, B และ C อยู่ด้านใน ตามรูปด้านขวามือ

คราวนี้กลับมาลองดูการ Flatten Array ด้วยตัวอย่างก่อนหน้ากันนะครับ

โดยเราจำลองว่า เรามี Observable ที่ด้านในเก็บ Observable สองตัว ที่มีข้อมูลต่างกัน โดยตัวแรกจะเก็บค่า “Miracle is awesome!” และ “Miracle is cool!” และตัวที่สองคือ “Topson is awesome!” และ “Topson is cool!”

การ Flatten Array ที่เป็น observable (ผลลัพธ์ที่เราอยากได้)

หลังจากที่ Flatten เรียบร้อยแล้ว จะต้องได้ผลลัพธ์ออกมาอยู่ในรูปของ Observable ที่ด้านในมีค่า “Miracle is awesome!” ,“Miracle is cool!” , “Topson is awesome!” และ “Topson is cool!” ตามลำดับเหมือนทางด้านขวามือ เพื่อที่เราจะได้ Subscribe แค่รอบเดียวแล้วได้ค่าเลย ดังนั้นการที่เราต้องเรียก Subscribe() ซ้อนใน Subscribe อีกทีก็เพื่อให้เข้าถึงค่าด้านในของ Observable นั้นได้

ผลที่ตามมาหลังจาก Flattening ด้วยการ Subscribe()

แต่การที่เราเรียก Subscribe() ซ้อนกัน มันอาจก็ให้เกิดปัญหากับเราในกรณีที่เรา implement หรือ handle ไม่ดี เนื่องจากข้อมูลทุกตัวที่รับเข้ามาจะต้องทำการ subscribe ต่ออีกครั้ง ทำให้ในตอนที่มีจำนวนข้อมูลมากๆมันอาจจะทำให้เกิด memory leaks ได้ หรือบางครั้งถ้าเรา implement ไม่ดีพอก็อาจทำให้เกิด bug ที่แก้ได้ค่อนข้างยากหรือใช้เวลาในการแก้นานมากได้

ดังนั้นเวลาที่เรา “Flattening” เราจำเป็นต้องคิดด้วยว่าเราต้องการได้ผลลัพธ์เป็นแบบไหน

4 รูปแบบที่สามารถใช้งานได้ด้วยการ Flattening

โดยทั้ง 4 แบบจะมีคุณสมบัติและความสามารถที่ต่างกันอย่างชัดเจน ดังนั้นเวลาเลือกจะต้องพิจารณาถึงผลลัพธ์ที่อยากได้

merge : สามารถเปลี่ยนให้ Observable ที่รับเข้ามาแบบ multiple แล้ว emit ออกไปแค่ตัวเดียว

concat: จะ Subscribe() ตามลำดับและจะทำก็ต่อเมื่อตัวก่อนหน้าเสร็จแล้วเท่านั้น

switch: เมื่อมีค่า input ใหม่เข้ามาจะทำการ unsubscribe() ตัวก่อนหน้าทิ้งทันที

exhaust: ในขณะที่ยังทำตัวเก่าอยู่ จะไม่สนใจตัวใหม่ที่เข้ามาหรือไม่ทำการ subscribe() ตัวใหม่นั่นเอง จะต้องรอจนกว่าจะทำตัวปัจจุบันเสร็จแล้วเท่านั้น

4 Strategies of Flattening

ในส่วนของ 4 ตัวที่เราสามารถเอาไปใช้ได้แล้ว ยังมีในเวอร์ชั่น standalone ที่สามารถเอาไปใช้ต่อหลังจากที่ chain ใน pipe โดยจะอยู่ต่อจาก operator map ที่เราต้องการให้เกิดผลของ Flattening นั้นๆ

เมื่อเราใช้ mergeAll() ร่วมกับตัวอย่างของ playerObservable

หลังจากที่เรารู้ว่าอยากจะได้ผลลัพธ์ของการ Flattening เป็นแบบไหนนั้น เราก็สามารถเอาไปใช้งานได้ถูกวิธี โดยในตัวอย่างข้างล่างคือการที่เราอยากให้ผลลัพธ์ของมันมารวมกันแล้วค่อยส่งค่านั้นออกมาให้เรา เลยเลือกใช้ mergeAll หลังจากที่ค่า value ผ่านกระบวนการ map เรียบร้อยแล้ว

การใช้ mergeAll กับ playerObservable

จะเห็นว่าผลลัพธ์ที่ได้ออกมานั้นมีข้อมูลเหมือนกับตอนที่เรา Flattening ก่อนหน้าเลย แต่มีความปลอดภัยและอ่านง่ายกว่าอันแรก เพราะเมื่อเราใช้งานลักษณะนี้จะไม่ต้องมากังวลถึงปัญหา memory leak, การ unsubscribe หรือ bug ที่เกิดจากกระบวนการดังกล่าว เพียงแค่เรียกใช้งานให้ถูกตามที่ต้องการ

รูปแบบของ Marble Diagram ของ mergeAll()

โดยกำหนดให้มี Observable ทั้งหมด 2 ตัวที่ต้องการจะ mergeAll เข้าด้วยกัน และมี input ที่จะเข้ามาอยู่ทั้งหมด 4 ค่า โดยจะแยกตาม Observable ไทมไลน์ของตัวเองตามภาพ

Marble Diagram อธิบายการทำงานของ mergeAll()

จะเห็นได้ว่าเมื่อค่าของผลลัพธ์ที่ได้จากการผ่าน mergeAll() นั้นจะมีค่าเท่ากับค่าของ Observable มารวมกันตามช่วงของเวลา โดยใครมาก่อนก็อยู่ก่อน โดยจะยุบจาก 2 Observable เหลือแค่ตัวเดียว

โดยเมื่อนำ mergeAll() มาใช้กับ playerObservable จะได้ Marble Diagram ตามนี้

โดยกำหนดให้ค่าของ input ที่เข้ามานั้นเป็นค่าของ value ใน observable ที่รวมกับข้อความจาก API เรียบร้อยแล้ว

Marble Diagram อธิบายการทำงานของ mergeAll() ด้วยตัวอย่างของ playerObservable

เมื่อผ่าน mergeAll() เข้ามาเราจะได้ Observable ตัวเดียวที่มีค่าของทั้ง 2 observable รวมกันมาใช้งาน ซึ่งตรงตามที่เราอยากได้

สำหรับ Part1 ก็ขอจบไว้ที่การทำความเข้าใจวิธีการและการใช้งานของ map รวมถึงเข้าใจว่า Flattening นั้นคืออะไร มีวิธีการใช้งานแบบไหน และต้องคำนึงถึงอะไรบ้างในตอนที่ใช้ และสำหรับ Part ถัดไปจะเป็นการอธิบาย operator ที่มีความสามารถของ Mapping และ Flattening รวมกัน ว่าเป็นยังไงและใช้งานยังไงได้บ้างนะครับ

--

--