sdgfsdh
sdgfsdh

Reputation: 37025

How to construct a lens of 2 properties?

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

Answers (1)

Brian Berns
Brian Berns

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

Related Questions