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 })
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.tryFindorMap.add, like your high-levelprimaryOrder_lens does.This seems to be due to an implied type-level invariant: the
Ordersmap must always contain an entry for the primary order. SinceOrderBookas 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
OrderBooklike this instead:The
primaryOrder_lens would then be trivial, but the actual best answer will probably depend on your use case. (Note that this is similar toNonEmptyMapin FSharpPlus, with the assumption thatOrderhas its ownID : Guidfield.)