อธิบายใน 5 นาที: Monads ใน JavaScript ธรรมดา

Dec 22 2022
แม้จะมีภาพหน้าปกที่ดูน่ากลัว แต่ฉันจะพยายามอธิบายพระโดยใช้คณิตศาสตร์ให้ยากที่สุดเท่าที่จะทำได้ ไม่มีการกล่าวถึงทฤษฎีหมวดหมู่ ไม่มี Kleisli ไม่มีแม้แต่ Curry
ภาพถ่ายโดย Dan Cristian Pădureț บน Unsplash

แม้จะมีภาพหน้าปกที่ดูน่ากลัว แต่ฉันจะพยายามอธิบายพระโดยใช้คณิตศาสตร์ให้ยากที่สุดเท่าที่จะทำได้ ไม่มีการกล่าวถึงทฤษฎีหมวดหมู่ ไม่มี Kleisli ไม่มีแม้แต่ Curry แม้ว่าจะมีNaN.

ในคำพูดของ Douglas Crockford ผู้ยิ่งใหญ่:

นอกจากจะดีและมีประโยชน์แล้ว มันยังถูกสาปอีกด้วย และคำสาปของ Monad คือเมื่อคุณได้รับสิ่งศักดิ์สิทธิ์ เมื่อคุณเข้าใจ - "โอ้ นั่นคือสิ่งที่มันเป็น" - คุณจะสูญเสียความสามารถในการอธิบายให้คนอื่นฟัง

ฉันมีทฤษฎีหนึ่ง — นี่เป็นเพราะในบรรดาบทความที่ยอดเยี่ยมเกี่ยวกับเรื่องนี้ ซึ่งให้คำอธิบายที่ดีเกี่ยวกับทฤษฎีทางคณิตศาสตร์ที่อยู่เบื้องหลัง Monads และวิธีที่พวกเขาควรนำไปใช้ (พร้อมกับตัวอย่างที่สวยงาม!) มีสิ่งหนึ่งที่ผิด:

ฉันต้องการสิ่งเหล่านั้นเพื่ออะไร

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

ตกลงตอนนี้เรามีสิ่งนั้นแล้ว - โมนาดคืออะไร? วิธีที่ง่ายที่สุดในการพูดก็คือ — มันห่อหุ้มค่าบางอย่างไว้ ในแง่ของการเขียนโปรแกรมเชิงฟังก์ชัน monad คือfunctorซึ่งเป็นโครงสร้างที่อนุญาตให้ใช้ฟังก์ชันกับมัน เป็นชั้นพิเศษของสิ่งที่เป็นนามธรรมระหว่างค่าและการประมวลผลอะไรก็ตามที่เราต้องการใส่ค่าลงไป แต่นั่นไม่ได้บอกอะไรเราเลย

มาดู monad ที่ง่ายที่สุดที่คุณอาจจินตนาการได้: การนำIdentitymonad ไปใช้:

const Identity = (value) => ({
  value: () => value,
  map: (fn) => Identity(fn(value)),
});

ลองดูตัวอย่างที่ง่ายที่สุด:

const addTax = (value) => value * 1.1;

const cartValue = Identity(100);

const cartValueWithTax = cartValue
  .map(addTax) // At this point this is Identity(110)
  .value(); // Equal to 110 as a number

ขอบคุณภาพจาก https://makeameme.org/meme/Please-bear-with-tmby8v

Identitymonad มี - เช่นเดียวกับฟังก์ชันidentityในไลบรารีการเขียนโปรแกรมเชิงฟังก์ชัน - เป็นตรรกะที่ค่อนข้างโปร่งใส เป็นตัวยึดตำแหน่ง monad เช่นเดียวกับidentityฟังก์ชันตัวยึดตำแหน่ง การใช้ มูลค่าของเราได้ผลดีสำหรับสถานการณ์ข้างต้น แต่ลองคิดดูว่าจะเกิดอะไรขึ้นหากมูลค่าของรถเข็น ของaddTaxเราเท่ากับ undefinedด้วยวิธีการที่ยอดเยี่ยมของ JavaScriptเราจะได้รับไฟล์NaN.

โดยปกติ เพื่อหลีกเลี่ยงปัญหานี้ วิธีที่ใช้งานง่ายที่สุดคือเพิ่มการตรวจสอบค่าให้กับaddTaxฟังก์ชัน นี่เป็นแนวทางที่ผิด มันบดบังวัตถุประสงค์ของการทำงานและทำให้บริสุทธิ์น้อยลง ฟังก์ชันaddTaxควรทำเพียงสิ่งเดียวและสิ่งเดียวเท่านั้น: add.Tax

ตกลง, wiseguy - คุณอาจพูดว่า - เราควรคำนึงถึงundefinedคุณค่าอย่างไร? นั่นคือสิ่งที่เรามี monads สำหรับ เราต้องการรักษาฟังก์ชัน ให้ บริสุทธิ์ เรายังต้องการให้ตรรกะของเราทำงานแตกต่างกันไปตามค่าที่เราส่งผ่านไปยังฟังก์ชัน สถานที่ที่ดีที่สุดในการกำหนดพฤติกรรมนั้นคือfunctor !

มาดูกันว่า Monad ที่ได้รับความนิยมมากที่สุดคือ: Maybe. นี่อาจเป็นการใช้งานขั้นพื้นฐาน (ฉันออกจากวิธีการปกติที่คุณอาจพบใน monad ofเพื่อความชัดเจน):

const Maybe = (value) => ({
  value: () => value,
  map: (fn) => value === undefined || value === null 
    ? Maybe(null) 
    : Maybe(fn(value)),
});

const primitiveCart = 100;
const undefinedCart = undefined;
const nullishCart = null;

const maybeCart = Maybe(100);
const maybeUndefinedCart = Maybe(undefined);
const maybeNullishCart = Maybe(null);


addTax(primitiveCart); // boring, 110
addTax(undefinedCart); // NaN
addTax(nullishCart); // 0 - thanks, JavaScript!

maybeCart.map(addTax).value(); // 110
maybeUndefinedCart.map(addTax).value(); // null
maybeNullishCart.map(addTax).value(); // null

แน่นอนว่านี่เป็นเพียงรอยขีดข่วนของความเป็นไปได้มากมายที่การใช้ monads นำมา สมมติว่าเราต้องการทักทายแขกทุกคนที่เข้ามาในโรงแรมของเรา:

const greet = (name) => `Hello, ${name}!`;

ลองมาดูตัวอย่างพื้นฐานอีกตัวอย่างหนึ่ง — the Optionalmonad:

const Optional = (value) => ({
  value: () => value,
  orElse: (alternativeValue) => value 
    ? Optional(value) 
    : Optional(alternativeValue),
  map: (fn) => Optional(fn(value)),
});

const firstGuest = "John";
const anonymousGuest = undefined;

greet(firstGuest); // "Hello, John!"
greet(anonymousGuest); // "Hello, undefined" - this is a bit embarrasing

const OptionalWithStrangerFallback = (name) => Optional(name)
  .orElse("stranger");

const firstGuestOptional = OptionalWithStrangerFallback(firstGuest);
const anonymousGuestOptional = OptionalWithStrangerFallback(anonymousGuest);

// I really hope you know where this is going at this point

firstGuestOptional.map(greet).value(); "Hello, John!"
anonymousGuestOptional.map(greet).value(); "Hello, stranger!"

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