Reputation: 37025
Suppose I have a record like this:
type Order = | Order
type OrderBook =
{
PrimaryOrderID : Guid
Orders : Map<Guid, Order>
}
I would like to do nested updates using lenses.
Here are the optics type aliases:
/// Lens from 'a -> 'b.
type Lens<'a,'b> =
('a -> 'b) * ('b -> 'a -> 'a)
/// Prism from 'a -> 'b.
type Prism<'a,'b> =
('a -> 'b option) * ('b -> 'a -> 'a)
I would like to construct a lens for the primary order. This may be None
if the primary order does not exist in the Orders
map.
Here is what I came up with:
module Optics =
let primaryOrder_ : Prism<OrderBook, Order> =
let get =
fun orderBook ->
orderBook.Orders
|> Map.tryFind orderBook.PrimaryOrderID
let set =
fun primaryOrder orderBook ->
{
orderBook with
Orders =
orderBook.Orders
|> Map.add orderBook.PrimaryOrderID primaryOrder
}
get, set
However, I was wondering if there is a more elegant way to define this in terms of primaryOrderID_
and orders_
lenses?
module Optics =
let primaryOrderID_ : Lens<OrderBook, Guid> =
(fun x -> x.PrimaryOrderID), (fun v x -> { x with PrimaryOrderID = v })
let orders_ : Lens<OrderBook, Map<Guid, Order>> =
(fun x -> x.Orders), (fun v x -> { x with Orders = v })
Upvotes: 0
Views: 77
Reputation: 17028
I'm not an expert on optics, but I think the simple answer has to be no, because neither of the field-level lenses will call Map.tryFind
or Map.add
, like your high-level primaryOrder_
lens does.
This seems to be due to an implied type-level invariant: the Orders
map must always contain an entry for the primary order. Since OrderBook
as currently written doesn't enforce this, things could break if the invariant isn't honored in client code.
I think if you address this underlying issue first, the optics will then be easier to figure out. One simple approach would be to define OrderBook
like this instead:
type OrderBook =
{
PrimaryOrder : Order // the entire order, not just its ID
OtherOrders : Map<Guid, Order>
}
The primaryOrder_
lens would then be trivial, but the actual best answer will probably depend on your use case. (Note that this is similar to NonEmptyMap
in FSharpPlus, with the assumption that Order
has its own ID : Guid
field.)
Upvotes: 0