Reader Monad สำหรับ Dependency Injection: การอ้างอิงหลายครั้งการเรียกซ้อนกัน


87

เมื่อถูกถามเกี่ยวกับ Dependency Injection ใน Scala คำตอบจำนวนมากชี้ไปที่การใช้ Reader Monad ไม่ว่าจะเป็นคำตอบจาก Scalaz หรือเพียงแค่หมุนของคุณเอง มีจำนวนของบทความที่ชัดเจนมากอธิบายพื้นฐานของวิธีการ (เช่นมีการพูดคุย Runar ของ , บล็อกของเจสัน ) แต่ผมไม่ได้จัดการเพื่อหาตัวอย่างที่สมบูรณ์มากขึ้นและผมไม่เห็นข้อดีของวิธีการที่มากกว่าเช่นขึ้น DI "manual" แบบดั้งเดิม (ดูคำแนะนำที่ฉันเขียน ) ส่วนใหญ่ฉันอาจพลาดประเด็นสำคัญบางอย่างดังนั้นคำถามนี้

ตัวอย่างเช่นสมมติว่าเรามีคลาสเหล่านี้:

trait Datastore { def runQuery(query: String): List[String] }
trait EmailServer { def sendEmail(to: String, content: String): Unit }

class FindUsers(datastore: Datastore) {
  def inactive(): Unit = ()
}

class UserReminder(findUser: FindUsers, emailServer: EmailServer) {
  def emailInactive(): Unit = ()
}

class CustomerRelations(userReminder: UserReminder) {
  def retainUsers(): Unit = {}
}

ฉันกำลังสร้างโมเดลสิ่งต่าง ๆ โดยใช้คลาสและพารามิเตอร์คอนสตรัคเตอร์ซึ่งเข้ากันได้ดีกับแนวทาง DI "ดั้งเดิม" อย่างไรก็ตามการออกแบบนี้มีข้อดีอยู่สองประการ:

  • แต่ละฟังก์ชันมีการระบุการอ้างอิงอย่างชัดเจน เราถือว่าการอ้างอิงเป็นสิ่งที่จำเป็นจริงๆเพื่อให้ฟังก์ชันการทำงานทำงานได้อย่างถูกต้อง
  • การอ้างอิงถูกซ่อนไว้ในฟังก์ชันการทำงานเช่นUserReminderไม่มีความคิดที่FindUsersต้องการพื้นที่เก็บข้อมูล ฟังก์ชันสามารถอยู่ในหน่วยคอมไพล์แยกกันได้
  • เราใช้เฉพาะ Scala ที่บริสุทธิ์เท่านั้น การใช้งานสามารถใช้ประโยชน์จากคลาสที่ไม่เปลี่ยนรูปฟังก์ชันลำดับที่สูงขึ้นวิธีการ "ตรรกะทางธุรกิจ" สามารถส่งคืนค่าที่รวมอยู่ในIOmonad หากเราต้องการจับเอฟเฟกต์เป็นต้น

สิ่งนี้จะถูกจำลองด้วย Reader monad ได้อย่างไร? จะเป็นการดีที่จะรักษาลักษณะข้างต้นไว้เพื่อให้ชัดเจนว่าแต่ละฟังก์ชันต้องการการอ้างอิงประเภทใดและซ่อนการอ้างอิงของฟังก์ชันหนึ่งจากฟังก์ชันอื่น โปรดทราบว่าการใช้classes เป็นรายละเอียดการใช้งานมากกว่า บางทีวิธีแก้ปัญหาที่ "ถูกต้อง" โดยใช้ Reader monad อาจใช้อย่างอื่น

ฉันพบคำถามที่ค่อนข้างเกี่ยวข้องซึ่งแนะนำอย่างใดอย่างหนึ่ง:

  • ใช้วัตถุสภาพแวดล้อมเดียวที่มีการอ้างอิงทั้งหมด
  • โดยใช้สภาพแวดล้อมในท้องถิ่น
  • รูปแบบ "พาร์เฟต์"
  • แผนที่ที่จัดทำดัชนีประเภท

อย่างไรก็ตามนอกเหนือจากการเป็น (แต่เป็นเรื่องส่วนตัว) ที่ซับซ้อนเกินไปสำหรับสิ่งง่ายๆเช่นนี้ในโซลูชันเหล่านี้ทั้งหมดเช่นretainUsersวิธีการ (ซึ่งเรียกemailInactiveซึ่งเรียกinactiveหาผู้ใช้ที่ไม่ได้ใช้งาน) จะต้องรู้เกี่ยวกับการDatastoreพึ่งพาเพื่อ สามารถเรียกฟังก์ชันที่ซ้อนกันได้อย่างถูกต้องหรือฉันคิดผิด?

การใช้ Reader Monad สำหรับ "แอปพลิเคชันทางธุรกิจ" ในแง่มุมใดจะดีกว่าการใช้พารามิเตอร์ตัวสร้างเพียงอย่างเดียว


1
Reader monad ไม่ใช่กระสุนเงิน ฉันคิดว่าถ้าคุณต้องการการอ้างอิงหลายระดับการออกแบบของคุณก็ค่อนข้างดี
ZhekaKozlov

อย่างไรก็ตามมักถูกอธิบายว่าเป็นอีกทางเลือกหนึ่งของ Dependency Injection; บางทีมันควรจะอธิบายว่าเป็นส่วนเติมเต็ม? บางครั้งฉันรู้สึกว่า DI ถูกไล่โดย "โปรแกรมเมอร์ที่ใช้งานได้จริง" ดังนั้นฉันจึงสงสัยว่า "อะไรแทน" :) ไม่ว่าจะด้วยวิธีใดฉันคิดว่าการมีการอ้างอิงหลายระดับหรือมากกว่าบริการภายนอกหลายอย่างที่คุณต้องพูดคุยคือ "แอปพลิเคชันธุรกิจ" ขนาดกลาง - ใหญ่ทุกตัวดูเหมือน (ไม่ใช่กรณีสำหรับห้องสมุดอย่างแน่นอน)
adamw

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

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

คำตอบ:


37

วิธีการจำลองตัวอย่างนี้

สิ่งนี้จะถูกจำลองด้วย Reader monad ได้อย่างไร?

ฉันไม่แน่ใจว่าควรจะจำลองด้วย Reader หรือไม่ แต่สามารถทำได้โดย:

  1. การเข้ารหัสคลาสเป็นฟังก์ชันที่ทำให้โค้ดเล่นได้ดีกว่ากับ Reader
  2. การเขียนฟังก์ชั่นด้วย 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 = ???
}
// usage: val result = new Foo(dependency).bar(arg)

กลายเป็น

object Foo {
  def bar: Dep => Arg => Res = ???
}
// usage: val result = Foo.bar(dependency)(arg)

เก็บไว้ในใจว่าแต่ละ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() - ฟังก์ชันที่ส่งผ่านไปยังพารามิเตอร์แรก

โปรดทราบว่าโค้ดแสดงคุณสมบัติที่ต้องการสามประการจากคำถาม:

  1. เป็นที่ชัดเจนว่าแต่ละฟังก์ชันต้องการการอ้างอิงประเภทใด
  2. ซ่อนการอ้างอิงของฟังก์ชันหนึ่งจากฟังก์ชันอื่น
  3. 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 อย่างเต็มที่เพราะฉันไม่ได้ใช้มัน

  1. ความสม่ำเสมอ - ไม่ว่าเนื้อหาจะสั้น / ยาวเพียงใดเพื่อความเข้าใจมันเป็นเพียง Reader และคุณสามารถเขียนด้วยอินสแตนซ์อื่นได้อย่างง่ายดายบางทีอาจจะแนะนำประเภท Config อีกหนึ่งประเภทและเพิ่มlocalสายเรียกเข้าไว้ด้านบน ประเด็นนี้คือ IMO ค่อนข้างเป็นเรื่องของรสนิยมเพราะเมื่อคุณใช้คอนสตรัคเตอร์ไม่มีใครขัดขวางไม่ให้คุณแต่งสิ่งที่คุณชอบเว้นแต่จะมีใครทำอะไรโง่ ๆ เช่นทำงานในคอนสตรัคเตอร์ซึ่งถือว่าเป็นการปฏิบัติที่ไม่ดีใน OOP
  2. Reader เป็น monad เพื่อที่จะได้รับผลประโยชน์ทั้งหมดที่เกี่ยวข้องกับที่ - sequence, traverseวิธีการดำเนินการฟรี
  3. ในบางกรณีคุณอาจพบว่าควรสร้าง Reader เพียงครั้งเดียวและใช้สำหรับ Configs ที่หลากหลาย ด้วยตัวสร้างไม่มีใครขัดขวางคุณในการทำเช่นนั้นคุณเพียงแค่ต้องสร้างกราฟออบเจ็กต์ทั้งหมดใหม่สำหรับทุก Config ที่เข้ามา แม้ว่าฉันจะไม่มีปัญหากับสิ่งนั้น (ฉันชอบทำแบบนั้นกับทุกคำขอในการสมัคร) แต่ก็ไม่ใช่ความคิดที่ชัดเจนสำหรับหลาย ๆ คนด้วยเหตุผลที่ฉันอาจคาดเดาได้เท่านั้น
  4. Reader ผลักดันให้คุณใช้ฟังก์ชันต่างๆมากขึ้นซึ่งจะเล่นได้ดีขึ้นกับแอปพลิเคชันที่เขียนในรูปแบบ FP ส่วนใหญ่
  5. ผู้อ่านแยกความกังวล คุณสามารถสร้างโต้ตอบกับทุกสิ่งกำหนดตรรกะโดยไม่ต้องให้การอ้างอิง จริงจัดหาในภายหลังแยกต่างหาก (ขอบคุณ Ken Scrambler สำหรับประเด็นนี้) สิ่งนี้มักจะได้ยินถึงข้อดีของ Reader แต่ก็เป็นไปได้ด้วยตัวสร้างธรรมดา

ฉันอยากจะบอกสิ่งที่ฉันไม่ชอบใน Reader

  1. การตลาด. บางครั้งฉันรู้สึกประทับใจที่ Reader ทำการตลาดสำหรับการอ้างอิงทุกประเภทโดยไม่มีความแตกต่างว่าเป็นคุกกี้เซสชันหรือฐานข้อมูล สำหรับฉันแล้วมีความรู้สึกเพียงเล็กน้อยในการใช้ Reader สำหรับวัตถุที่คงที่จริงเช่นเซิร์ฟเวอร์อีเมลหรือที่เก็บจากตัวอย่างนี้ สำหรับการอ้างอิงดังกล่าวฉันพบว่าตัวสร้างธรรมดาและ / หรือฟังก์ชันประยุกต์บางส่วนจะดีกว่า โดยพื้นฐานแล้ว Reader ช่วยให้คุณมีความยืดหยุ่นเพื่อให้คุณสามารถระบุการอ้างอิงของคุณได้ทุกครั้งที่โทร แต่ถ้าคุณไม่ต้องการจริงๆคุณก็จ่ายภาษีเท่านั้น
  2. ความหนักเบาโดยนัย - การใช้ Reader โดยไม่มีนัยจะทำให้ตัวอย่างอ่านยาก ในทางกลับกันเมื่อคุณซ่อนส่วนที่มีเสียงดังโดยใช้นัยและเกิดข้อผิดพลาดบางครั้งคอมไพเลอร์จะทำให้คุณถอดรหัสข้อความได้ยาก
  3. พิธีด้วยpure, localและการสร้างการเรียนการกำหนดค่าของตัวเอง / ใช้ tuples ว่า Reader บังคับให้คุณเพิ่มรหัสบางอย่างที่ไม่เกี่ยวกับโดเมนที่เป็นปัญหาดังนั้นจึงมีสัญญาณรบกวนในโค้ด ในทางกลับกันแอปพลิเคชันที่ใช้ตัวสร้างมักใช้รูปแบบโรงงานซึ่งมาจากภายนอกโดเมนปัญหาด้วยดังนั้นจุดอ่อนนี้จึงไม่ร้ายแรง

จะเกิดอะไรขึ้นถ้าฉันไม่ต้องการแปลงคลาสของฉันเป็นอ็อบเจกต์ที่มีฟังก์ชัน?

คุณต้องการ. ในทางเทคนิคคุณสามารถหลีกเลี่ยงสิ่งนั้นได้ แต่ดูว่าจะเกิดอะไรขึ้นถ้าฉันไม่แปลงFindUsersคลาสเป็นอ็อบเจ็กต์ บรรทัดเพื่อความเข้าใจตามลำดับจะมีลักษณะดังนี้:

getAddresses <- ((ds: Datastore) => new FindUsers(ds).inactive _).local[Config](_.dataStore)

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


ขอบคุณสำหรับคำตอบโดยละเอียด :) จุดหนึ่งที่ไม่ชัดเจนสำหรับฉันคือทำไมDatastoreและEmailServerถูกปล่อยให้เป็นลักษณะและอื่น ๆ กลายเป็นobjects? มีความแตกต่างพื้นฐานในบริการ / การพึ่งพาเหล่านี้ / (แต่คุณเรียกว่า) ซึ่งทำให้บริการเหล่านี้แตกต่างกันหรือไม่?
adamw

อืม ... ฉันไม่สามารถแปลงเช่นEmailSenderเป็นวัตถุได้ใช่ไหม? จากนั้นฉันจะไม่สามารถแสดงการพึ่งพาได้โดยไม่ต้องมีประเภท ...
ดัมว

อ่าการพึ่งพาจะอยู่ในรูปแบบของฟังก์ชันที่มีประเภทที่เหมาะสมดังนั้นแทนที่จะใช้ชื่อประเภททุกอย่างจะต้องเข้าสู่ลายเซ็นของฟังก์ชัน (ชื่อที่เป็นเพียงบังเอิญ) อาจจะ แต่ฉันไม่มั่นใจ;)
adamw

แก้ไข. แทนที่จะขึ้นอยู่กับคุณจะขึ้นอยู่กับEmailSender (String, String) => Unitไม่ว่าจะเป็นที่น่าเชื่อหรือไม่เป็นปัญหาอื่น :) Function2เพื่อให้แน่ใจก็ทั่วไปมากขึ้นอย่างน้อยเนื่องจากทุกคนมีอยู่แล้วขึ้นอยู่กับ
Przemek Pokrywka

แน่นอนว่าคุณต้องการตั้งชื่อ (String, String) => Unitเพื่อให้สื่อถึงความหมายบางอย่างแม้ว่าจะไม่ใช้นามแฝงประเภท แต่มีบางอย่างที่ตรวจสอบในเวลาคอมไพล์;)
adamw

3

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

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


ชั้นกลางจะรู้เฉพาะเกี่ยวกับการอ้างอิงระดับกลางได้อย่างไรไม่ใช่ทั้งหมด คุณช่วยยกตัวอย่างโค้ดที่แสดงว่าสามารถนำตัวอย่างไปใช้งานโดยใช้เครื่องอ่าน monad ได้อย่างไร
adamw

ฉันอาจจะอธิบายได้ไม่ดีไปกว่าบล็อกของ Json (ที่คุณโพสต์) ในการเสนอราคาแบบฟอร์มที่นั่น "ไม่เหมือนในตัวอย่างนัยเราไม่มี UserRepository ที่ใดก็ได้ในลายเซ็นของ userEmail และ userInfo" ตรวจสอบตัวอย่างนั้นอย่างรอบคอบ
Daniel Langdon

1
ดีใช่ แต่นี้อนุมานว่าผู้อ่าน Monad คุณกำลังใช้อยู่ parametrised กับที่มีการอ้างอิงถึงConfig UserRepositoryความจริงมันไม่สามารถมองเห็นได้โดยตรงในลายเซ็น แต่ฉันจะบอกว่ามันแย่ไปกว่านั้นคุณไม่รู้จริงๆว่าการอ้างอิงรหัสของคุณกำลังใช้อยู่ในแวบแรก การไม่ขึ้นอยู่Configกับการอ้างอิงทั้งหมดหมายความว่าแต่ละวิธีขึ้นอยู่กับวิธีการทั้งหมดหรือไม่?
adamw

ขึ้นอยู่กับพวกเขา แต่ไม่จำเป็นต้องรู้ เช่นเดียวกับในตัวอย่างของคุณกับคลาส ฉันเห็นว่ามันเท่ากัน :-)
Daniel Langdon

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

1

คำตอบที่ยอมรับจะให้คำอธิบายที่ดีเยี่ยมเกี่ยวกับวิธีการทำงานของ Reader Monad

ฉันต้องการเพิ่มสูตรเพื่อสร้างฟังก์ชันสองฟังก์ชันที่มีการอ้างอิงที่แตกต่างกันโดยใช้ Cats Library Reader ตัวอย่างข้อมูลนี้มีอยู่ในScastie ด้วย

ให้กำหนดสองฟังก์ชันที่เราต้องการเขียน: ฟังก์ชันจะคล้ายกับที่กำหนดไว้ในคำตอบที่ยอมรับ

  1. กำหนดทรัพยากรที่ฟังก์ชันขึ้นอยู่
  case class DataStore()
  case class EmailServer()
  1. กำหนดฟังก์ชันแรกด้วยการDataStoreพึ่งพา รับDataStoreและส่งคืนรายชื่อผู้ใช้ที่ไม่ได้ใช้งาน
  def f1(db:DataStore):List[String] = List("john@test.com", "james@test.com", "maria@test.com")
  1. กำหนดฟังก์ชันอื่นโดยEmailServerเป็นหนึ่งในการพึ่งพา
  def f2_raw(emailServer: EmailServer, usersToEmail:List[String]):Unit =

    usersToEmail.foreach(user => println(s"emailing ${user} using server ${emailServer}"))

ตอนนี้สูตรในการเขียนทั้งสองฟังก์ชัน

  1. ขั้นแรกให้นำเข้า Reader จาก Cats Library
  import cats.data.Reader
  1. เปลี่ยนฟังก์ชันที่สองเพื่อให้มีการพึ่งพาเพียงครั้งเดียว
  val f2 = (server:EmailServer) => (usersToEmail:List[String]) => f2_raw(server, usersToEmail)

ตอนนี้f2ใช้เวลาEmailServerและส่งคืนฟังก์ชันอื่นที่ใช้ในการListส่งอีเมลของผู้ใช้

  1. สร้างCombinedConfigคลาสที่มีการอ้างอิงสำหรับทั้งสองฟังก์ชัน
  case class CombinedConfig(dataStore:DataStore, emailServer: EmailServer)
  1. สร้างผู้อ่านโดยใช้ 2 ฟังก์ชัน
  val r1 = Reader(f1)
  val r2 = Reader(f2)
  1. เปลี่ยนผู้อ่านเพื่อให้สามารถทำงานกับการกำหนดค่าแบบรวมได้
  val r1g = r1.local((c:CombinedConfig) => c.dataStore)
  val r2g = r2.local((c:CombinedConfig) => c.emailServer)
  1. เขียนผู้อ่าน
  val composition = for {
    u <- r1g
    e <- r2g
  } yield e(u)
  1. ส่งผ่านCombinedConfigและเรียกใช้องค์ประกอบ
  val myConfig = CombinedConfig(DataStore(), EmailServer())

  println("Invoking Composition")
  composition.run(myConfig)
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.