Dmitry Klochkov
Dmitry Klochkov

Reputation: 2635

Clear way to update some nested struct from a bigger struct in place

Say we have some complex struct with multiple nested levels(for simplicity, in the example will be only one level, but there could be more).

Example. We have a data structure:

struct Company {
    var employee: [Int: Employee]
}

struct Employee {
    var name: String
    var age: Int
}

var company = Company(employee: [
    1: Employee(name: "Makr", age: 25),
    2: Employee(name: "Lysa", age: 30),
    3: Employee(name: "John", age: 28)
    ])

Now we want to create a function which updates some Employee of the company in place. We could write it using an inout param:

func setAge(_ age: Int, forEmployee employee: inout Employee) {
    employee.age = age  
}

setAge(26, forEmployee: &company.employees[1]!)

This works, but as you can see we need to unwrap expression 'company.employees[1]' before passing it by ref. This forced unwrap can produce runtime error if there is no such employee for the provided key.

So we need to check if the employee exists:

if company.employees[1] != nil {
    setAge(26, forEmployee: &company.employees[1]!)
}

This also works, but this code is kind of ugly because we need to repeat the expression 'company.employees[1]' two times.

So the question is: Is there some way to get rid of this repetition?

I tried to use optional inout param in the modifying function but could not get it working.

Upvotes: 1

Views: 300

Answers (3)

DeFrenZ
DeFrenZ

Reputation: 2252

Based on your comments, like

What I wanted in the first place is just to have a reference to a substructure of a bigger structure so the part of code that is dealing with the substructure could know nothing about where is this substructure located in the bigger structure.

and

It would be ideal if I just could create a local inout var. Like if var employ: inout Employee? = company.employee[1] { // make whatever I want with that employee }.

I think that what you want is a generic update function. In the community this is part of the family of utility functions referred as with (https://forums.swift.org/t/circling-back-to-with/2766)

The version that you need in this case is one that basically guards on nil, so I suggest something like

func performUpdateIfSome <T> (_ value: inout T?, update: (inout T) throws -> Void) rethrows {
  guard var _value = value else { return }
  try update(&_value)
  value = _value
}

with this utility then what you wanted to do would be done with

performUpdateIfSome(&company.employees[1], update: { $0.age = 26 })

Note

If you want to abstract away how to access the employee but not the company, then keypaths are an option as well :)

Upvotes: 2

Robert Dresler
Robert Dresler

Reputation: 11140

I would simply add extension to Employee which set employee's age

extension Employee {
    mutating func setAge(_ age: Int) {
        self.age = age
    }
}

Then you can use optional chaining for calling. So if value for key 1 doesn't exist, nothing happens and code goes on

company.employee[1]?.setAge(26)

Edit:

If your goal is just to change some property and then return object, simply create method which takes optional parameter and returns optional value

func setAge(_ age: Int, forEmployee employee: inout Employee?) -> Employee? {
    employee?.age = age
    return employee
}

if let employee = setAge(26, forEmployee: &company.employees[1]) { ... }

Upvotes: 1

Joakim Danielson
Joakim Danielson

Reputation: 51882

You need to hide the implementation and let the struct handle the logic with specific error handling strategy, like throwing an error or simply return true/false depending on success or simply ignore any problems. I don't know what the Int key stands for but here I guess it's an ID of some sort, so add this to Company struct

mutating func setAge(_ age: Int, forId id: Int) -> Bool {
    if employee.keys.contains(id) {
        employee[id]?.age = age
        return true
    }
    return false
}

Upvotes: 1

Related Questions