4

I'm trying to write a protocol that reconciles two different structs that describe the same concept, a stop of some kind. Both have a Code, Description, and Latitude & Longitude coordinates, but for one type, the Description could be nil, and for the other type, the coordinates might be nil.

How can I write a single protocol that reconciles these two structs?

Here's my protocol:

protocol Stop {
    var Code : String { get }
    var Description : String { get }
    var Latitude : Double { get }
    var Longitude : Double { get }
}

And the two types of stops:

struct BusStop : Stop {  // Compiler error: doesn't implement Description
    var Code : String
    var Description : String?
    var Latitude : Double
    var Longitude : Double
    // Various other properties
}

struct TrainStop : Stop {  // Compiler error: doesn't implement Latitude or Longitude
    var Code : String
    var Description : String
    var Latitude : Double?
    var Longitude : Double?
    // Various other properties
}

In C# (my mother tongue), I would write an explicit interface implementation like so (pseudo-code):

// At the end of the BusStop struct
var Stop.Description : String { return Description ?? string.Empty }

// At the end of the TrainStop struct
var Stop.Latitude : Double { return Latitude ?? 0 }
var Stop.Longitude : Double { return Longitude ?? 0 }

However, I'm not aware of any similar functionality in Swift. Given that I'm unable to change the existing property definitions of BusStop and TrainStop, how can I write the Stop protocol so that it wraps around both structs and returns the properties when available?

Extragorey
  • 1,654
  • 16
  • 30

2 Answers2

4

The desired feature from explicit interface implementations is that they are statically dispatched, right? If you use Description on a BusStop, it will be an optional string, but if you use Description on a Stop, it will be a non-optional string.

In Swift, extension members are statically dispatched, so you can make use of this to achieve something similar:

extension Stop where Self == BusStop {
    // Since the type of "self" here is BusStop, "Description" refers to the one declared in BusStop
    // not this one here, so this won't cause infinite recursion
    var Description : String { return self.Description ?? "" }
}

extension Stop where Self == TrainStop {
    var Latitude: Double { return self.Latitude ?? 0 }
    var Longitude: Double { return self.Longitude ?? 0 }
}

This code shows that this works:

let busStop = BusStop(Code: "ABC", Description: "My Bus Stop", Latitude: 0, Longitude: 0)
print(type(of: busStop.Description)) // Optional<String>
let stop: Stop = busStop
print(type(of: stop.Description)) // String

However, I still don't think this is good Swift code. It is often bad to just directly translate an API from one language to another. If I were you, I would make Longitude, Latitude and Description in Stop to be all optionals.

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • Are you sure *extension members are statically dispatched* ? can you provide a reference to that please? – Mojtaba Hosseini Oct 08 '19 at 07:06
  • @MojtabaHosseini There is [this article](https://www.rightpoint.com/rplabs/switch-method-dispatch-table), and there is also a similar [SO post](https://stackoverflow.com/questions/43743114/how-can-i-explicitly-implement-a-protocol-in-swift-if-it-is-impossible-why) that shows the same behaviour. Can't find official sources though, but Swift official references IMO are always quite lacking. – Sweeper Oct 08 '19 at 07:13
  • If I make `Description`, `Latitude`, and `Longitude` optionals in `Stop` then I get the same compiler error on the structs that define them as non-optionals. Apparently optionals and non-optionals can't be implicitly casted in either direction. – Extragorey Oct 09 '19 at 01:30
  • @Extragorey I mean you should still use the same trick with protocol extensions, but make the three protocol properties optional. I am saying that having a protocol with non-optional properties but conforming classes with optional properties doesn't make much sense. – Sweeper Oct 09 '19 at 05:39
  • 1
    I would give it another +1 if I could. very handy. – Mojtaba Hosseini Oct 15 '19 at 20:45
0

I agree with @Sweeper, it might be that your design is not the best one, if you found the need to bring different data layouts under the same umbrella. Nonetheless, I want to share another possible approach to your problem.

Instead of having all Stop properties into one protocol, you could shove them in another struct, and have the Stop protocol return that struct:

protocol Stop {
    var stopData: StopData { get }
}

struct StopData {
    var code: String
    var stopDescription: String
    var latitude: Double
    var longitude: Double
}

You can then add the following conformances for your two structs:

struct BusStop: Stop {
    var code: String
    var busStopDescription: String?
    var latitude: Double
    var longitude: Double

    var stopData: StopData {
        return StopData(code: code,
                        stopDescription: busStopDescription ?? "",
                        latitude: latitude,
                        longitude: longitude)
    }
}

struct TrainStop: Stop {
    var code: String
    var trainStopDescription: String
    var latitude: Double?
    var longitude: Double?

    var stopData: StopData {
        return StopData(code: code,
                        stopDescription: trainStopDescription,
                        latitude: latitude ?? 0,
                        longitude: longitude ?? 0)
    }
}

This means that you'll be circulating StopData instances in the rest of the app instead of the Stop protocol.

P.S. I also changed the property names to be more in line with the Swift naming guidelines.

Cristik
  • 30,989
  • 25
  • 91
  • 127
  • I was thinking of this last week but at last, I don't think this is a good idea. because there is a **unnecessary allocation at each property call**! and that could cause an epic performance drop, specially when the protocol has too many properties. This approach would be nice if swift had *nested computed properties*. – Mojtaba Hosseini Oct 08 '19 at 07:05
  • @MojtabaHosseini structs are cheap to create and use. Also note that "premature optimisation is the root of all evil" (Donand Knuth). If you think that some code poses performance problems, make sure to profile it to see if it actually has a performance impact. – Cristik Oct 08 '19 at 07:10
  • What if structs representing *SwiftUI*'s `View`s? Are those cheap enough to allocate and deallocate without concern? But I'm aggree with you. I must profile and see myself in action. – Mojtaba Hosseini Oct 08 '19 at 07:13
  • @MojtabaHosseini structs are structs, regardless what they represent. And let's not deviate from the scope of the question. – Cristik Oct 08 '19 at 07:18
  • I've been tasked with merging the functionality of two similar apps, hence the design is not ideal, but I'm trying to unify concepts where I can to reduce duplicate code. – Extragorey Oct 09 '19 at 01:40
  • This approach is similar to the idea of simply defining new fields in the protocol and implementing all those fields on each struct, i.e. `StopCode`, `StopDescription`, etc., which I'm trying to avoid as it adds a lot of boilerplate code to each stop type. But I'll take it as a last resort. – Extragorey Oct 09 '19 at 01:42