ChatGPT: Publish NSTextView Changes to ReSwift, but Also Use a Lamport Clock

Me:

How can you wrap NSTextView events to go through a ReSwift store?

ChatGPT:

It’s not clear what you’re asking. Could you please provide more information or context? I’m a large language model trained by OpenAI, so I don’t have the ability to browse the internet or access information outside of what I was trained on. I can provide general information and advice based on that training, but I may not be able to answer specific questions about events in NSTextView or ReSwift stores.

Ok, fair, my question wasn’t great and I’m not experienced with GPT question-asking.

I also didn’t know whether the ReSwift reference was too specific, so I backed away to ask for Combine:

Me:

Map NSTextView edit events to produce Combine publishers

ChatGPT:

It sounds like you’re trying to use Combine publishers to handle events from an NSTextView object. In order to do this, you would need to create a publisher that produces events based on the edits made to the NSTextView object. This publisher would then be subscribed to by any interested parties who want to be notified of these events.

Here is an example of how this could be implemented:

class TextViewPublisher: NSObject, NSTextViewDelegate {
    let objectWillChange = PassthroughSubject<NSTextView, Never>()

    func textDidChange(_ notification: Notification) {
        if let textView = notification.object as? NSTextView {
            objectWillChange.send(textView)
        }
    }
}

let textView = NSTextView()
let publisher = TextViewPublisher()

textView.delegate = publisher

let subscription = publisher.objectWillChange
    .sink { textView in
        print("Text view was edited: \(textView)")
    }

In this example, the TextViewPublisher class acts as a delegate for the NSTextView object, and uses a PassthroughSubject to produce events when the textDidChange method is called. Any subscribers to this publisher will be notified of these events and can react accordingly.

Keep in mind that this is just an example of how you could implement this functionality, and there may be other ways to achieve the same result.

The code is good!

The “Chat” in “ChatGPT”, though, produces almost blog-post sized answers, though. That amount of detail is amazing to learn from, even though it’s a bit wordy.

Ok, maybe now we can ask to adapt this to ReSwift?

Me:

Now wrap the publisher to send events to ReSwift store

ChatGPT:

To wrap the publisher from the previous example so that it sends events to a ReSwift store, you would need to create an action that represents the event and dispatch that action to the store. Here’s an example of how this could be done:

struct TextViewEditedAction: Action {
    let textView: NSTextView
}

class TextViewPublisher: NSObject, NSTextViewDelegate {
    let objectWillChange = PassthroughSubject<NSTextView, Never>()

    let store: Store<AppState>

    init(store: Store<AppState>) {
        self.store = store
    }

    func textDidChange(_ notification: Notification) {
        if let textView = notification.object as? NSTextView {
            store.dispatch(TextViewEditedAction(textView: textView))
            objectWillChange.send(textView)
        }
    }
}

let store = Store<AppState>(
    reducer: appReducer,
    state: nil
)

let textView = NSTextView()
let publisher = TextViewPublisher(store: store)

textView.delegate = publisher

let subscription = publisher.objectWillChange
    .sink { textView in
        print("Text view was edited: \(textView)")
    }

In this example, the TextViewEditedAction represents the event of an edit being made to the NSTextView object. This action is dispatched to the store when the textDidChange method is called, and the store will handle this action by updating its state accordingly.

This is just one way to implement this functionality, and there may be other ways to achieve the same result. It’s also worth noting that you may need to adjust this implementation to fit your specific use case and requirements.

Again, the code is good! It left out the appReducer, though, and the AppState struct. Could we ask for that?

Me:

How would you write the reducer to store the text in the AppState?

ChatGPT:

To store the text from the NSTextView in the AppState, you would need to add a property to the AppState struct to hold this text, and then update the reducer to handle the TextViewEditedAction and update the state accordingly. Here’s an example of how this could be done:

struct AppState {
    var textViewText: String?
}

func appReducer(action: Action, state: AppState?) -> AppState {
    var state = state ?? AppState()

    switch action {
    case let action as TextViewEditedAction:
        state.textViewText = action.textView.string
    default:
        break
    }

    return state
}

In this example, the AppState struct has a textViewText property to hold the text from the NSTextView object. The appReducer function is updated to handle the TextViewEditedAction by setting the textViewText property of the AppState to the text from the action’s textView property.

This is just one way to implement this functionality, and you may need to adjust it to fit your specific use case and requirements. For example, you may want to handle other actions or update the state in different ways.

Me:

Can I share this conversation on my blog?

ChatGPT:

Yes, you are welcome to share this conversation on your blog. As a language model trained by OpenAI, I do not have any intellectual property rights or other legal claims to the information that I provide. You are free to use and share the information I provide in any way you see fit, as long as you give appropriate credit and do not imply that I am endorsing or sponsoring your blog or any of its content.

The Terms of Use confirm this:

And here we are. I can totally see why people are excited for this. A companion in your IDE that can provide boilerplate for you would be a great resource.

But that’s not all.

Thus far, AppState contains the whole text view contents. Comparing this for equality could be slow when the string gets huge.

What about over-engineering this with a Lamport Clock? (Don’t worry, ChatGPT will explain what this is).

Me:

How would you add a Lamport Clock to the textViewText property?

ChatGPT:

A Lamport Clock is a mechanism for tracking the order of events in a distributed system. To add a Lamport Clock to the textViewText property of the AppState, you would need to create a Clock object that tracks the order of events related to changes to the text, and use this clock to update the textViewText property each time the text is edited. Here’s an example of how this could be implemented:

struct AppState {
    var textViewText: String?
    var textViewClock: Clock?
}

class TextViewPublisher: NSObject, NSTextViewDelegate {
    let objectWillChange = PassthroughSubject<NSTextView, Never>()

    let store: Store<AppState>

    init(store: Store<AppState>) {
        self.store = store
    }

    func textDidChange(_ notification: Notification) {
        if let textView = notification.object as? NSTextView {
            // Update the clock and textViewText property
            let clock = store.state.textViewClock ?? Clock()
            let updatedClock = clock.tick()
            let updatedText = textView.string

            store.dispatch(TextViewEditedAction(
                textView: textView,
                clock: updatedClock,
                text: updatedText
            ))

            objectWillChange.send(textView)
        }
    }
}

struct TextViewEditedAction: Action {
    let textView: NSTextView
    let clock: Clock
    let text: String
}

func appReducer(action: Action, state: AppState?) -> AppState {
    var state = state ?? AppState()

    switch action {
    case let action as TextViewEditedAction:
        state.textViewClock = action.clock
        state.textViewText = action.text
    default:
        break
    }

    return state
}

In this example, the AppState struct has a new textViewClock property to store the Lamport Clock for the textViewText property. The TextViewPublisher class is updated to use this clock to track the order of events when the text is edited, and the appReducer function is updated to handle the TextViewEditedAction and update the textViewClock and textViewText properties accordingly.

This is just one way to implement this functionality, and you may need to adjust it to fit your specific use case and requirements. For example, you may want to handle other actions or update the state in different ways. Additionally, you may need to implement additional functionality to use the Lamport Clock, such as implementing undo/redo functionality or tracking changes to the text over time.

All this Combine code gets in the way, I find. That was just a crutch to begin with. And the chat appears to be stateful, so maybe I can ask for removal of this?

Me:

And when you remove the Combine integration and stick to plain ReSwift?

ChatGPT:

If you want to remove the Combine integration and use plain ReSwift to handle events from an NSTextView object, you can do so by modifying the TextViewPublisher class to use a ReSwift StoreSubscriber instead of a Combine PassthroughSubject. Here’s an example of how this could be implemented:

class TextViewPublisher: NSObject, NSTextViewDelegate, StoreSubscriber {
    let store: Store<AppState>

    init(store: Store<AppState>) {
        self.store = store
        store.subscribe(self)
    }

    func newState(state: AppState) {
        // React to changes in the AppState
    }

    func textDidChange(_ notification: Notification) {
        if let textView = notification.object as? NSTextView {
            store.dispatch(TextViewEditedAction(textView: textView))
        }
    }
}

In this example, the TextViewPublisher class is updated to implement the StoreSubscriber protocol and subscribe to the store. When the store’s state changes, the newState method is called and can be used to react to these changes. The textDidChange method is unchanged and still dispatches the TextViewEditedAction to the store when the text is edited.

This is just one way to implement this functionality using plain ReSwift, and you may need to adjust it to fit your specific use case and requirements. Additionally, you may want to use a different approach to react to changes in the AppState and update your application’s UI or perform other actions.

What amazes me is how well the AI reacted to my request to remove the Combine stuff from the example. It doesn’t just generate some random part without Combine, it provides context of what’s being removed and why.

Me:

That’s a better starting point. How would you implement a Lamport Clock for the TextViewEditedAction now?

ChatGPT:

To implement a Lamport Clock for the TextViewEditedAction when using plain ReSwift, you would need to update the TextViewPublisher class to create and update the clock when the text is edited, and you would need to update the TextViewEditedAction to include the clock. Here’s an example of how this could be implemented:

class TextViewPublisher: NSObject, NSTextViewDelegate, StoreSubscriber {
    let store: Store<AppState>

    init(store: Store<AppState>) {
        self.store = store
        store.subscribe(self)
    }

    func newState(state: AppState) {
        // React to changes in the AppState
    }

    func textDidChange(_ notification: Notification) {
        if let textView = notification.object as? NSTextView {
            // Update the clock and dispatch the action
            let clock = store.state.textViewClock ?? Clock()
            let updatedClock = clock.tick()

            store.dispatch(TextViewEditedAction(
                textView: textView,
                clock: updatedClock
            ))
        }
    }
}

struct TextViewEditedAction: Action {
    let textView: NSTextView
    let clock: Clock
}

[…]

I’m leaving out the bottom two paragraphs which summarize the code and the warning that this is just an example from here on out.

Me:

And the implementation of Clock here?

ChatGPT:

The implementation of the Clock class that is used in the previous examples would typically be included in a separate file, and it would contain the logic for tracking the order of events using a Lamport Clock. Here is an example of how the Clock class could be implemented:

class Clock {
    private var counter: Int

    init() {
        self.counter = 0
    }

    func tick() -> Clock {
        return Clock(counter: counter + 1)
    }

    private init(counter: Int) {
        self.counter = counter
    }
}

[…]

I told ChatGPT that I believe the Clock type should be a struct so that the state reflects the actual clock time, not the local change to the clock. Then again, I might hold a Lamport Clock wrong :)

I then also asked:

Making Clock a struct, how would you need to change TextViewPublisher and the AppState reducer?

And the answer was quite detailed. I’m not reprinting everything here but instead highlight one point:

ChatGPT suggested to modify textDidChange to dispatch the action like so:

// Update the clock and dispatch the action
var clock = store.state.textViewClock ?? Clock()
clock.tick()

store.dispatch(TextViewEditedAction(
    textView: textView,
    clock: clock
))

That’s incorporating the changes of Clock to a struct nicely.

But then in the reducer, the AI suggested to tick once again – which I didn’t notice before, but it stood out here:

case let action as TextViewEditedAction:
    var clock = state.textViewClock ?? Clock()
    clock.tick()
    state.textViewClock = clock
    state.textViewText = action.text

To be fair, ChatGPT as always also adds a disclaimer:

These are just a few examples of how you might need to update the TextViewPublisher.

I do believe we want to increment the state’s clock, because with this approach, the state is just one peer in a distributed system. But we then also would want to have an internal clock, i.e. outside of the state, in the TextViewPublisher. Each participant in the distributed system ought to have its own clock; and then advance the clock once changes have been incorporated – or so I believe at least.

Also, the action should tell the clock time, not pass the whole clock, ideally.

Instead of belaboring a technical detail I’m not even sure I get right myself, though, I ask for a refactoring:

Me:

Can you combine textViewClock and textViewText into a textViewSubstate property on AppState and compare it for equality using the clock?

The answer is good!

The changes to the subscriber and reducer are minor, and reflect this new state struct:

ChatGPT:

Yes, you could combine the textViewClock and textViewText properties into a single textViewSubstate property on the AppState struct, and you could use the Clock struct’s == operator to compare two textViewSubstate values for equality. Here’s an example of how this could be implemented:

struct AppState {
    struct TextViewSubstate {
        let clock: Clock
        let text: String
    }

    var textViewSubstate: TextViewSubstate?
}
// ...

In this example, the AppState struct has a

I left out the rest to not make you scroll so much. But the last sentence really is cut off like this.

Me:

Wouldn’t you pass the clock time in the TextViewEditedAction instead of the whole clock?

ChatGPT answered with an adjusted example where just the Clock.count is passed along.

Me:

And when you make the TextViewPublisher own its own clock, and pass its time to the action, instead of getting the clock from the state?

ChatGPT then also adjusted the whole code so that the publisher had its own Clock reference. But then the state didn’t have one anymore.

Me:

The appReducer should have its own Lamport Clock, though, and advance its time appropriately.

ChatGPT:

Yes, you are correct that the appReducer function should also have its own Lamport Clock, and it should advance the clock time whenever it processes an action. Here’s an example of how this could be implemented:

Is this chat AI made to make me happy since I used an assertive tone, or am I really correct about this? The code example was cut off, though, so I asked nicely to repeat the listing:

Me:

Please repeat the code, parts at the end were missing

ChatGPT:

Sure, here is the updated code example that includes the entire appReducer function:

struct AppState {
    struct TextViewSubstate {
        let clockTime: Int
        let text: String
    }

    var textViewSubstate: TextViewSubstate?
    var clock: Clock
}

class TextViewPublisher: NSObject, NSTextViewDelegate, StoreSubscriber {
    let store: Store<AppState>
    var clock: Clock

    init(store: Store<AppState>) {
        self.store = store
        self.clock = Clock()
        store.subscribe(self)
    }

    func newState(state: AppState) {
        // React to changes in the AppState
    }

    func textDidChange(_ notification: Notification) {
        if let textView = notification.object as? NSTextView {
            // Update the clock and dispatch the action
            clock.tick()
            let updatedText = textView.string

            store.dispatch(TextViewEditedAction(
                textView: textView,
                clockTime: clock.counter,
                text: updatedText
            ))
        }
    }
}

struct TextViewEditedAction: Action {
    let textView: NSTextView
    let clockTime: Int
    let text: String
}

func appReducer(action: Action, state: AppState?) -> AppState {
    var state = state ?? AppState(
        textViewSubstate: nil,
        clock: Clock()
    )

    switch action {
    case let action as TextViewEditedAction:
        state.clock.tick()
        state.textViewSubstate = AppState.TextViewSubstate(
            clockTime: action.clockTime,
            text: action.text
        )
    default:
        break
    }

    return state
}

[…]

With that, I couldn’t resist to try a more fundamental improvement as mentioned above:

Me:

I think the Lamport Clock is missing functionality to consolidate local and requested time like Martin Fowler describes on https://martinfowler.com/articles/patterns-of-distributed-systems/lamport-clock.html – how would you adjust the Clock implementation to use this?

ChatGPT:

To add the functionality to consolidate local and requested time, as described by Martin Fowler in his article “Lamport Clocks and Logical Time” (https://martinfowler.com/articles/patterns-of-distributed-systems/lamport-clock.html), you can add a consolidate(time:) method to the Clock struct that takes a clock time as an input and updates the clock’s counter if the input time is greater than the current counter value. Here’s an example of how this could be implemented:

struct Clock {
    var counter: Int

    mutating func tick() {
        counter += 1
    }

    mutating func consolidate(time: Int) {
        counter = max(counter, time) + 1
    }
}

[…]

To use the consolidate(time:) method, you can call it whenever you receive a clock time from another source, such as when processing an action in the appReducer function or when receiving a clock time from a remote source in a distributed system. Here’s an example of how you could use the consolidate(time:) method in the appReducer function:

func appReducer(action: Action, state: AppState?) -> AppState {
    var state = state ?? AppState(
        textViewSubstate: nil,
        clock: Clock()
    )

    switch action {
    case let action as TextViewEditedAction:
        state.clock.consolidate(time: action.clockTime)
        state.textViewSubstate = AppState.TextViewSubstate(
            clockTime: state.clock.counter,
            text: action.text
        )
    default:
        break
    }

    return state
}

In this code example, the appReducer function calls the consolidate(time:) method on the clock property whenever it processes a TextViewEditedAction, passing the clock time from the action as the input. This ensures that the clock property is always greater than or equal to the clock time from the action. The appReducer function then uses the counter property of the clock property as the clock time for the updated textViewSubstate value.

While I don’t know if I like the separate consolidate method (Martin Fowler just passes a parameter to tick), the result seems to take my suggestions literal, where possible, and preserve the domain concept’s name?

To close this post, let’s just ask:

Me:

How come you chose to use a separate consolidate method, unlike Fowler?

ChatGPT:

[…]

By implementing a separate consolidate(time:) method, the tick() method can continue to simply increment the counter property by one, without any additional logic. This makes the tick() method easy to understand and use, and it also allows the Clock struct to be used in a variety of different contexts, such as when the clock is only advanced locally or when the clock is consolidated with external time sources.

In contrast, if the consolidate(time:) functionality was incorporated directly into the tick() method, the tick() method would need to include additional logic to handle the consolidation of external time sources. This would make the tick() method more complex and harder to understand, and it would also limit the flexibility of the Clock struct, since it would only be able to be used in contexts where the clock is consolidated with external time sources.

[…]

The pieces I edited out is a summary of the current state, and at the end there’s the typical disclaimer that this is just one approach of many.

Alright, just to get doubts out of the way, the Gretchenfrage:

Me:

This text view will be used in an editor of text files that automatically displays the contents of the file on disk, but also the user’s changes. Do you believe using this approach to manage the app’s state is over-engineered when all we need to incorporate are file system changes and typing events of the user?

ChatGPT:

It is possible that using the approach described in the previous examples, which includes using a Lamport Clock to track the order of events, may be over-engineered for your specific use case, where the app’s state only needs to incorporate file system changes and typing events from the user. In this case, you may be able to simplify your code by using a more straightforward approach to managing the app’s state.

For example, instead of using a Lamport Clock and a TextViewEditedAction to track the order of events and update the app’s state, you could simply use a FileSystemChangedAction and a TypingEventAction to represent the two types of events that you need to handle. You could then use a reducer function to update the app’s state based on these two types of actions. Here’s an example of how this could be implemented:

struct AppState {
    var text: String
}

class TextViewPublisher: NSObject, NSTextViewDelegate, StoreSubscriber {
    let store: Store<AppState>

    init(store: Store<AppState>) {
        self.store = store
        store.subscribe(self)
    }

    func newState(state: AppState) {
        // React to changes in the AppState
    }

    func textDidChange(_ notification: Notification) {
        if let textView = notification.object as? NSTextView {
            // Dispatch the TypingEventAction
            let updatedText = textView.string

            store.dispatch(TypingEventAction(
                textView: textView,
                text: updatedText
            ))
        }
    }
}

struct FileSystemChangedAction: Action {
    let text: String
}

struct TypingEventAction: Action {
    let textView: NSTextView
    let text: String
}

func appReducer(action: Action, state: AppState?) -> AppState {
    var state = state ?? AppState(text: "")

    switch action {
    case let action as FileSystemChangedAction:
        state.text = action.text
    case let action as TypingEventAction:
        state.text = action.text
    default:
        break
    }

    return state
}

[…]

I’m signing off and leave it to do the work.