davecom / SwiftGraph

A Graph Data Structure in Pure Swift
Apache License 2.0
758 stars 80 forks source link

Make conformances to Codable conditional #81

Closed mrwerdo closed 2 years ago

mrwerdo commented 3 years ago

I've ran into a problem when using the graph types with types that don't conform to Codable. Previously, if you used WeightedGraph<V, W>, then V and W both must conform to Codable, which prohibits WeightedGraph from being used in the case where V and W don't conform to Codable.

This pull request makes the conformance to Encodable and Decodable conditional for UnweightedGraph, WeightedGraph, and UniqueElementsGraph.

The conformances should be the same as the generated code by the compiler when confirming to Codable.

There are breaking changes, namely that:

Let me know what you think. I'm welcome to any feedback you have to offer. Thanks.

davecom commented 3 years ago

Hi @Mrwerdo,

Thanks for the contribution and detailed explanation. There's actually quite a bit of history here and I'm not sure if I remember it all correctly because it's been years now, but: When Codable was introduced in Swift 4, we initially added subclasses of WeightedGraph and UnweightedGraph that conformed to it. Some time later, if I'm remembering correctly, we tried this approach of conditional conformance when it became available and there were some issues in the Swift language (bugs really I think) that prevented us from going this direction.

We went to full Codable compliance as a simplification for most people. May I ask what your use case is? Do you have a type that's not Codable that you want to use with SwiftGraph?

I'm not opposed to the change really, but since we already have changed this multiple times, I don't want to "pull the rug out from under people" so to speak who may be using it the way it is. I'd also like to maybe leave this open for a while to hear other people's perspectives (sometimes that never happens in a middling popular project, but we'll see).

Thanks, David

mrwerdo commented 3 years ago

Hi David,

You're welcome. Hopefully someone will be able to shed some light on whether these changes are practical or not for the community.

My use case is modelling a network for a game I'm making in my spare time. Some of the structures are like so:

struct Message {
    enum Address {
         case all
         case device(Device)
    }
    var destination: Address
    var source: Device
}

class Device {
    var sentMessages: [Message] = []
    var receivedMessages: [Message] = []
}

var topology = UnweightedGraph<Device>()

Making Message.Address conform to Codable is unclear because the encoder would need to resolve the references to instances of Device. At this point, it's easier for me to make UnweightedGraph conform conditionally to Codable than it is to implement an encoding and decoding mechanism.

Maybe there's another way of making the changes without breaking existing clients? Perhaps by creating new types and deprecating old ones? I'm not fussed if you reject the pull request, I just thought that I'd see whether you were interested in them.

-Andrew

sbeitzel commented 3 years ago

I'm not yet a user of this project (although I'm planning to incorporate it into a personal project soon) so my opinion may not have much weight. That said, @Mrwerdo , I think the affordance that comes from enforced adherence to Codable is that when/if GraphML or GraphViz exports are supported (through custom encoders) then everything will Just Work. Meanwhile, your specific case could be handled with a little work:

struct Message {
    enum Address {
        case all
        case device(String) // device ID
    }
    var destination: Address
    var source: String // device ID
}

class Device: Identifiable {
    static let devNull = Device()

    let id: String = UUID().uuidString
...
}

class DeviceManager {
    var deviceDictionary = [String: Device]()

    func createDevice() -> Device {
        let device = Device()
        deviceDictionary[device.id] = device
        return device
    }

    func lookup(_ deviceID: String) -> Device {
        if let found = deviceDictionary[deviceID] {
            return found
        }
        return Device.devNull
    }
}

...and so on. This would make it easy for Device and Message to be Codable, making it easy to save and restore your game state, as well.

davecom commented 2 years ago

I really appreciate the effort here, but I think for most users of the library the default conformance is more of a help than a hindrance. I am also concerned about changing the defaults around this area once again when we've already changed them in the past. Going back and forth for a few users of the library who are working with it in this area is not good. And I agree with @sbeitzel, that it may be really nice that we had it by default in the future if a contribution for GraphML or GraphViz comes through.