gpt4 book ai didi

scala - 我什么时候必须使用 State Monad 或 .copy?

转载 作者:行者123 更新时间:2023-12-04 22:26:43 26 4
gpt4 key购买 nike

我在一本 haskell 的书中看到了一个例子,其中创建了一个简单的堆栈(push、pop)并在它们使用 .put(..) 或 .pop() 更新堆栈时返回 monad 状态。

提醒一下,该示例与此类似:

pop :: State Stack Int  
pop = State $ \(x:xs) -> (x,xs)

push :: Int -> State Stack ()
push a = State $ \xs -> ((),a:xs)

这是有道理的,但在看到这个例子后,我开始怀疑我的域实体(假设客户)。现在,我只是使用 .copy() 返回一个新的更新客户,每次我想更新它的某些内容(例如年龄)时,就像许多文档中指出的那样,而不是使用任何 State monad。我使用 .copy() 的方式没有什么特别之处:
case class Customer(age: Int, name: String) {
def updateAge(newAge: Int): Customer = {
this.copy(age = newAge)
}
}

然而,我开始认为,与其只返回一个更新的副本,我还必须返回一个 State Monad,但到目前为止我还没有看到任何这样的例子(我正在自己学习),因此我感到很困惑。 我的印象是有时使用 .copy() 并且在其他不同的情况下使用 State Monad,但是两者都管理 State.....

每次更新客户的年龄时,我是否应该返回一个 State Monad?

最佳答案

你永远不必返回 State 值——它只是使某些类型的组合成为可能,这使得构建程序的某些方法更清晰。

对于这样的主题,我认为从一个具体的、动机明确的例子开始是最好的,Circe JSON library 包含一些基于 State 的方便解码方法,我认为这有助于展示 State 如何有用。例如,假设我们有一个 Scala 案例类:

case class User(id: Long, name: String, posts: List[Long])

我们希望能够像这样解码 JSON 文档:
val goodDocument = """{"id": 12345, "name": "Foo McBar", "posts": []}"""

但不是这样的:
val badDocument =
"""{"id": 12345, "name": "Foo McBar", "posts": [], "bad-stuff": null}"""

如果我们手动为 User 编写一个 Circe 解码器,它可能看起来像这样:
implicit val decodeUser: Decoder[User] = Decoder.instance { c =>
for {
id <- c.downField("id").as[Long]
name <- c.downField("name").as[String]
posts <- c.downField("posts").as[List[Long]]
} yield User(id, name, posts)
}

不幸的是,这不符合我们的要求——它接受两个文档,因为它没有跟踪哪些字段已经被解码,哪些字段还剩下。
scala> decode[User](goodDocument)
res0: Either[io.circe.Error,User] = Right(User(12345,Foo McBar,List()))

scala> decode[User](badDocument)
res1: Either[io.circe.Error,User] = Right(User(12345,Foo McBar,List()))

我们可以通过跟踪自己并在看到字段后“删除”字段来解决此问题:
import io.circe.{DecodingFailure, Json}

implicit val decodeUser: Decoder[User] = Decoder.instance { c =>
val idC = c.downField("id")

def isEmptyObject(json: Json): Boolean = json.asObject.exists(_.isEmpty)

for {
id <- idC.as[Long]
afterIdC = idC.delete
nameC = afterIdC.downField("name")
name <- nameC.as[String]
afterNameC = nameC.delete
postsC = afterNameC.downField("posts")
posts <- postsC.as[List[Long]]
afterPostsC = postsC.delete
_ <- if (afterPostsC.focus.exists(isEmptyObject)) Right(()) else {
Left(DecodingFailure("Bad fields", c.history))
}
} yield User(id, name, posts)
}

哪个效果很好!
scala> decode[User](goodDocument)
res2: Either[io.circe.Error,User] = Right(User(12345,Foo McBar,List()))

scala> decode[User](badDocument)
res3: Either[io.circe.Error,User] = Left(DecodingFailure(Bad fields, List()))

但是代码是一团糟,因为我们必须交错我们的逻辑以通过我们漂亮的干净的基于 Decoder.Resultfor 理解来跟踪使用的内容。

Circe 支持跟踪已使用字段的解码器的方式基于 Cats 的 StateT 。使用这些辅助方法,您可以编写以下内容:
import cats.instances.either._

implicit val decodeUser: Decoder[User] = Decoder.fromState(
for {
id <- Decoder.state.decodeField[Long]("id")
name <- Decoder.state.decodeField[String]("name")
posts <- Decoder.state.decodeField[List[Long]]("posts")
_ <- Decoder.state.requireEmpty
} yield User(id, name, posts)
)

这看起来很像我们最初的(非使用场跟踪)实现,但它的工作方式与我们的第二个实现完全相同。

我们可以看一下单个操作的类型:
scala> Decoder.state.decodeField[String]("name")
res4: StateT[Decoder.Result,ACursor,String] = ...

这是以下内容的一种包装器:
ACursor => Decoder.Result[(ACursor, Long)]

这基本上结合了上面详细实现中的 afterIdC.downField("name")nameC.deletenameC.as[String] 行,但将它们封装在我们可以在 for 理解中使用的单个操作中。

在 case 类上调用 copy 以获取更新的值当然非常好,而且大多数时候这正是您想要的。您不应该使用 State 使您的代码复杂化,除非您确定它正在增加值(value),并且当您发现自己通过某种其他类型的操作对 copy 的调用和赋值进行线程化时,它最有可能增加值(value),就像我们在现场使用跟踪中看到的那样和上面的解码。

关于scala - 我什么时候必须使用 State Monad 或 .copy?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/59023827/

26 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com