ตัวเลือก/บางที, ทั้งสองอย่าง และ Monads ในอนาคตใน JavaScript, Python, Ruby, Swift และ Scala

เผยแพร่แล้ว: 2022-03-11

บทช่วยสอนเกี่ยวกับ monad นี้ให้คำอธิบายสั้น ๆ เกี่ยวกับ monads และแสดงวิธีใช้งาน Monads ที่มีประโยชน์ที่สุดในห้าภาษาการเขียนโปรแกรมที่แตกต่างกัน—หากคุณกำลังมองหา monads ใน JavaScript, monads ใน Python, monads ใน Ruby, monads ใน Swift และ/หรือ monads ใน Scala หรือเพื่อเปรียบเทียบการใช้งานใด ๆ คุณกำลังอ่านบทความที่ถูกต้อง!

เมื่อใช้ Monads เหล่านี้ คุณจะกำจัดชุดข้อบกพร่องต่างๆ เช่น ข้อยกเว้นที่ไม่มีตัวชี้ ข้อยกเว้นที่ไม่สามารถจัดการได้ และเงื่อนไขการแข่งขัน

นี่คือสิ่งที่ฉันครอบคลุมด้านล่าง:

  • บทนำสู่ทฤษฎีหมวดหมู่
  • นิยามของโมนาด
  • การใช้งานตัวเลือก ("อาจจะ") monad, monad ก็ได้, และ monad ในอนาคต บวกกับโปรแกรมตัวอย่างที่ใช้ประโยชน์จากสิ่งเหล่านี้ ใน JavaScript, Python, Ruby, Swift และ Scala

มาเริ่มกันเลย! จุดแรกของเราคือทฤษฎีหมวดหมู่ ซึ่งเป็นพื้นฐานสำหรับโมนาด

ทฤษฎีหมวดหมู่เบื้องต้น

ทฤษฎีหมวดหมู่เป็นสาขาวิชาคณิตศาสตร์ที่ได้รับการพัฒนาอย่างแข็งขันในช่วงกลางศตวรรษที่ 20 ตอนนี้เป็นพื้นฐานของแนวคิดการเขียนโปรแกรมเชิงฟังก์ชันมากมายรวมถึงโมนาด มาดูแนวคิดทฤษฎีหมวดหมู่กันโดยย่อ ซึ่งปรับให้เข้ากับคำศัพท์เฉพาะด้านการพัฒนาซอฟต์แวร์

จึงมีแนวคิดหลักสามประการที่กำหนด หมวดหมู่:

  1. ประเภท ก็เหมือนกับที่เราเห็นในภาษาที่พิมพ์แบบคงที่ ตัวอย่าง: Int , String , Dog , Cat , ฯลฯ
  2. ฟังก์ชั่น เชื่อมต่อสองประเภท ดังนั้นจึงสามารถแสดงเป็นลูกศรจากประเภทหนึ่งไปยังอีกประเภทหนึ่งหรือสำหรับตัวเอง ฟังก์ชั่น $f$ จากประเภท $T$ ถึงประเภท $U$ สามารถแสดงเป็น $f: T \to U$ คุณสามารถมองมันเป็นฟังก์ชันภาษาโปรแกรมที่รับอาร์กิวเมนต์ประเภท $T$ และส่งกลับค่าของประเภท $U$
  3. องค์ประกอบ คือการดำเนินการ ซึ่งแสดงโดยตัวดำเนินการ $\cdot$ ที่สร้างฟังก์ชันใหม่จากฟังก์ชันที่มีอยู่ ในหมวดหมู่ จะรับประกันฟังก์ชันใด ๆ $f: T \to U$ และ $g: U \to V$ เสมอ มีฟังก์ชันเฉพาะ $h: T \to V$ ฟังก์ชันนี้แสดงเป็น $f \cdot g$ การดำเนินการนี้จับคู่ฟังก์ชันกับฟังก์ชันอื่นอย่างมีประสิทธิภาพ ในภาษาโปรแกรม แน่นอนว่าการดำเนินการนี้เป็นไปได้เสมอ ตัวอย่างเช่น หากคุณมีฟังก์ชันที่คืนค่าความยาวของสตริง —$strlen: String \to Int$— และฟังก์ชันที่บอกว่าตัวเลขเป็นคู่ —$even: Int \to Boolean$— คุณสามารถสร้าง function $even{\_}strlen: String \to Boolean$ ซึ่งบอกได้ว่าความยาวของ String นั้นเท่ากันหรือไม่ ในกรณีนี้ $even{\_}strlen = even \cdot strlen$ องค์ประกอบแสดงถึงคุณสมบัติสองประการ:
    1. การเชื่อมโยง: $f \cdot g \cdot h = (f \cdot g) \cdot h = f \cdot (g \cdot h)$
    2. การมีอยู่ของฟังก์ชันเอกลักษณ์: $\forall T: \exists f: T \to T$ หรือในภาษาอังกฤษแบบธรรมดา สำหรับ $T$ ทุกประเภท จะมีฟังก์ชันที่จับคู่ $T$ กับตัวมันเอง

มาดูหมวดหมู่ง่ายๆ กัน

หมวดหมู่อย่างง่ายที่เกี่ยวข้องกับ String, Int และ Double และฟังก์ชันบางอย่างในนั้น

หมายเหตุด้านข้าง: เราคิดว่า Int , String และประเภทอื่นๆ ทั้งหมดรับประกันว่าไม่ใช่ค่าว่าง กล่าวคือ ไม่มีค่า null

หมายเหตุด้านข้าง 2: นี่เป็นเพียง ส่วนหนึ่ง ของหมวดหมู่ แต่นั่นคือทั้งหมดที่เราต้องการสำหรับการสนทนาของเรา เพราะมันมีส่วนที่จำเป็นทั้งหมดที่เราต้องการ และไดอะแกรมจะรกน้อยกว่าด้วยวิธีนี้ หมวดหมู่จริงจะมีฟังก์ชันที่ประกอบด้วยทั้งหมดเช่น $roundToString: Double \to String = intToString \cdot round$ เพื่อตอบสนองเงื่อนไของค์ประกอบของหมวดหมู่

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

คงจะดีไม่น้อยหากโค้ดทั้งหมดของเราทำงานในระดับความเสถียรนี้ อย่างแน่นอน! แล้ว I/O ล่ะ? เราไม่สามารถอยู่ได้โดยปราศจากมันอย่างแน่นอน นี่คือจุดที่โซลูชัน monad เข้ามาช่วยเหลือ: พวกเขาแยกการทำงานที่ไม่เสถียรทั้งหมดออกเป็นโค้ดขนาดเล็กพิเศษที่ได้รับการตรวจสอบอย่างดี จากนั้นคุณสามารถใช้การคำนวณที่เสถียรในแอปทั้งหมดของคุณได้!

เข้าสู่ Monads

เรียกพฤติกรรมที่ไม่เสถียรเช่น I/O ว่าเป็น ผลข้างเคียง ตอนนี้ เราต้องการทำงานกับฟังก์ชันที่กำหนดไว้ก่อนหน้านี้ทั้งหมดของเรา เช่น length และ types เช่น String ในลักษณะที่เสถียรเมื่อมี ผลข้างเคียง นี้

เรามาเริ่มด้วยหมวดหมู่ว่าง $M[A]$ กันก่อน และทำให้เป็นหมวดหมู่ที่จะมีค่าที่มีผลข้างเคียงประเภทหนึ่งโดยเฉพาะ และค่าที่ไม่มีผลข้างเคียงด้วย สมมติว่าเราได้กำหนดหมวดหมู่นี้และว่างเปล่า ขณะนี้ไม่มีประโยชน์อะไรที่เราสามารถทำได้ ดังนั้นเพื่อให้มีประโยชน์ เราจะปฏิบัติตามสามขั้นตอนเหล่านี้:

  1. กรอกค่าของประเภทจากหมวดหมู่ $A$ เช่น String , Int , Double ฯลฯ (กล่องสีเขียวในแผนภาพด้านล่าง)
  2. เมื่อเรามีค่าเหล่านี้แล้ว เราก็ยังคงไม่สามารถทำอะไรที่มีความหมายกับพวกมันได้ ดังนั้นเราต้องการวิธีที่จะนำแต่ละฟังก์ชัน $f: T \to U$ จาก $A$ มาสร้างฟังก์ชัน $g: M[T] \to M [U]$ (ลูกศรสีน้ำเงินในแผนภาพด้านล่าง) เมื่อเรามีฟังก์ชันเหล่านี้แล้ว เราก็สามารถทำทุกอย่างด้วยค่าในหมวดหมู่ $M[A]$ ที่เราสามารถทำได้ในหมวด $A$
  3. ตอนนี้เรามีหมวดหมู่ $M[A]$ ใหม่แล้ว คลาสใหม่ของฟังก์ชันก็พร้อมลายเซ็น $h: T \to M[U]$ (ลูกศรสีแดงในแผนภาพด้านล่าง) สิ่งเหล่านี้เกิดขึ้นจากการส่งเสริมค่านิยมในขั้นตอนที่หนึ่งซึ่งเป็นส่วนหนึ่งของ codebase ของเรา กล่าวคือ เราเขียนมันตามความจำเป็น สิ่งเหล่านี้คือสิ่งสำคัญที่จะทำให้การทำงานกับ $M[A]$ แตกต่างจากการทำงานกับ $A$ ขั้นตอนสุดท้ายคือการทำให้ฟังก์ชันเหล่านี้ทำงานได้ดีกับประเภทใน $M[A]$ เช่นกัน กล่าวคือ สามารถรับฟังก์ชันได้ $m: M[T] \to M[U]$ จาก $h: T \ ถึง M[U]$

การสร้างหมวดหมู่ใหม่: หมวดหมู่ A และ M[A] รวมถึงลูกศรสีแดงจาก A's Double ถึง M[A]'s Int ซึ่งมีป้ายกำกับว่า "roundAsync" M[A] นำทุกค่าและฟังก์ชันของ A กลับมาใช้ใหม่ ณ จุดนี้

เรามาเริ่มกันโดยกำหนดสองวิธีในการส่งเสริมค่าของประเภท $A$ ให้เป็นค่าของประเภท $M[A]$: ฟังก์ชันหนึ่งที่ไม่มีผลข้างเคียง และอีกฟังก์ชันหนึ่งมีผลข้างเคียง

  1. ค่าแรกเรียกว่า $pure$ และถูกกำหนดสำหรับแต่ละค่าของหมวดหมู่ที่เสถียร: $pure: T \to M[T]$ ผลลัพธ์ของค่า $M[T]$ จะไม่มีผลข้างเคียง ดังนั้นฟังก์ชันนี้จึงเรียกว่า $pure$ เช่น สำหรับ I/O monad $pure$ จะคืนค่าบางค่าทันทีโดยไม่มีความเป็นไปได้ที่จะเกิดความล้มเหลว
  2. ตัวที่สองเรียกว่า $constructor$ และไม่เหมือน $pure$ ที่คืนค่า $M[T]$ พร้อมผลข้างเคียงบางอย่าง ตัวอย่างของ $constructor$ สำหรับ async I/O monad อาจเป็นฟังก์ชันที่ดึงข้อมูลบางส่วนจากเว็บและส่งคืนเป็น String ค่าที่ส่งคืนโดย $constructor$ จะมีประเภท $M[String]$ ในกรณีนี้

ตอนนี้ เรามีสองวิธีในการส่งเสริมคุณค่าใน $M[A]$ แล้ว ในฐานะโปรแกรมเมอร์ที่จะเลือกฟังก์ชันที่จะใช้ ขึ้นอยู่กับเป้าหมายของโปรแกรมของคุณ ลองพิจารณาตัวอย่างที่นี่: คุณต้องการดึงหน้า HTML เช่น https://www.toptal.com/javascript/option-maybe-either-future-monads-js และสำหรับสิ่งนี้ คุณสร้างฟังก์ชัน $fetch$ เนื่องจากอาจมีข้อผิดพลาดขณะดึงข้อมูล เช่น คิดว่าเครือข่ายล้มเหลว ฯลฯ คุณจะใช้ $M[String]$ เป็นประเภทส่งคืนของฟังก์ชันนี้ ดังนั้นมันจะดูเหมือน $fetch: String \to M[String]$ และที่ใดที่หนึ่งในเนื้อหาของฟังก์ชันที่นั่น เราจะใช้ $constructor$ สำหรับ $M$

ตอนนี้ สมมติว่าเราสร้างฟังก์ชันจำลองสำหรับการทดสอบ: $fetchMock: String \to M[String]$ มันยังคงมีลายเซ็นเดิม แต่คราวนี้เราเพียงแค่ฉีดหน้า HTML ที่เป็นผลลัพธ์ภายในเนื้อความของ $fetchMock$ โดยไม่ต้องดำเนินการใดๆ กับเครือข่ายที่ไม่เสถียร ดังนั้นในกรณีนี้ เราก็แค่ใช้ $pure$ ในการนำ $fetchMock$ ไปใช้งาน

ในขั้นตอนต่อไป เราจำเป็นต้องมีฟังก์ชันที่สนับสนุนฟังก์ชันที่กำหนดเอง $f$ จากหมวดหมู่ $A$ ถึง $M[A]$ (ลูกศรสีน้ำเงินในไดอะแกรม) อย่างปลอดภัย ฟังก์ชันนี้เรียกว่า $map: (T \to U) \to (M[T] \to M[U])$

ตอนนี้ เรามีหมวดหมู่ (ซึ่งสามารถมีผลข้างเคียงได้หากเราใช้ $constructor$) ซึ่งมีฟังก์ชันทั้งหมดจากหมวดหมู่ที่เสถียร ซึ่งหมายความว่าพวกมันจะเสถียรใน $M[A]$ เช่นกัน คุณอาจสังเกตเห็นว่าเราได้แนะนำคลาสของฟังก์ชันอื่นอย่างชัดเจน เช่น $f: T \to M[U]$ เช่น $pure$ และ $constructor$ เป็นตัวอย่างของฟังก์ชันดังกล่าวสำหรับ $U = T$ แต่อาจมีมากกว่านั้นได้อย่างชัดเจน เช่น หากเราใช้ $pure$ แล้วตามด้วย $map$ โดยทั่วไป เราต้องการวิธีจัดการกับฟังก์ชันที่กำหนดเองในรูปแบบ $f: T \to M[U]$

หากเราต้องการสร้างฟังก์ชันใหม่โดยอิงจาก $f$ ที่สามารถใช้กับ $M[T]$ ได้ เราอาจลองใช้ $map$ แต่นั่นจะทำให้เราใช้ฟังก์ชัน $g: M[T] \to M[M[U]]$ ซึ่งไม่ดีเลยเนื่องจากเราไม่ต้องการให้มี $M[M[A]]$ อีกหมวดหนึ่ง เพื่อจัดการกับปัญหานี้ เราขอแนะนำฟังก์ชันสุดท้าย: $flatMap: (T \to M[U]) \to (M[T] \to M[U])$

แต่ทำไมเราถึงต้องการทำเช่นนั้น? สมมติว่าเรากำลังตามขั้นตอนที่ 2 นั่นคือ เรามี $pure$, $constructor$ และ $map$ สมมติว่าเราต้องการดึงหน้า HTML จาก toptal.com จากนั้นสแกน URL ทั้งหมดที่นั่นและดึงออกมา ฉันจะสร้างฟังก์ชัน $fetch: String \to M[String]$ ที่ดึง URL เพียงรายการเดียวและส่งคืนหน้า HTML

จากนั้นฉันจะใช้ฟังก์ชันนี้กับ URL และรับหน้าจาก toptal.com ซึ่งก็คือ $x: M[String]$ ตอนนี้ ฉันแปลงไฟล์ $x$ และในที่สุดก็มาถึง URL $u: M[String]$ ฉันต้องการใช้ฟังก์ชัน $fetch$ กับมัน แต่ฉันทำไม่ได้ เพราะต้องใช้ประเภท $String$ ไม่ใช่ $M[String]$ นั่นเป็นเหตุผลที่เราต้องการ $flatMap$ เพื่อแปลง $fetch: String \to M[String]$ เป็น $m_fetch: M[String] \to M[String]$

ตอนนี้เราได้ทำทั้งสามขั้นตอนเสร็จแล้ว เราก็สามารถสร้างการแปลงค่าใดๆ ที่เราต้องการได้ ตัวอย่างเช่น หากคุณมีค่า $x$ ของประเภท $M[T]$ และ $f: T \to U$ คุณสามารถใช้ $map$ เพื่อใช้ $f$ กับค่า $x$ และรับค่า $y$ ประเภท $M[U]$ ด้วยวิธีนี้ การแปลงค่าใดๆ สามารถทำได้โดยปราศจากจุดบกพร่อง 100 เปอร์เซ็นต์ ตราบใดที่การใช้งาน $pure$, $constructor$, $map$ และ $flatMap$ ไม่มีจุดบกพร่อง

ดังนั้น แทนที่จะจัดการกับเอฟเฟกต์ที่น่ารังเกียจในแต่ละครั้งที่คุณพบพวกมันใน codebase คุณเพียงแค่ต้องตรวจสอบให้แน่ใจว่ามีเพียงสี่ฟังก์ชันเท่านั้นที่ได้รับการติดตั้งอย่างถูกต้อง ในตอนท้ายของโปรแกรม คุณจะได้รับเพียง $M[X]$ อันเดียว ซึ่งคุณสามารถแกะค่า $X$ ได้อย่างปลอดภัย และจัดการกับกรณีข้อผิดพลาดทั้งหมด

นี่คือสิ่งที่ monad เป็น : สิ่งที่ใช้ $pure$, $map$ และ $flatMap$ (จริงๆ แล้ว $map$ สามารถได้มาจาก $pure$ และ $flatMap$ แต่มันเป็นฟังก์ชันที่มีประโยชน์และแพร่หลายมาก ดังนั้นฉันจึงไม่ได้ละเว้นจากคำจำกัดความ)

The Option Monad หรือที่รู้จักในชื่อ Maybe Monad

โอเค มาดำดิ่งสู่การใช้งานจริงและการใช้งาน Monads กัน โมนาดที่มีประโยชน์มากอย่างแรกคือ โมนาตัวเลือก หากคุณมาจากภาษาโปรแกรมแบบคลาสสิก คุณอาจพบปัญหาการขัดข้องหลายครั้งเนื่องจากข้อผิดพลาดของตัวชี้ค่าว่าง (null pointer) ที่น่าอับอาย Tony Hoare ผู้ประดิษฐ์ null เรียกสิ่งประดิษฐ์นี้ว่า "ข้อผิดพลาดพันล้านดอลลาร์":

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

ดังนั้นเรามาลองปรับปรุงกัน monad ตัวเลือกอาจมีค่าที่ไม่ใช่ค่าว่างหรือไม่มีค่า ค่อนข้างคล้ายกับค่า null แต่มีโมนาดนี้ เราจึงสามารถใช้ฟังก์ชันที่กำหนดไว้อย่างดีของเราได้อย่างปลอดภัยโดยไม่ต้องกลัวข้อยกเว้นตัวชี้ค่าว่าง มาดูการใช้งานในภาษาต่างๆ กัน:

JavaScript—ตัวเลือก Monad/บางที Monad

 class Monad { // pure :: a -> M a pure = () => { throw "pure method needs to be implemented" } // flatMap :: # M a -> (a -> M b) -> M b flatMap = (x) => { throw "flatMap method needs to be implemented" } // map :: # M a -> (a -> b) -> M b map = f => this.flatMap(x => new this.pure(f(x))) } export class Option extends Monad { // pure :: a -> Option a pure = (value) => { if ((value === null) || (value === undefined)) { return none; } return new Some(value) } // flatMap :: # Option a -> (a -> Option b) -> Option b flatMap = f => this.constructor.name === 'None' ? none : f(this.value) // equals :: # M a -> M a -> boolean equals = (x) => this.toString() === x.toString() } class None extends Option { toString() { return 'None'; } } // Cached None class value export const none = new None() Option.pure = none.pure export class Some extends Option { constructor(value) { super(); this.value = value; } toString() { return `Some(${this.value})` } }

Python—ตัวเลือก Monad/บางที Monad

 class Monad: # pure :: a -> M a @staticmethod def pure(x): raise Exception("pure method needs to be implemented") # flat_map :: # M a -> (a -> M b) -> M b def flat_map(self, f): raise Exception("flat_map method needs to be implemented") # map :: # M a -> (a -> b) -> M b def map(self, f): return self.flat_map(lambda x: self.pure(f(x))) class Option(Monad): # pure :: a -> Option a @staticmethod def pure(x): return Some(x) # flat_map :: # Option a -> (a -> Option b) -> Option b def flat_map(self, f): if self.defined: return f(self.value) else: return nil class Some(Option): def __init__(self, value): self.value = value self.defined = True class Nil(Option): def __init__(self): self.value = None self.defined = False nil = Nil()

Ruby—ตัวเลือก Monad/บางที Monad

 class Monad # pure :: a -> M a def self.pure(x) raise StandardError("pure method needs to be implemented") end # pure :: a -> M a def pure(x) self.class.pure(x) end def flat_map(f) raise StandardError("flat_map method needs to be implemented") end # map :: # M a -> (a -> b) -> M b def map(f) flat_map(-> (x) { pure(f.call(x)) }) end end class Option < Monad attr_accessor :defined, :value # pure :: a -> Option a def self.pure(x) Some.new(x) end # pure :: a -> Option a def pure(x) Some.new(x) end # flat_map :: # Option a -> (a -> Option b) -> Option b def flat_map(f) if defined f.call(value) else $none end end end class Some < Option def initialize(value) @value = value @defined = true end end class None < Option def initialize() @defined = false end end $none = None.new()

Swift—Option Monad/บางที Monad

 import Foundation enum Maybe<A> { case None case Some(A) static func pure<B>(_ value: B) -> Maybe<B> { return .Some(value) } func flatMap<B>(_ f: (A) -> Maybe<B>) -> Maybe<B> { switch self { case .None: return .None case .Some(let value): return f(value) } } func map<B>(f: (A) -> B) -> Maybe<B> { return self.flatMap { type(of: self).pure(f($0)) } } }

สกาล่า—ตัวเลือก โมนาด/บางที โมนาด

 import language.higherKinds trait Monad[M[_]] { def pure[A](a: A): M[A] def flatMap[A, B](ma: M[A])(f: A => M[B]): M[B] def map[A, B](ma: M[A])(f: A => B): M[B] = flatMap(ma)(x => pure(f(x))) } object Monad { def apply[F[_]](implicit M: Monad[F]): Monad[F] = M implicit val myOptionMonad = new Monad[MyOption] { def pure[A](a: A) = MySome(a) def flatMap[A, B](ma: MyOption[A])(f: A => MyOption[B]): MyOption[B] = ma match { case MyNone => MyNone case MySome(a) => f(a) } } } sealed trait MyOption[+A] { def flatMap[B](f: A => MyOption[B]): MyOption[B] = Monad[MyOption].flatMap(this)(f) def map[B](f: A => B): MyOption[B] = Monad[MyOption].map(this)(f) } case object MyNone extends MyOption[Nothing] case class MySome[A](x: A) extends MyOption[A]

เราเริ่มต้นด้วยการนำคลาส Monad มาใช้ซึ่งจะเป็นพื้นฐานสำหรับการใช้งาน monad ทั้งหมดของเรา การมีคลาสนี้มีประโยชน์มาก เพราะการใช้เพียงสองวิธี — pure และ flatMap — สำหรับ monad เฉพาะ คุณจะได้รับวิธีการมากมายฟรี (เราจำกัดให้เหลือแค่วิธี map ในตัวอย่างของเรา แต่โดยทั่วไปมีหลายวิธี วิธีที่เป็นประโยชน์อื่นๆ เช่น sequence และการ traverse เพื่อทำงานกับอาร์เรย์ของ Monad )

เราสามารถแสดง map เป็นองค์ประกอบของ pure และ flatMap คุณสามารถดูได้จาก $ flatMap ลายเซ็นของ flatMap: (T \to M[U]) \to (M[T] \to M[U])$ ว่าใกล้เคียงกับ $map: (T \to U) \ ถึง (M[T] \to M[U])$ ความแตกต่างคือ $M$ เพิ่มเติมที่อยู่ตรงกลาง แต่เราสามารถใช้ฟังก์ชัน pure เพื่อแปลง $U$ เป็น $M[U]$ ด้วยวิธีนี้เราแสดง map ในแง่ของ flatMap และ pure

วิธีนี้ใช้ได้ผลดีสำหรับ Scala เพราะมันมีระบบประเภทขั้นสูง นอกจากนี้ยังใช้งานได้ดีกับ JS, Python และ Ruby เนื่องจากมีการพิมพ์แบบไดนามิก ขออภัย มันใช้ไม่ได้กับ Swift เพราะมันเป็นแบบสแตติกและไม่มีคุณสมบัติประเภทขั้นสูง เช่น ประเภทที่สูงกว่า ดังนั้นสำหรับ Swift เราจะต้องใช้ map สำหรับแต่ละโมนาด

นอกจากนี้ โปรดทราบว่า Monad ของ Option เป็นมาตรฐาน โดยพฤตินัย สำหรับภาษาอย่าง Swift และ Scala ดังนั้นเราจึงใช้ชื่อที่แตกต่างกันเล็กน้อยสำหรับการใช้งาน monad ของเรา

ตอนนี้เรามีคลาส Monad พื้นฐานแล้ว มาดูการใช้งาน monad ของ Option กัน ตามที่กล่าวไว้ก่อนหน้านี้ แนวคิดพื้นฐานคือ Option มีค่าบางอย่าง (เรียกว่า Some ) หรือหรือไม่มีค่าใดๆ เลย ( None )

วิธี pure เพียงส่งเสริมค่าเป็น Some ในขณะที่วิธี flatMap ตรวจสอบค่าปัจจุบันของ Option - หากเป็น None ก็จะส่งคืน None และหากเป็น Some ที่มีค่าพื้นฐาน จะแยกค่าพื้นฐาน ใช้ f() กับ และส่งคืนผลลัพธ์

โปรดทราบว่าเพียงแค่ใช้สองฟังก์ชันและ map นี้ เป็นไปไม่ได้ที่จะเกิดข้อยกเว้นตัวชี้ null เลยทีเดียว (ปัญหา อาจ เกิดขึ้นในการนำวิธี flatMap ใช้งาน แต่นั่นเป็นเพียงสองสามบรรทัดในโค้ดของเราที่เราตรวจสอบเพียงครั้งเดียว หลังจากนั้น เราเพียงแค่ใช้ Option monad ของเราตลอดทั้งโค้ดของเราในหลายพันแห่งและไม่ ต้องกลัวข้อยกเว้นตัวชี้ null เลย)

ทั้ง Monad

มาดำดิ่งสู่โมนาดที่สองกัน : ทั้งสองอย่าง โดยพื้นฐานแล้วจะเหมือนกับ Option monad แต่ด้วย Some เรียกว่า Right และ None เรียกว่า Left แต่คราวนี้ Left ได้รับอนุญาตให้มีค่าพื้นฐานได้เช่นกัน

เราต้องการสิ่งนั้นเพราะมันสะดวกมากที่จะแสดงข้อยกเว้น หากมีข้อยกเว้นเกิดขึ้น ค่าของ Either จะเป็น Left(Exception) ฟังก์ชัน flatMap จะไม่คืบหน้าหากค่าเป็น Left ซึ่งซ้ำความหมายของการส่งข้อยกเว้น: หากเกิดข้อยกเว้น เราจะหยุดการดำเนินการเพิ่มเติม

JavaScript—ทั้ง Monad

 import Monad from './monad'; export class Either extends Monad { // pure :: a -> Either a pure = (value) => { return new Right(value) } // flatMap :: # Either a -> (a -> Either b) -> Either b flatMap = f => this.isLeft() ? this : f(this.value) isLeft = () => this.constructor.name === 'Left' } export class Left extends Either { constructor(value) { super(); this.value = value; } toString() { return `Left(${this.value})` } } export class Right extends Either { constructor(value) { super(); this.value = value; } toString() { return `Right(${this.value})` } } // attempt :: (() -> a) -> M a Either.attempt = f => { try { return new Right(f()) } catch(e) { return new Left(e) } } Either.pure = (new Left(null)).pure

Python—ทั้ง Monad

 from monad import Monad class Either(Monad): # pure :: a -> Either a @staticmethod def pure(value): return Right(value) # flat_map :: # Either a -> (a -> Either b) -> Either b def flat_map(self, f): if self.is_left: return self else: return f(self.value) class Left(Either): def __init__(self, value): self.value = value self.is_left = True class Right(Either): def __init__(self, value): self.value = value self.is_left = False

ทับทิม—ทั้งโมนาด

 require_relative './monad' class Either < Monad attr_accessor :is_left, :value # pure :: a -> Either a def self.pure(value) Right.new(value) end # pure :: a -> Either a def pure(value) self.class.pure(value) end # flat_map :: # Either a -> (a -> Either b) -> Either b def flat_map(f) if is_left self else f.call(value) end end end class Left < Either def initialize(value) @value = value @is_left = true end end class Right < Either def initialize(value) @value = value @is_left = false end end

Swift—ทั้ง Monad

 import Foundation enum Either<A, B> { case Left(A) case Right(B) static func pure<C>(_ value: C) -> Either<A, C> { return Either<A, C>.Right(value) } func flatMap<D>(_ f: (B) -> Either<A, D>) -> Either<A, D> { switch self { case .Left(let x): return Either<A, D>.Left(x) case .Right(let x): return f(x) } } func map<C>(f: (B) -> C) -> Either<A, C> { return self.flatMap { Either<A, C>.pure(f($0)) } } }

สกาลา—ทั้งโมนาด

 package monad sealed trait MyEither[+E, +A] { def flatMap[EE >: E, B](f: A => MyEither[EE, B]): MyEither[EE, B] = Monad[MyEither[EE, ?]].flatMap(this)(f) def map[EE >: E, B](f: A => B): MyEither[EE, B] = Monad[MyEither[EE, ?]].map(this)(f) } case class MyLeft[E](e: E) extends MyEither[E, Nothing] case class MyRight[A](a: A) extends MyEither[Nothing, A] // ... implicit def myEitherMonad[E] = new Monad[MyEither[E, ?]] { def pure[A](a: A) = MyRight(a) def flatMap[A, B](ma: MyEither[E, A])(f: A => MyEither[E, B]): MyEither[E, B] = ma match { case MyLeft(a) => MyLeft(a) case MyRight(b) => f(b) } }

นอกจากนี้ โปรดทราบด้วยว่าการตรวจจับข้อยกเว้นทำได้ง่าย: สิ่งที่คุณต้องทำคือแมป Left to Right (แม้ว่า เราไม่ได้ทำในตัวอย่างของเรา เพื่อความกระชับ)

โมนาดแห่งอนาคต

มาสำรวจ Monad สุดท้ายที่เราต้องการ: Monad แห่งอนาคต The Future monad นั้นเป็นคอนเทนเนอร์สำหรับค่าที่สามารถใช้ได้ในขณะนี้หรือจะสามารถใช้ได้ในอนาคตอันใกล้ คุณสามารถสร้าง chains of Futures ด้วย map และ flatMap ที่จะรอให้ค่า Future ได้รับการแก้ไขก่อนที่จะรันโค้ดชิ้นถัดไปที่ขึ้นอยู่กับค่าที่จะถูกแก้ไขก่อน ซึ่งคล้ายกับแนวคิดของ Promises ใน JS

เป้าหมายการออกแบบของเราในตอนนี้คือการเชื่อมโยง async API ที่มีอยู่ในภาษาต่างๆ เข้ากับฐานเดียวที่สอดคล้องกัน ปรากฎว่าวิธีการออกแบบที่ง่ายที่สุดคือการใช้การเรียกกลับใน $constructor$

แม้ว่าการออกแบบการโทรกลับจะแนะนำปัญหา callback hell ใน JavaScript และภาษาอื่นๆ จะไม่มีปัญหาสำหรับเรา เนื่องจากเราใช้ monads อันที่จริง Promise object ซึ่งเป็นพื้นฐานสำหรับโซลูชันของ JavaScript เพื่อเรียกกลับนรกคือ Monad นั่นเอง!

แล้วผู้สร้าง Monad ในอนาคตล่ะ? มีลายเซ็นนี้:

constructor :: ((Either err a -> void) -> void) -> Future (Either err a)

มาแบ่งมันเป็นชิ้นๆ ขั้นแรกให้กำหนด:

type Callback = Either err a -> void

ดังนั้น Callback จึงเป็นฟังก์ชันที่รับข้อผิดพลาดหรือค่าที่แก้ไขเป็นอาร์กิวเมนต์ และไม่คืนค่าใดๆ ตอนนี้ลายเซ็นของเรามีลักษณะดังนี้:

constructor :: (Callback -> void) -> Future (Either err a)

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

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

เมื่ออนาคตได้รับการแก้ไขแล้ว การเรียกกลับแต่ละรายการในรายการนี้จะถูกทริกเกอร์เพียงครั้งเดียวด้วยค่าที่แก้ไขแล้วในเธรดแยกกัน (หรือเป็นฟังก์ชันถัดไปที่จะดำเนินการในลูปเหตุการณ์ ในกรณีของ JS) โปรดทราบว่าสิ่งสำคัญคือต้อง ใช้พื้นฐานการซิงโครไนซ์อย่างระมัดระวัง มิฉะนั้นเงื่อนไขการแข่งขันจะเป็นไปได้

โฟลว์พื้นฐานคือ: คุณเริ่มการคำนวณแบบอะซิงโครนัสที่ให้มาเป็นอาร์กิวเมนต์ตัวสร้าง และชี้การเรียกกลับนั้นไปยังเมธอดการเรียกกลับภายในของเรา ในระหว่างนี้ คุณสามารถสมัครใช้งาน Future monad และใส่การโทรกลับของคุณในคิวได้ เมื่อคำนวณเสร็จแล้ว วิธีการโทรกลับภายในจะเรียกการเรียกกลับทั้งหมดในคิว หากคุณคุ้นเคยกับ Reactive Extensions (RxJS, RxSwift เป็นต้น) พวกเขาจะใช้แนวทางที่คล้ายกันมากในการจัดการ async

API สาธารณะของ Monad ในอนาคตประกอบด้วย pure , map และ flatMap เช่นเดียวกับใน monads ก่อนหน้า เรายังต้องการวิธีการที่สะดวกสองสามวิธี:

  1. async ซึ่งใช้ฟังก์ชันการบล็อกแบบซิงโครนัสและดำเนินการบนเธรดแยกต่างหากและ
  2. traverse ซึ่งใช้อาร์เรย์ของค่าและฟังก์ชันที่จับคู่ค่ากับ Future และส่งคืนค่า Future ของอาร์เรย์ของค่าที่แก้ไขได้

มาดูกันว่ามันเล่นอย่างไร:

JavaScript—โมนาดในอนาคต

 import Monad from './monad'; import { Either, Left, Right } from './either'; import { none, Some } from './option'; export class Future extends Monad { // constructor :: ((Either err a -> void) -> void) -> Future (Either err a) constructor(f) { super(); this.subscribers = []; this.cache = none; f(this.callback) } // callback :: Either err a -> void callback = (value) => { this.cache = new Some(value) while (this.subscribers.length) { const subscriber = this.subscribers.shift(); subscriber(value) } } // subscribe :: (Either err a -> void) -> void subscribe = (subscriber) => (this.cache === none ? this.subscribers.push(subscriber) : subscriber(this.cache.value)) toPromise = () => new Promise( (resolve, reject) => this.subscribe(val => val.isLeft() ? reject(val.value) : resolve(val.value)) ) // pure :: a -> Future a pure = Future.pure // flatMap :: (a -> Future b) -> Future b flatMap = f => new Future( cb => this.subscribe(value => value.isLeft() ? cb(value) : f(value.value).subscribe(cb)) ) } Future.async = (nodeFunction, ...args) => { return new Future(cb => nodeFunction(...args, (err, data) => err ? cb(new Left(err)) : cb(new Right(data))) ); } Future.pure = value => new Future(cb => cb(Either.pure(value))) // traverse :: [a] -> (a -> Future b) -> Future [b] Future.traverse = list => f => list.reduce( (acc, elem) => acc.flatMap(values => f(elem).map(value => [...values, value])), Future.pure([]) )

Python—อนาคต Monad

 from monad import Monad from option import nil, Some from either import Either, Left, Right from functools import reduce import threading class Future(Monad): # __init__ :: ((Either err a -> void) -> void) -> Future (Either err a) def __init__(self, f): self.subscribers = [] self.cache = nil self.semaphore = threading.BoundedSemaphore(1) f(self.callback) # pure :: a -> Future a @staticmethod def pure(value): return Future(lambda cb: cb(Either.pure(value))) def exec(f, cb): try: data = f() cb(Right(data)) except Exception as err: cb(Left(err)) def exec_on_thread(f, cb): t = threading.Thread(target=Future.exec, args=[f, cb]) t.start() def async(f): return Future(lambda cb: Future.exec_on_thread(f, cb)) # flat_map :: (a -> Future b) -> Future b def flat_map(self, f): return Future( lambda cb: self.subscribe( lambda value: cb(value) if (value.is_left) else f(value.value).subscribe(cb) ) ) # traverse :: [a] -> (a -> Future b) -> Future [b] def traverse(arr): return lambda f: reduce( lambda acc, elem: acc.flat_map( lambda values: f(elem).map( lambda value: values + [value] ) ), arr, Future.pure([])) # callback :: Either err a -> void def callback(self, value): self.semaphore.acquire() self.cache = Some(value) while (len(self.subscribers) > 0): sub = self.subscribers.pop(0) t = threading.Thread(target=sub, args=[value]) t.start() self.semaphore.release() # subscribe :: (Either err a -> void) -> void def subscribe(self, subscriber): self.semaphore.acquire() if (self.cache.defined): self.semaphore.release() subscriber(self.cache.value) else: self.subscribers.append(subscriber) self.semaphore.release()

ทับทิม—โมนาดในอนาคต

 require_relative './monad' require_relative './either' require_relative './option' class Future < Monad attr_accessor :subscribers, :cache, :semaphore # initialize :: ((Either err a -> void) -> void) -> Future (Either err a) def initialize(f) @subscribers = [] @cache = $none @semaphore = Queue.new @semaphore.push(nil) f.call(method(:callback)) end # pure :: a -> Future a def self.pure(value) Future.new(-> (cb) { cb.call(Either.pure(value)) }) end def self.async(f, *args) Future.new(-> (cb) { Thread.new { begin cb.call(Right.new(f.call(*args))) rescue => e cb.call(Left.new(e)) end } }) end # pure :: a -> Future a def pure(value) self.class.pure(value) end # flat_map :: (a -> Future b) -> Future b def flat_map(f) Future.new(-> (cb) { subscribe(-> (value) { if (value.is_left) cb.call(value) else f.call(value.value).subscribe(cb) end }) }) end # traverse :: [a] -> (a -> Future b) -> Future [b] def self.traverse(arr, f) arr.reduce(Future.pure([])) do |acc, elem| acc.flat_map(-> (values) { f.call(elem).map(-> (value) { values + [value] }) }) end end # callback :: Either err a -> void def callback(value) semaphore.pop self.cache = Some.new(value) while (subscribers.count > 0) sub = self.subscribers.shift Thread.new { sub.call(value) } end semaphore.push(nil) end # subscribe :: (Either err a -> void) -> void def subscribe(subscriber) semaphore.pop if (self.cache.defined) semaphore.push(nil) subscriber.call(cache.value) else self.subscribers.push(subscriber) semaphore.push(nil) end end end

Swift—อนาคต Monad

 import Foundation let background = DispatchQueue(label: "background", attributes: .concurrent) class Future<Err, A> { typealias Callback = (Either<Err, A>) -> Void var subscribers: Array<Callback> = Array<Callback>() var cache: Maybe<Either<Err, A>> = .None var semaphore = DispatchSemaphore(value: 1) lazy var callback: Callback = { value in self.semaphore.wait() self.cache = .Some(value) while (self.subscribers.count > 0) { let subscriber = self.subscribers.popLast() background.async { subscriber?(value) } } self.semaphore.signal() } init(_ f: @escaping (@escaping Callback) -> Void) { f(self.callback) } func subscribe(_ cb: @escaping Callback) { self.semaphore.wait() switch cache { case .None: subscribers.append(cb) self.semaphore.signal() case .Some(let value): self.semaphore.signal() cb(value) } } static func pure<B>(_ value: B) -> Future<Err, B> { return Future<Err, B> { $0(Either<Err, B>.pure(value)) } } func flatMap<B>(_ f: @escaping (A) -> Future<Err, B>) -> Future<Err, B> { return Future<Err, B> { [weak self] cb in guard let this = self else { return } this.subscribe { value in switch value { case .Left(let err): cb(Either<Err, B>.Left(err)) case .Right(let x): f(x).subscribe(cb) } } } } func map<B>(_ f: @escaping (A) -> B) -> Future<Err, B> { return self.flatMap { Future<Err, B>.pure(f($0)) } } static func traverse<B>(_ list: Array<A>, _ f: @escaping (A) -> Future<Err, B>) -> Future<Err, Array<B>> { return list.reduce(Future<Err, Array<B>>.pure(Array<B>())) { (acc: Future<Err, Array<B>>, elem: A) in return acc.flatMap { elems in return f(elem).map { val in return elems + [val] } } } } }

สกาลา—โมนาดในอนาคต

 package monad import java.util.concurrent.Semaphore class MyFuture[A] { private var subscribers: List[MyEither[Exception, A] => Unit] = List() private var cache: MyOption[MyEither[Exception, A]] = MyNone private val semaphore = new Semaphore(1) def this(f: (MyEither[Exception, A] => Unit) => Unit) { this() f(this.callback _) } def flatMap[B](f: A => MyFuture[B]): MyFuture[B] = Monad[MyFuture].flatMap(this)(f) def map[B](f: A => B): MyFuture[B] = Monad[MyFuture].map(this)(f) def callback(value: MyEither[Exception, A]): Unit = { semaphore.acquire cache = MySome(value) subscribers.foreach { sub => val t = new Thread( new Runnable { def run: Unit = { sub(value) } } ) t.start } subscribers = List() semaphore.release } def subscribe(sub: MyEither[Exception, A] => Unit): Unit = { semaphore.acquire cache match { case MyNone => subscribers = sub :: subscribers semaphore.release case MySome(value) => semaphore.release sub(value) } } } object MyFuture { def async[B, C](f: B => C, arg: B): MyFuture[C] = new MyFuture[C]({ cb => val t = new Thread( new Runnable { def run: Unit = { try { cb(MyRight(f(arg))) } catch { case e: Exception => cb(MyLeft(e)) } } } ) t.start }) def traverse[A, B](list: List[A])(f: A => MyFuture[B]): MyFuture[List[B]] = { list.foldRight(Monad[MyFuture].pure(List[B]())) { (elem, acc) => Monad[MyFuture].flatMap(acc) ({ values => Monad[MyFuture].map(f(elem)) { value => value :: values } }) } } } // ... implicit val myFutureMonad = new Monad[MyFuture] { def pure[A](a: A): MyFuture[A] = new MyFuture[A]({ cb => cb(myEitherMonad[Exception].pure(a)) }) def flatMap[A, B](ma: MyFuture[A])(f: A => MyFuture[B]): MyFuture[B] = new MyFuture[B]({ cb => ma.subscribe(_ match { case MyLeft(e) => cb(MyLeft(e)) case MyRight(a) => f(a).subscribe(cb) }) }) }

ตอนนี้ สังเกตว่า API สาธารณะของ Future ไม่มีรายละเอียดระดับต่ำ เช่น เธรด สัญญาณ หรือสิ่งอื่นใด สิ่งที่คุณต้องมีก็คือการจัดหาบางสิ่งด้วยการโทรกลับ แค่นั้นเอง!

เขียนโปรแกรมจาก Monads

โอเค งั้นเรามาลองใช้ Monads ของเราสร้างโปรแกรมกัน สมมติว่าเรามีไฟล์ที่มีรายการ URL และเราต้องการดึงข้อมูล URL เหล่านี้ไปพร้อมกัน จากนั้น เราต้องการตัดการตอบกลับเป็น 200 ไบต์แต่ละรายการเพื่อให้กระชับและพิมพ์ผลลัพธ์ออกมา

เราเริ่มต้นด้วยการแปลง API ภาษาที่มีอยู่เป็นอินเทอร์เฟซ monadic (ดูฟังก์ชัน readFile และ fetch ) ตอนนี้เรามีสิ่งนั้นแล้ว เราสามารถเขียนมันเพื่อให้ได้ผลลัพธ์สุดท้ายเป็นลูกโซ่เดียว โปรดทราบว่าโซ่นั้นปลอดภัยอย่างยิ่ง เนื่องจากรายละเอียดที่เต็มไปด้วยเลือดทั้งหมดมีอยู่ใน Monads

JavaScript—ตัวอย่างโปรแกรม Monad

 import { Future } from './future'; import { Either, Left, Right } from './either'; import { readFile } from 'fs'; import https from 'https'; const getResponse = url => new Future(cb => https.get(url, res => { var body = ''; res.on('data', data => body += data); res.on('end', data => cb(new Right(body))); res.on('error', err => cb(new Left(err))) })) const getShortResponse = url => getResponse(url).map(resp => resp.substring(0, 200)) Future .async(readFile, 'resources/urls.txt') .map(data => data.toString().split("\n")) .flatMap(urls => Future.traverse(urls)(getShortResponse)) .map(console.log)

Python—Sample Monad Program

 import http.client import threading import time import os from future import Future from either import Either, Left, Right conn = http.client.HTTPSConnection("en.wikipedia.org") def read_file_sync(uri): base_dir = os.path.dirname(__file__) #<-- absolute dir the script is in path = os.path.join(base_dir, uri) with open(path) as f: return f.read() def fetch_sync(uri): conn.request("GET", uri) r = conn.getresponse() return r.read().decode("utf-8")[:200] def read_file(uri): return Future.async(lambda: read_file_sync(uri)) def fetch(uri): return Future.async(lambda: fetch_sync(uri)) def main(args=None): lines = read_file("../resources/urls.txt").map(lambda res: res.splitlines()) content = lines.flat_map(lambda urls: Future.traverse(urls)(fetch)) output = content.map(lambda res: print("\n".join(res))) if __name__ == "__main__": main()

Ruby—Sample Monad Program

 require './lib/future' require 'net/http' require 'uri' semaphore = Queue.new def read(uri) Future.async(-> () { File.read(uri) }) end def fetch(url) Future.async(-> () { uri = URI(url) Net::HTTP.get_response(uri).body[0..200] }) end read("resources/urls.txt") .map(-> (x) { x.split("\n") }) .flat_map(-> (urls) { Future.traverse(urls, -> (url) { fetch(url) }) }) .map(-> (res) { puts res; semaphore.push(true) }) semaphore.pop

Swift—Sample Monad Program

 import Foundation enum Err: Error { case Some(String) } func readFile(_ path: String) -> Future<Error, String> { return Future<Error, String> { callback in background.async { let url = URL(fileURLWithPath: path) let text = try? String(contentsOf: url) if let res = text { callback(Either<Error, String>.pure(res)) } else { callback(Either<Error, String>.Left(Err.Some("Error reading urls.txt"))) } } } } func fetchUrl(_ url: String) -> Future<Error, String> { return Future<Error, String> { callback in background.async { let url = URL(string: url) let task = URLSession.shared.dataTask(with: url!) {(data, response, error) in if let err = error { callback(Either<Error, String>.Left(err)) return } guard let nonEmptyData = data else { callback(Either<Error, String>.Left(Err.Some("Empty response"))) return } guard let result = String(data: nonEmptyData, encoding: String.Encoding.utf8) else { callback(Either<Error, String>.Left(Err.Some("Cannot decode response"))) return } let index = result.index(result.startIndex, offsetBy: 200) callback(Either<Error, String>.pure(String(result[..<index]))) } task.resume() } } } var result: Any = "" let _ = readFile("\(projectDir)/Resources/urls.txt") .map { data -> [String] in data.components(separatedBy: "\n").filter { (line: String) in !line.isEmpty } }.flatMap { urls in return Future<Error, String>.traverse(urls) { url in return fetchUrl(url) } }.map { responses in print(responses) } RunLoop.main.run()

Scala—Sample Monad Program

 import scala.io.Source import java.util.concurrent.Semaphore import monad._ object Main extends App { val semaphore = new Semaphore(0) def readFile(name: String): MyFuture[List[String]] = MyFuture.async[String, List[String]](filename => Source.fromResource(filename).getLines.toList, name) def fetch(url: String): MyFuture[String] = MyFuture.async[String, String]( uri => Source.fromURL(uri).mkString.substring(0, 200), url ) val future = for { urls <- readFile("urls.txt") entries <- MyFuture.traverse(urls)(fetch _) } yield { println(entries) semaphore.release } semaphore.acquire }

There you have it—monad solutions in practice. You can find a repo containing all the code from this article on GitHub.

Overhead: Done. Benefits: Ongoing

For this simple monad-based program, it might look like overkill to use all the code that we wrote before. But that's just the initial setup, and it will stay constant in its size. Imagine that from now on, using monads, you can write a lot of async code, not worrying about threads, race conditions, semaphores, exceptions, or null pointers! สุดยอด!