'Self is immutable' error when using Protocol function that is implemented in Protocol Extension

Hello,

I am attempting to make a system where a 'Signal Monitor' watches several 'Signal Generators' and something happens if one of the signal generators, well, generates a signal.

I thought that a pair of protocols would work well here. I have defined the protocols and also extended those to provide built-in implementations for some functions.

I want one Monitor and several Signal Generators. I have made them classes as I want them to be able to mutate themselves and I want the same instances to stick around.

The Monitor keeps track of the Signal Generators via an array. The Signal Generators keep a reference to the Monitor so that they can signal back to it.

This is all fine. Every time I make a new Signal Generator I can set its monitor property manually and add it to the tracking array manually. However, I thought that wouldn't it be nice to add a 'startMonitoring:signalGenerator' function to the Monitor so that it would both add the new generator to the array and also set the monitor property on the generator instance. I also thought that this would be good to put into the protocol extension so that there's an automatic implementation for any conforming type.

However this doesn't work and fails with an error Cannot use mutating member on immutable value: 'self' is immutable

Which surprises me, because all the relevant instances are vars and therefore should be immutable. When I replicate the functionality directly into the type it works.

What's the difference? What am I doing wrong?

Thanks.

import Foundation

@main
public struct GateControlMain {
	public private(set) var text = "Hello, World!"
	
	public static func main() {
		let runLoop = RunLoop.current
		let distantFuture = Date.distantFuture
		///	Set this to false when we want to exit the app...
		let shouldKeepRunning = true
		
		var gc = GateControl()
		gc.doSomething()
		gc.start()

		//	Run forever
		while shouldKeepRunning == true &&
				runLoop.run(mode:.default, before: distantFuture) {}
	}
	
}


//	Gate Signal Generation Protocols

///	A Type that can signal to the monitor that something has changed and it thinks that the gate should now be open.
protocol GateSignalGenerator {
	///	The Gate Signal Monitor (sort of like a delegate)
	var gateSignalMonitor: GateSignalMonitor? { get set }
	///	Should the gate be open as far as this signal generator is concerned?
	func shouldBeOpen() -> Bool
	///	Casues the Gate Signal Monitor to poll all signal generators because one of them has changed state.
	func triggerPollingOfAllSignals()
}


extension GateSignalGenerator {
	//	Default Implementation
	func triggerPollingOfAllSignals() {
		guard let monitor = self.gateSignalMonitor else {
			print("No monitor set for \(self)!!!!")
			return
		}
		monitor.requestToPollSignalGenerators(sender: self)
	}
}

///	A Type that monitors gate signals and when informed of a change, checks them all.
protocol GateSignalMonitor {
	///	All the monitored signals
	var gateSignalGenerators: [GateSignalGenerator] { get set }
	/// Add a signal generator to our observed signals
	mutating func startMonitoringSignalGenerator(_ sigGen: GateSignalGenerator)
	///	A way for the signals to notify the monitor that something has changed
	func requestToPollSignalGenerators(sender: GateSignalGenerator)
}

extension GateSignalMonitor {
	///	Adds the signal generator and makes sure that it's 'monitor' property is correctly set.
	mutating func startMonitoringSignalGenerator(_ sigGen: GateSignalGenerator) {
		gateSignalGenerators.append(sigGen)
	//	sigGen.gateSignalMonitor = self		//	Can't do this as we would not be altering the original sigGen, only a copy, even if the sigGen param is marked as inout.
	}
}



///	Controls the opening and closing of the gate
class GateControl : GateSignalMonitor {
	var gateSignalGenerators = [GateSignalGenerator]()
	var thingThatMutates = "I should change"
	
	var gateIsClosed = true
	var testTimer = ExampleOnOffTimer()

	init() {
		self.setupLogging()
		log.info("Gate Control Starts")
	}

	//  GateSignalMonitor Protocol
	func requestToPollSignalGenerators(sender: GateSignalGenerator) {
		log.info("\(sender) requested a poll of all signal generators")
		//	Check to see if any of them want the gate open.
	}
	

	
	func doSomething() {
		print(thingThatMutates)
		thingThatMutates = "I have changed"
		print(thingThatMutates)
	}
	
	func start() {
		testTimer.start()
		testTimer.gateSignalMonitor = self
		
		//		THIS FAILS (Cannot use mutating member on immutable value: 'self' is immutable)
		startMonitoringSignalGenerator(testTimer)
		//		THIS WORKS
		gateSignalGenerators.append(testTimer)
		
	}

	
}

class ExampleOnOffTimer: GateSignalGenerator {
...... code snipped here .....
}

That means you should remove the mutating prefix in front of functions, and use nonmutating set for properties. Mutating (in the context of classes) means that the function/setter can change what the reference points to

3 Likes

Ah, that's interesting, I didn't know that... I think that my mental model of how mutating works was incorrect.

This page looks useful:

It will be impossible to append to gateSignalGenerators if its setter is mutating, and the GateSignalMonitor is a value type. In the absence of any other code, it's not clear why you wouldn't add this constraint:

protocol GateSignalMonitor: AnyObject
3 Likes

Thanks. I'm a very inexpert (and yet long-term) hobbyist and am still getting my head around Swift vs my old Obj-C mindset. I didn't even think about there being the choice of constraining the protocol to only Classes. I think my brain is very loosely-typed!

It is fascinating how many planes of intersection there are in Swift.

Thanks both.

2 Likes

Almost. AnyObject used to effectively mean that (and was spelled that way!), but now it can refer to actors as well. In the future, it's possible that other types will be considered "objects", too.

1 Like

Oh no... Now I've changed something to use Generics (for the first time).... This really is a slippery slope.

3 Likes