全文共7490字,预计学习时长15分钟或更长
图片来源:unsplash.com/@lysanderyuen
在函数式编程中,必须保证其不变性。因此,每当需要修改数据结构的内容时,都会随之创建一个具有更新值的新实例。随着数据结构越来越复杂,创建副本就越来越繁琐。
为了简化这一过程,技术人员设计了一组通常名为Optics的函数,以便简单地访问或修改整个数据结构的各个部分。这些函数必须遵守某些定律,以保证其行为的可预测性和直观性(例如,如果对修改后的值进行回读,就可以直接获得修改值)。
本文提供了一些例子,用以示范如何使用名为Monocle的Scala语言中的Optics库。
Monocle示例
为了演示Monocle的使用方法,首先创建一个简单的域模型:
import monocle.macros.Lensessealed trait RoomTariffcase class NonRefundable(fee: BigDecimal) extends RoomTariffcase class Flexible(fee: BigDecimal) extends RoomTariff@Lenses("_") case class Hotel(name: String, address: String, rating: Int, rooms: List[Room], facilities: Map[String, List[String]]) @Lenses("_") case class Room(name: String, boardType: Option [String], price: Price, roomTariff: RoomTariff)@Lenses("_") case class Price(amount: BigDecimal, currency: String)
@Lenses 这一注释能够为每种属性自动生成一个透镜。创建一个虚拟酒店为例:
图片来源:unsplash.com/@runnyrem
val rooms = List( Room("Double", Some("Half Board"), Price(10, "USD"), NonRefundable(1)), Room("Twin", None, Price(20, "USD"), Flexible(0)) , Room("Executive", None, Price(200, "USD"), Flexible(0)) ) val facilities = Map("business" -> List("conference room")) val hotel = Hotel("Hotel Paradise", "100 High Street", 5, rooms, facilities)
现在,到了有趣的部分了:
根据List中的房间位置更改房间
test("double price of even rooms") {val updatedHotel = (_rooms composeTraversal filterIndex{i: Int => i/2*2 == i} composeLens _price composeLens _amount modify(_ * 2)) (hotel) assert(updatedHotel.rooms(0).price.amount == hotel.rooms(0).price.amount * 2) assert(updatedHotel.rooms(1).price.amount == hotel.rooms(1).price.amount) assert(updatedHotel.rooms(2).price.amount == hotel.rooms(2).price.amount * 2) } test("set price of 2nd room") { val newValue = 12val roomToUpdate = 1assert(hotel.rooms(roomToUpdate).price.amount != newValue) val updatedHotel = (_rooms composeOptional index(roomToUpdate) composeLens _price composeLens _amount set newValue)(hotel)val updatedRoomList = (index[List[Room], Int, Room](roomToUpdate) composeLens _price composeLens _amount set newValue)(hotel.rooms) assert(updatedHotel.rooms(roomToUpdate).price.amount == newValue) assert(updatedRoomList(roomToUpdate).price.amount == newValue) }
修改不存在的房间
test("no changes are made when attempting to modify a non-existing room") { val newValue = 12val roomToUpdate = 3assert(hotel.rooms.length == 3)val updatedHotel = (_rooms composeOptional index(roomToUpdate) composeLens _price composeLens _amount set newValue)(hotel) assert(hotel == updatedHotel) } test("hotel 'disappears' when attempting to modify a non-existing room") { val newValue = 12val roomToUpdate = 3assert(hotel.rooms.length == 3) val updatedHotel = (_rooms composeOptional index(roomToUpdate) composeLens _price composeLens _amount setOption newValue)(hotel) assert(updatedHotel.isEmpty) }
更改可选值
图片来源:unsplash.com/@olav_ahrens
test("set a value inside an Option") { val newValue = "New Board Type"val roomToUpdate = 0assert(!hotel.rooms (roomToUpdate).boardType.contains(newValue))val updatedHotel = (_rooms composeOptional index(roomToUpdate) composeLens _boardType composeOptional some.asOptional set newValue)(hotel) assert(updatedHotel.rooms(roomToUpdate).boardType.contains(newValue)) } test("no changes are made when attempting to modify an empty Option") { val newValue = "New Board Type"val roomToUpdate = 1assert(hotel.rooms(roomToUpdate).boardType.isEmpty)val updatedHotel = (_rooms composeOptional index(roomToUpdate) composeLens _boardType composeOptional some.asOptional set newValue)(hotel) assert(updatedHotel.rooms(roomToUpdate).boardType.isEmpty) } test("hotel 'disappears' when attempting to modify an empty Option") { val newValue = "New Board Type"val roomToUpdate = 1assert(hotel.rooms(roomToUpdate).boardType.isEmpty)val updatedHotel = (_rooms composeOptional index(roomToUpdate) composeLens _boardType composeOptional some.asOptional setOption newValue)(hotel) assert(updatedHotel.isEmpty) }
使用应用函数进行更改
test("divide prices by 10"){ assert(hotel.rooms(0).price.amount == 10)assert(hotel.rooms(1).price.amount == 20)val updatedHotel = (_rooms composeTraversal each composeLens _price composeLens _amount modify(_ / 10))(hotel) assert(updatedHotel.rooms(0).price.amount == 1) assert(updatedHotel.rooms(1).price.amount == 2) } test("divide prices by 0"){ assert(hotel.rooms(0).price.amount == 10)assert(hotel.rooms(1).price.amount == 20)val updatedHotel = (_rooms composeTraversal each composeLens _price composeLens _amount). modifyF[Option](y => Try{y / 0}.toOption)(hotel) assert(updatedHotel.isEmpty) }
修改房间号
test("append a room"){assert(hotel.rooms.length == 3)val newRoom = Room("Triple", None, Price(1, "USD"), Flexible(0))val updatedHotel = (_rooms set _snoc(hotel.rooms, newRoom))(hotel) assert(updatedHotel.rooms.length == 4) assert(updatedHotel.rooms(3) == newRoom) } test("prepend a room"){assert(hotel.rooms.length == 3)val newRoom = Room("Triple", None, Price(1, "USD"), Flexible(0))val updatedHotel = (_rooms set _cons(newRoom, hotel.rooms))(hotel) assert(updatedHotel.rooms.length == 4) assert(updatedHotel.rooms(0) == newRoom) }
使用棱镜修改房价表
test("set prices of Flexible rooms") {val prism = Prism.partial[RoomTariff, BigDecimal] {case Flexible(x) => x}(Flexible)val newValue = 100 assert(hotel.rooms(0).roomTariff == NonRefundable(1)) ssert(hotel.rooms(1).roomTariff == Flexible(0)) assert(hotel.rooms(2).roomTariff == Flexible(0)) val updatedHotel = (_rooms composeTraversal each composeLens _roomTariff composePrism prism set newValue)(hotel) assert(hotel.rooms(0).roomTariff == updatedHotel.rooms(0).roomTariff) assert(updatedHotel.rooms(1).roomTariff == Flexible(newValue)) assert(updatedHotel.rooms(2).roomTariff == Flexible(newValue)) }
操控地图
test("modifying business facilities") {val updatedHotel = (_facilities composeLens at("business") set Some(List("")))(hotel) assert(updatedHotel.facilities("business") == List("")) } test("removing business facilities") { val updatedHotel = (_facilities composeLens at("business") set None)(hotel)val updatedFacilities = remove("business")(hotel.facilities) assert(updatedHotel.facilities.get("business").isEmpty) assert(updatedFacilities.get("business").isEmpty) } test("adding entertainment facilities") {val updatedHotel = (_facilities composeLens at("entertainment") set Some(List("satellite tv", "internet")))(hotel) assert(updatedHotel.facilities("entertainment") == List("satellite tv", "internet")) }
折叠房间清单
test("folding over room prices to add them up") { assert(hotel.rooms(0).price.amount == 10) assert(hotel.rooms(1).price.amount == 20)assert(hotel.rooms(2).price.amount == 200) assert((_rooms composeFold Fold.fromFoldable[List, Room] foldMap (_.price.amount))(hotel) == 230)
修改符合特定标准的房间
val unsafePrism = UnsafeSelect.unsafeSelect[Room](_.name == "Double") test("double price of Double rooms using unsafe operation") {val updatedHotel = (_rooms composeTraversal each composePrism unsafePrism composeLens _price composeLens _amount modify (_ * 2)) (hotel) assert(hotel.rooms.filter(_.name == "Double").map (_.price.amount*2) == updatedHotel.rooms.filter(_.name == "Double").map(_.price.amount)) }
最后一个示例使用了不安全的棱镜(不安全是因为它不符合任何棱镜定律)。下面通过检验定律来进行证实:
val roomGen: Gen[Room] = for { name <- Gen.oneOf("Double", "Twin", "Executive") board <- Gen.option(Gen.alphaStr) price <- for{ price <- Gen.posNum[Double] currency <- Gen.oneOf("USD", "GBP", "EUR") } yield Price(price, currency) tariff <- Gen.oneOf(Gen.posNum[Double].map(NonRefundable(_)), Gen.posNum[Double].map(Flexible(_))) } yield Room(name, board, price, tariff) implicit val roomArb: Arbitrary[Room] = Arbitrary(roomGen) implicit val arbAA: Arbitrary[Room => Room] = Arbitrary{for{ room <- roomGen } yield (_: Room) => room } checkAll("unsafe prism", PrismTests(unsafePrism))
进行上述测试时,下列两个测试未能成功运行:
· Prism.compose modify
· Prism.round trip another way
所以,为了找出失败原因,接下来对“round trip other way”定律进行测试。这是它在PrismLaws中的定义:
def roundTripOtherWay(a: A): IsEq[Option[A]] = prism.getOption(prism.reverseGet(a)) <==> Some(a)
下图展示了该定律是如何被打破的:
val a = Room(Twin,None,Price(1.0,USD),Flexible(1.0))val b = unsafePrism.reverseGet(a) = Room(Twin,None,Price(1.0,USD),Flexible(1.0))val c = unsafePrism.getOption(b) = NoneNone != Some(a)
因此,当使用unsafePrism来更改谓词中包含的属性以创建棱镜时,必定是是不安全的。
以上所有示例传送门:https://github.com/falvarezb/blog-bytecode/blob/postLenses/src/test/scala/fjab/LensesTest.scala
留言 点赞 关注
我们一起分享AI学习与发展的干货
欢迎关注全平台AI垂类自媒体 “读芯术”
(添加小编微信:dxsxbb,加入读者圈,一起讨论最新鲜的人工智能科技哦~)