After examining our immutable example implementation, we are not able to say that it covers all the functionalities of the imperative approach. For instance, it does not provide us with a way to change the producer
of a product
. After all, we cannot change it.
Whenever we need to change any property of the product
, we need to go through the following process:
let mexicanBananas = FunctionalProduct(name: bananas.name, price: bananas.price, quantity: bananas.quantity, producer: Producer(name: "XYZ", address: "New Mexico, Mexico"))
This solution is verbose and does not look nice. Let's examine how we can improve this process.
The first solution is to provide a new init
method that copies the current instance. This approach is called copy constructor. Let's add our new init
method and leverage it:
init(products: [FunctionalProduct], lastModified: NSDate) { self.products = products self.lastModified = lastModified } init(productTracker: FunctionalProductTracker, products: [FunctionalProduct]? = nil, lastModified: NSDate? = nil) { self.products = products ?? productTracker.products self.lastModified = lastModified ?? productTracker.lastModified }
We added the default init
as well because by adding a new init
method to our struct
, we lost the benefit of automatic init
generation. We also need to change our addNewProduct
to accommodate these changes:
func addNewProduct(item: FunctionalProduct) -> FunctionalProductTracker { return FunctionalProductTracker(productTracker: self, products: self.products + [item]) }
Whenever we need to modify our object partially, we will be able to do so easily using this technique.
In the previous section, we covered copy constructors. Here, we will examine a functional structure called lens. Simply put, lenses are functional getters and setters that are implemented for a whole object and its parts:
Let's implement a Lens
:
struct Lens<Whole, Part> { let get: Whole -> Part let set: (Part, Whole) -> Whole }
Let's use it to change our FunctionalProduct
object to get and set the producer
property:
let prodProducerLens: Lens<FunctionalProduct, Producer> = Lens(get: { $0.producer}, set: { FunctionalProduct(name: $1.name, price: $1.price, quantity: $1.quantity, producer: $0)})
Let's change the producer for mexicanBananas
:
let mexicanBananas2 = prodProducerLens.set(Producer(name: "QAZ", address: "Yucatan, Mexico"), mexicanBananas)
Through our lens, we can change it as shown in the preceding code.
Let's examine another example. Suppose that we have a Producer
object as follows:
let chineeseProducer = Producer(name: "KGJ", address: "Beijing, China")
We want to change the address
:
let producerAddressLens: Lens<Producer, String> = Lens(get: { $0.address }, set: { Producer(name: $1.name, address: $0)}) let chineeseProducer2 = producerAddressLens.set("Shanghai, China", chineeseProducer)
Suppose that we had mexicanBananas2
and needed to have a Chinese banana producer, then we could use:
let chineseBananaProducer = prodProducerLens.set( producerAddressLens.set("Shanghai, China", chineseProducer), mexicanBananas2)
This syntax does not look very simple, and it seems that we did not gain much after all. In the next section, we will simplify it.
Lens composition will help to simplify our lens; let's examine how:
infix operator >>> { associativity right precedence 100 } func >>><A,B,C>(l: Lens<A,B>, r: Lens<B,C>) -> Lens<A,C> { return Lens(get: { r.get(l.get($0)) }, set: { (c, a) in l.set(r.set(c,l.get(a)), a) }) }
Let's test this:
let prodProducerAddress = prodProducerLens >>> producerAddressLens let mexicanBananaProducerAddress = prodProducerAddress.get(mexicanBananas2) let newProducer = prodProducerAddress.set("Acupulco, Mexico", mexicanBananas2) print(newProducer)
The result is going to be:
FunctionalProduct(name: "Banana", price: 0.79, quantity: 2, producer: Producer(name: "QAZ", address: "Acupulco, Mexico"))
Using lenses and composition, we were able to get and set a product's producer address.