2
votes

I'm confused about why I am getting this error (swift 4.2.1).

// next, select only entries in range
let filteredDataOpt: [TimeSeriesEntry?] = filteredApps
    .map { data in
        let isInDate = dates.contains { date in
            guard let d = date else {
                return false
            }
            return Calendar.current.isDate(d, equalTo: data.date, toGranularity: Calendar.Component.day)
        }
        return isInDate ? timeSeriesDataFromAppData(data) : nil
    }.append(contentsOf: locationsData.map { data in
        let isInDate = dates.contains { date in
            guard let d = date else {
                return false
            }
            return Calendar.current.isDate(d, equalTo: data.date, toGranularity: Calendar.Component.day)
        }
        return isInDate ? timeSeriesDataFromLocationData(data) : nil
    })

This produces

Cannot use mutating member on immutable value: function call returns immutable value

on the third line.

But this doesn't:

// next, select only entries in range
let filteredDataOpt: [AppData?] = filteredByApps
    .map { data in
        let isInDate = dates.contains { date in
            guard let d = date else {
                return false
            }
            return Calendar.current.isDate(d, equalTo: data.date, toGranularity: Calendar.Component.day)
        }
        return isInDate ? data : nil
}
let filteredData: [AppData] = filteredDataOpt.compactMap { $0 }

My confusion stems from the fact that I am manipulating a sequence with append rather than first assigning it to a constant and then appending to it. Why is my sequence read-only?

edit: apparently map is always (And at first glance at least, bizarrely) returning a constant. In full, my solution is just:

var filteredDataOpt: [TimeSeriesEntry?] = filteredApps
    .map { data in
        let isInDate = dates.contains { date in
            guard let d = date else {
                return false
            }
            return Calendar.current.isDate(d, equalTo: data.date, toGranularity: Calendar.Component.day)
        }
        return isInDate ? self.timeSeriesData(appData: data) : nil
}
filteredDataOpt.append(contentsOf: self.locationsData.map { data in
    let isInDate = dates.contains { date in
        guard let d = date else {
            return false
        }
        return Calendar.current.isDate(d, equalTo: data.date, toGranularity: Calendar.Component.day)
    }
    return isInDate ? self.timeSeriesData(locationData: data) : nil
})
let filteredData = filteredDataOpt.compactMap { $0 }

But, does anyone else find that unsatisfactory? I'm stuck with:

  • intermediate variables
  • variables where what I need is just a constant
4
apparently map is always (And at first glance at least, bizarrely) returning a constant — mutability is not property of the value itself, but of it's "binding" (variable or constant); map returns just a "unbound" value. - user28434'mstep
Smaller example: [1, 2].map({$0 * 2}).append(6) - vacawama
@user28434 This is actually very important. Even if you could use a mutating method on a temporary expression, the result would not be correct. - Sulthan

4 Answers

5
votes

Your problem can be reduced to the following:

let data = [1, 2, 3]
let data2 = [4, 5, 6]

let filteredData: [Int] = data
    .map { $0 }
    .append(contentsOf: data2.map { $0 })

The solution is to use concatenation instead of append:

let data = [1, 2, 3]
let data2 = [4, 5, 6]

let filteredData: [Int] = data
    .map { $0 }
    + data2.map { $0 }

For an explanation, this is similar to:

let a: Int = 0
let b = a += 1 // this is append
let c = (a + 1) += 1 // this is append with a temporary expression

(you would be adding into something that is immediately discarded and the value would not be stored into c).

which obviously should be done as

let a: Int = 0
let b = a + 1

Note that even if you could append on temporary return value, append does not have a return value and your result assigned to filteredDataOpt would be Void.

The reason why temporary expression are constant (immutable) is to prevent you from doing similar errors.

2
votes

Not an answer to your question but this will work

var filteredDataOpt: [TimeSeriesEntry?] = filteredApps
        .map { data in
            let isInDate = dates.contains { date in
                guard let d = date else {
                    return false
                }
                return Calendar.current.isDate(d, equalTo: data.date, toGranularity: Calendar.Component.day)
            }
            return isInDate ? timeSeriesDataFromAppData(data) : nil
        }
filteredDataOpt.append(contentsOf: locationsData.map { data in
            let isInDate = dates.contains { date in
                guard let d = date else {
                    return false
                }
                return Calendar.current.isDate(d, equalTo: data.date, toGranularity: Calendar.Component.day)
            }
            return isInDate ? timeSeriesDataFromLocationData(data) : nil})
2
votes

The problem is method append(contentsOf:) is mutating and the returned element of any function in swift is by default immutable.

That's why you cannot call the method append(contentsOf:) on the array returned by map method.

Better you can use non mutating method appending(contentsOf:) for your code.

So your code will be:

// next, select only entries in range
let filteredDataOpt: [TimeSeriesEntry?] = filteredApps
    .map { data in
        let isInDate = dates.contains { date in
            guard let d = date else {
                return false
            }
            return Calendar.current.isDate(d, equalTo: data.date, toGranularity: Calendar.Component.day)
        }
        return isInDate ? timeSeriesDataFromAppData(data) : nil
    }.appending(contentsOf: locationsData.map { data in
        let isInDate = dates.contains { date in
            guard let d = date else {
                return false
            }
            return Calendar.current.isDate(d, equalTo: data.date, toGranularity: Calendar.Component.day)
        }
        return isInDate ? timeSeriesDataFromLocationData(data) : nil
    })
1
votes

Functions return immutable values. That's just the way it is in Swift. If you want it to be mutable, you have to store it in a var first.

However, you can use + to concatenate an Array with any Sequence. So if filteredApps is an Array, this should work:

    let filteredDataOpt: [TimeSeriesEntry?] = filteredApps
        .map { data in
            let isInDate = dates.contains { date in
                guard let d = date else {
                    return false
                }
                return Calendar.current.isDate(d, equalTo: data.date, toGranularity: Calendar.Component.day)
            }
            return isInDate ? self.timeSeriesData(appData: data) : nil
    } + self.locationsData.map { data in
        let isInDate = dates.contains { date in
            guard let d = date else {
                return false
            }
            return Calendar.current.isDate(d, equalTo: data.date, toGranularity: Calendar.Component.day)
        }
        return isInDate ? self.timeSeriesData(locationData: data) : nil
    }
    let filteredData = filteredDataOpt.compactMap { $0 }

There are several other things we can do to clean up this code also. We can factor out the date test:

    func isValid(_ candidate: Date) -> Bool {
        return dates.contains { date in
            guard let d = date else {
                return false
            }
            return Calendar.current.isDate(d, equalTo: candidate, toGranularity: Calendar.Component.day)
        }
    }

    let filteredDataOpt: [TimeSeriesEntry?] = filteredApps
        .map { data in
            return isValid(data.date) ? self.timeSeriesData(appData: data) : nil
    } + self.locationsData.map { data in
        return isValid(data.date) ? self.timeSeriesData(locationData: data) : nil
    }
    let filteredData = filteredDataOpt.compactMap { $0 }

Depending on your data, it might be better to pre-compute the valid Date ranges:

    let calendar = Calendar.current
    let dayRanges: [Range<Date>] = dates.lazy.compactMap({ $0 }).map({ date in
        let start = calendar.startOfDay(for: date)
        let end = calendar.date(byAdding: .day, value: 1, to: start)!
        return start ..< end
    })

    func isValid(_ candidate: Date) -> Bool {
        return dayRanges.contains(where: { $0.contains(candidate) })
    }

We could also split the filtering from the transforming. That lets us eliminate the use of compactMap:

    let filteredData = Array(filteredApps.lazy.filter({ isValid($0.date) }).map(self.timeSeriesData))
        + locationsData.lazy.filter({ isValid($0.date) }).map(self.timeSeriesData)

Or we could use compactMap twice:

    let filteredData = filteredApps.compactMap({ isValid($0.date) ? self.timeSeriesData(appData: $0) : nil })
        + locationsData.compactMap({ isValid($0.date) ? self.timeSeriesData(locationData: $0) : nil })