代码详解:数据结构越来越复杂?试试Monocle的Optics库吧

全文共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,加入读者圈,一起讨论最新鲜的人工智能科技哦~)

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值