วิธีการจำลองตัวอย่างนี้
สิ่งนี้จะถูกจำลองด้วย Reader monad ได้อย่างไร?
ฉันไม่แน่ใจว่าควรจะจำลองด้วย Reader หรือไม่ แต่สามารถทำได้โดย:
- การเข้ารหัสคลาสเป็นฟังก์ชันที่ทำให้โค้ดเล่นได้ดีกว่ากับ Reader
- การเขียนฟังก์ชั่นด้วย Reader เพื่อความเข้าใจและการใช้งาน
ก่อนเริ่มต้นฉันต้องบอกคุณเกี่ยวกับการปรับโค้ดตัวอย่างเล็กน้อยที่ฉันรู้สึกว่าเป็นประโยชน์สำหรับคำตอบนี้ การเปลี่ยนแปลงครั้งแรกเป็นเรื่องเกี่ยวกับFindUsers.inactive
วิธีการ ฉันปล่อยให้มันกลับมาList[String]
เพื่อให้สามารถใช้รายการที่อยู่ในUserReminder.emailInactive
วิธีการ ฉันยังได้เพิ่มการใช้งานอย่างง่ายให้กับวิธีการ สุดท้ายตัวอย่างจะใช้ Reader monad รุ่นรีดด้วยมือต่อไปนี้:
case class Reader[Conf, T](read: Conf => T) { self =>
def map[U](convert: T => U): Reader[Conf, U] =
Reader(self.read andThen convert)
def flatMap[V](toReader: T => Reader[Conf, V]): Reader[Conf, V] =
Reader[Conf, V](conf => toReader(self.read(conf)).read(conf))
def local[BiggerConf](extractFrom: BiggerConf => Conf): Reader[BiggerConf, T] =
Reader[BiggerConf, T](extractFrom andThen self.read)
}
object Reader {
def pure[C, A](a: A): Reader[C, A] =
Reader(_ => a)
implicit def funToReader[Conf, A](read: Conf => A): Reader[Conf, A] =
Reader(read)
}
ขั้นตอนการสร้างโมเดล 1. คลาสการเข้ารหัสเป็นฟังก์ชัน
อาจจะเป็นทางเลือกฉันไม่แน่ใจ แต่ในภายหลังมันทำให้การเข้าใจดูดีขึ้น โปรดทราบว่าฟังก์ชันผลลัพธ์เป็นแบบ curried นอกจากนี้ยังใช้อาร์กิวเมนต์ constructor เดิมเป็นพารามิเตอร์แรก (รายการพารามิเตอร์) ทางนั้น
class Foo(dep: Dep) {
def bar(arg: Arg): Res = ???
}
กลายเป็น
object Foo {
def bar: Dep => Arg => Res = ???
}
เก็บไว้ในใจว่าแต่ละDep
, Arg
, Res
ประเภทสามารถ arbitrary สมบูรณ์: สิ่งอันดับฟังก์ชั่นหรือประเภทที่เรียบง่าย
นี่คือโค้ดตัวอย่างหลังจากการปรับเปลี่ยนครั้งแรกเปลี่ยนเป็นฟังก์ชัน:
trait Datastore { def runQuery(query: String): List[String] }
trait EmailServer { def sendEmail(to: String, content: String): Unit }
object FindUsers {
def inactive: Datastore => () => List[String] =
dataStore => () => dataStore.runQuery("select inactive")
}
object UserReminder {
def emailInactive(inactive: () => List[String]): EmailServer => () => Unit =
emailServer => () => inactive().foreach(emailServer.sendEmail(_, "We miss you"))
}
object CustomerRelations {
def retainUsers(emailInactive: () => Unit): () => Unit =
() => {
println("emailing inactive users")
emailInactive()
}
}
สิ่งหนึ่งที่สังเกตได้คือฟังก์ชันเฉพาะไม่ได้ขึ้นอยู่กับวัตถุทั้งหมด แต่เฉพาะส่วนที่ใช้โดยตรงเท่านั้น โดยที่ในUserReminder.emailInactive()
อินสแตนซ์เวอร์ชัน OOP จะเรียกuserFinder.inactive()
ที่นี่เพียงแค่เรียกinactive()
- ฟังก์ชันที่ส่งผ่านไปยังพารามิเตอร์แรก
โปรดทราบว่าโค้ดแสดงคุณสมบัติที่ต้องการสามประการจากคำถาม:
- เป็นที่ชัดเจนว่าแต่ละฟังก์ชันต้องการการอ้างอิงประเภทใด
- ซ่อนการอ้างอิงของฟังก์ชันหนึ่งจากฟังก์ชันอื่น
retainUsers
เมธอดไม่จำเป็นต้องรู้เกี่ยวกับการพึ่งพา Datastore
การสร้างแบบจำลองขั้นตอนที่ 2 การใช้ Reader เพื่อเขียนฟังก์ชันและเรียกใช้งาน
Reader monad ให้คุณเขียนเฉพาะฟังก์ชันที่ขึ้นอยู่กับประเภทเดียวกันเท่านั้น กรณีนี้มักไม่เกิดขึ้น ในตัวอย่างของเรา
FindUsers.inactive
ขึ้นอยู่กับDatastore
และบนUserReminder.emailInactive
EmailServer
ในการแก้ปัญหานั้นเราสามารถแนะนำประเภทใหม่ (มักเรียกว่า Config) ที่มีการอ้างอิงทั้งหมดจากนั้นเปลี่ยนฟังก์ชันเพื่อให้ทั้งหมดขึ้นอยู่กับมันและรับเฉพาะข้อมูลที่เกี่ยวข้องเท่านั้น เห็นได้ชัดว่าผิดจากมุมมองการจัดการการพึ่งพาเนื่องจากวิธีที่คุณสร้างฟังก์ชันเหล่านี้ขึ้นอยู่กับประเภทที่พวกเขาไม่ควรรู้ในตอนแรก
โชคดีที่ปรากฎว่ามีวิธีทำให้ฟังก์ชันใช้งานConfig
ได้แม้ว่าจะยอมรับเพียงบางส่วนเป็นพารามิเตอร์ก็ตาม เป็นวิธีการที่เรียกว่าlocal
กำหนดไว้ใน Reader จำเป็นต้องมีวิธีแยกส่วนที่เกี่ยวข้องออกจากไฟล์Config
.
ความรู้นี้นำไปใช้กับตัวอย่างในมือจะมีลักษณะดังนี้:
object Main extends App {
case class Config(dataStore: Datastore, emailServer: EmailServer)
val config = Config(
new Datastore { def runQuery(query: String) = List("john.doe@fizzbuzz.com") },
new EmailServer { def sendEmail(to: String, content: String) = println(s"sending [$content] to $to") }
)
import Reader._
val reader = for {
getAddresses <- FindUsers.inactive.local[Config](_.dataStore)
emailInactive <- UserReminder.emailInactive(getAddresses).local[Config](_.emailServer)
retainUsers <- pure(CustomerRelations.retainUsers(emailInactive))
} yield retainUsers
reader.read(config)()
}
ข้อดีของการใช้พารามิเตอร์ตัวสร้าง
การใช้ Reader Monad สำหรับ "แอปพลิเคชันทางธุรกิจ" ในแง่มุมใดจะดีกว่าการใช้พารามิเตอร์ตัวสร้างเพียงอย่างเดียว
ฉันหวังว่าการเตรียมคำตอบนี้จะทำให้ง่ายต่อการตัดสินด้วยตัวคุณเองว่าจะเอาชนะตัวสร้างธรรมดาในแง่มุมใดได้บ้าง แต่ถ้าฉันจะแจกแจงสิ่งเหล่านี้นี่คือรายการของฉัน ข้อจำกัดความรับผิดชอบ: ฉันมีพื้นหลัง OOP และฉันอาจไม่พอใจ Reader และ Kleisli อย่างเต็มที่เพราะฉันไม่ได้ใช้มัน
- ความสม่ำเสมอ - ไม่ว่าเนื้อหาจะสั้น / ยาวเพียงใดเพื่อความเข้าใจมันเป็นเพียง Reader และคุณสามารถเขียนด้วยอินสแตนซ์อื่นได้อย่างง่ายดายบางทีอาจจะแนะนำประเภท Config อีกหนึ่งประเภทและเพิ่ม
local
สายเรียกเข้าไว้ด้านบน ประเด็นนี้คือ IMO ค่อนข้างเป็นเรื่องของรสนิยมเพราะเมื่อคุณใช้คอนสตรัคเตอร์ไม่มีใครขัดขวางไม่ให้คุณแต่งสิ่งที่คุณชอบเว้นแต่จะมีใครทำอะไรโง่ ๆ เช่นทำงานในคอนสตรัคเตอร์ซึ่งถือว่าเป็นการปฏิบัติที่ไม่ดีใน OOP
- Reader เป็น monad เพื่อที่จะได้รับผลประโยชน์ทั้งหมดที่เกี่ยวข้องกับที่ -
sequence
, traverse
วิธีการดำเนินการฟรี
- ในบางกรณีคุณอาจพบว่าควรสร้าง Reader เพียงครั้งเดียวและใช้สำหรับ Configs ที่หลากหลาย ด้วยตัวสร้างไม่มีใครขัดขวางคุณในการทำเช่นนั้นคุณเพียงแค่ต้องสร้างกราฟออบเจ็กต์ทั้งหมดใหม่สำหรับทุก Config ที่เข้ามา แม้ว่าฉันจะไม่มีปัญหากับสิ่งนั้น (ฉันชอบทำแบบนั้นกับทุกคำขอในการสมัคร) แต่ก็ไม่ใช่ความคิดที่ชัดเจนสำหรับหลาย ๆ คนด้วยเหตุผลที่ฉันอาจคาดเดาได้เท่านั้น
- Reader ผลักดันให้คุณใช้ฟังก์ชันต่างๆมากขึ้นซึ่งจะเล่นได้ดีขึ้นกับแอปพลิเคชันที่เขียนในรูปแบบ FP ส่วนใหญ่
- ผู้อ่านแยกความกังวล คุณสามารถสร้างโต้ตอบกับทุกสิ่งกำหนดตรรกะโดยไม่ต้องให้การอ้างอิง จริงจัดหาในภายหลังแยกต่างหาก (ขอบคุณ Ken Scrambler สำหรับประเด็นนี้) สิ่งนี้มักจะได้ยินถึงข้อดีของ Reader แต่ก็เป็นไปได้ด้วยตัวสร้างธรรมดา
ฉันอยากจะบอกสิ่งที่ฉันไม่ชอบใน Reader
- การตลาด. บางครั้งฉันรู้สึกประทับใจที่ Reader ทำการตลาดสำหรับการอ้างอิงทุกประเภทโดยไม่มีความแตกต่างว่าเป็นคุกกี้เซสชันหรือฐานข้อมูล สำหรับฉันแล้วมีความรู้สึกเพียงเล็กน้อยในการใช้ Reader สำหรับวัตถุที่คงที่จริงเช่นเซิร์ฟเวอร์อีเมลหรือที่เก็บจากตัวอย่างนี้ สำหรับการอ้างอิงดังกล่าวฉันพบว่าตัวสร้างธรรมดาและ / หรือฟังก์ชันประยุกต์บางส่วนจะดีกว่า โดยพื้นฐานแล้ว Reader ช่วยให้คุณมีความยืดหยุ่นเพื่อให้คุณสามารถระบุการอ้างอิงของคุณได้ทุกครั้งที่โทร แต่ถ้าคุณไม่ต้องการจริงๆคุณก็จ่ายภาษีเท่านั้น
- ความหนักเบาโดยนัย - การใช้ Reader โดยไม่มีนัยจะทำให้ตัวอย่างอ่านยาก ในทางกลับกันเมื่อคุณซ่อนส่วนที่มีเสียงดังโดยใช้นัยและเกิดข้อผิดพลาดบางครั้งคอมไพเลอร์จะทำให้คุณถอดรหัสข้อความได้ยาก
- พิธีด้วย
pure
, local
และการสร้างการเรียนการกำหนดค่าของตัวเอง / ใช้ tuples ว่า Reader บังคับให้คุณเพิ่มรหัสบางอย่างที่ไม่เกี่ยวกับโดเมนที่เป็นปัญหาดังนั้นจึงมีสัญญาณรบกวนในโค้ด ในทางกลับกันแอปพลิเคชันที่ใช้ตัวสร้างมักใช้รูปแบบโรงงานซึ่งมาจากภายนอกโดเมนปัญหาด้วยดังนั้นจุดอ่อนนี้จึงไม่ร้ายแรง
จะเกิดอะไรขึ้นถ้าฉันไม่ต้องการแปลงคลาสของฉันเป็นอ็อบเจกต์ที่มีฟังก์ชัน?
คุณต้องการ. ในทางเทคนิคคุณสามารถหลีกเลี่ยงสิ่งนั้นได้ แต่ดูว่าจะเกิดอะไรขึ้นถ้าฉันไม่แปลงFindUsers
คลาสเป็นอ็อบเจ็กต์ บรรทัดเพื่อความเข้าใจตามลำดับจะมีลักษณะดังนี้:
getAddresses <- ((ds: Datastore) => new FindUsers(ds).inactive _).local[Config](_.dataStore)
ที่อ่านไม่ออกใช่ไหม ประเด็นคือ Reader ทำงานกับฟังก์ชั่นดังนั้นหากคุณยังไม่มีคุณต้องสร้างอินไลน์ซึ่งมักจะไม่ค่อยสวยเท่าไหร่