Naming conventions for convenience methods on enums

I'm wanting to extend DispatchTimeInterval to include convenience methods that provide conversion from one measurement unit to another. DispatchTimeInterval's cases are defined as such:

public enum DispatchTimeInterval : Equatable {

    case seconds(Int)
    case milliseconds(Int)
    case microseconds(Int)
    case nanoseconds(Int)
    case never
}

The first type of convenience method I want to add allows the user to retrieve an Int value representing the interval in a different unit. For example:

extension DispatchTimeInterval {

    func rawMicroseconds() -> Int {

        switch self {
        case .seconds(let secs):
            return secs * 1_000_000
        case .milliseconds(let millis):
            return millis * 1_000
        case .microseconds(let micros):
            return micros
        case .nanoseconds(let nano):
            return nano / 1_000
        case .never:
            return 0
        default:
            return 0
        }
    }
}

I would also like another set of methods that returns a new DispatchTimeInterval instance in a different specified time unit. For example:

extension DispatchTimeInterval {

    func asMicroseconds() -> DispatchTimeInterval {
		return .microseconds(self.rawMicroseconds())
    }
}

I've never done this before in Swift, and I'm quite new to the language. Do you think my choice of method names falls within the API Design Guidelines? I may have missed it, but I don't recall seeing anything in the API Design Guidelines that would lead me to believe that I've gotten the method naming correct. Which leads me to believe I might be wrong. The associated value, probably isn't best described as a raw value, or is it? "as" is used for down casting, but I'm not sure how to indicate that a method returns a new enum that expresses the original value as a different case.

Any clarity on these issues would be great!

1 Like

I'll have to spend some more time mulling this over, but I have two initial reactions:

  1. You should probably define rawMicroseconds as a computed property rather than as a function (i.e. I would tend to prefer var rawMicroseconds: Int over func rawMicroseconds() -> Int).

  2. This admittedly isn't related to naming conventions, but I think you may have reversed the division and multiplication operations in your implementation of rawMicroseconds (i.e. you should probably multiply seconds by 1,000,000 and divide nanoseconds by 1,000 to get their values in microseconds).

Hopefully someone more knowledgeable than me will be able to give you some further advice. :)

1 Like
  1. good point! lol

Was also wondering about declaring a DispatchTimeUnit enum (seems like Dispatch should have this type anyway) and passing it to an intValue() method to do the conversion.

enum DispatchTimeUnit {
    case second
    case millisecond
    case microsecond
    case nanosecond
}

func intValue(_ unit: DispatchTimeUnit) -> Int {
	return ...
}

I don’t think the raw prefix is necessary. If they don’t conflict with the case names at compile time, simply naming the computed properties after the unit type would be the most Swifty to me.

2 Likes

That sounds Swifty to me after doing some exploring this afternoon. I just don't know how to tackle the conversion to other DispatchTimeIntervals. I also don't want to introduce ambiguity, because it may not be evident from the call site whether interval.microseconds returns an Int or a new instance of the DispatchTimeInterval.microseconds enum case. That's why I used "raw" prefix when returning an Int.

I was thinking of implementing:

func convert(to unit: DispatchTimeUnit) {
     ...
}

to convert the existing instance, and:

func converted(to unit: DispatchTimeUnit) -> DispatchTimeInterval {
     ...
}

to generate a new instance of the time unit passed in. But then I need this awkward new type that might sow confusion with the DispatchTimeInterval enum cases of a similar name:

enum DispatchTimeUnit {
    case second
    case millisecond
    case microsecond
    case nanosecond
}

I would second the previous tweaks.

Question on approach (take it or leave it :blush:):
You didn't explain your use case, but I'm suspicious you could be working with DateComponents or DispatchTime instead of directly with DispatchTimeInterval

Regardless, you can leverage DispatchTime to avoid most of your math and simplify the code:

extension DispatchTimeInterval {

    var nanoSeconds: Int {
        let currentTime = DispatchTime.now()
        let timeDifference = (currentTime + self).uptimeNanoseconds - currentTime.uptimeNanoseconds
        return Int(timeDifference)
    }

    var microSeconds: Int {
        return nanoSeconds / 1_000
    }

    var asmicroseconds: DispatchTimeInterval {
        return .microseconds(microSeconds)
    }
}

If I needed to go down this road I would probably do this (not fully tested):

extension DispatchTimeInterval {

    private var nanoSeconds: Int {
        let currentTime = DispatchTime.now()
        let timeDifference = (currentTime + self).uptimeNanoseconds - currentTime.uptimeNanoseconds
        return Int(timeDifference)
    }
    var asNanoseconds: DispatchTimeInterval { return .nanoseconds(microSeconds) }

    private var microSeconds: Int { return nanoSeconds / 1_000 }
    var asMicroseconds: DispatchTimeInterval { return .microseconds(microSeconds) }

    private var milliSeconds: Int { return microSeconds / 1_000 }
    var asMilliseconds: DispatchTimeInterval { return .milliseconds(milliSeconds) }

    private var seconds: Int { return milliSeconds / 1_000 }
    var asSeconds: DispatchTimeInterval { return .seconds(seconds)}
}

(note that the raw values are private. Any arithmetic can be done with DispatchTime)

You could involve a whole other enum...but IMO this is small enough to not be an issue.

As far as conversion between different flavors of DispatchTimeInterval, my instinct would be to just extend the existing initialization syntax.

For example, where a DispatchTimeInterval is expected, you can currently say

.microseconds(100)

So just extend things so that you can also say

.microseconds(otherInterval)

where otherInterval is an existing DispatchTimeInterval.

extension DispatchTimeInterval {
    static func microseconds(_ interval: DispatchTimeInterval) -> DispatchTimeInterval {
        return .microseconds(self.microseconds())
    }
}

(Assuming you've defined microseconds et al already...)

There shouldn't be any conflict between microseconds as a function name and .microseconds as an enum value. I guess you could not have microseconds as a computed property also, though.

Edit: For some reason, I originally thought there wouldn't be a conflict between a computed property and a function (because the function has arguments and the sought-type of self.microseconds (Int) is distinct), but that's not true. However, a function microseconds() does work.

2 Likes

Okay, I thought this over and—inspired by @GarthSnyder's answer—here's what I came up with:

extension DispatchTimeInterval {

    var seconds: Int {
        switch self {
        case .seconds(let s):       return s
        case .milliseconds(let ms): return ms / 1_000
        case .microseconds(let μs): return μs / 1_000_000
        case .nanoseconds(let ns):  return ns / 1_000_000_000
        case .never:                return 0
        }
    }

    var milliseconds: Int {
        switch self {
        case .seconds(let s):       return s  * 1_000
        case .milliseconds(let ms): return ms
        case .microseconds(let μs): return μs / 1_000
        case .nanoseconds(let ns):  return ns / 1_000_000
        case .never:                return 0
        }
    }

    var microseconds: Int {
        switch self {
        case .seconds(let s):       return s  * 1_000_000
        case .milliseconds(let ms): return ms * 1_000
        case .microseconds(let μs): return μs
        case .nanoseconds(let ns):  return ns / 1_000
        case .never:                return 0
        }
    }

    var nanoseconds: Int {
        switch self {
        case .seconds(let s):       return s  * 1_000_000_000
        case .milliseconds(let ms): return ms * 1_000_000
        case .microseconds(let μs): return μs * 1_000
        case .nanoseconds(let ns):  return ns
        case .never:                return 0
        }
    }

    static func seconds(from interval: DispatchTimeInterval) -> DispatchTimeInterval {
        return DispatchTimeInterval.seconds(interval.seconds)
    }

    static func milliseconds(from interval: DispatchTimeInterval) -> DispatchTimeInterval {
        return DispatchTimeInterval.milliseconds(interval.milliseconds)
    }

    static func microseconds(from interval: DispatchTimeInterval) -> DispatchTimeInterval {
        return DispatchTimeInterval.microseconds(interval.microseconds)
    }

    static func nanoseconds(from interval: DispatchTimeInterval) -> DispatchTimeInterval {
        return DispatchTimeInterval.nanoseconds(interval.nanoseconds)
    }
}

A few things to note:

  1. I did away with the default case statement in your original code. If it were me and Apple decided to add more enumeration cases to DispatchTimeInterval in the future, I'd want the compiler to warn me that I'm no longer covering them all instead of silently falling back to a default implementation.

  2. Even though there are now three different versions of microseconds et al., they all have different signatures:

    • case microseconds(Int)
    • static func microseconds(from interval: DispatchTimeInterval) -> DispatchTimeInterval
    • var microseconds: Int

    This should (hopefully) mitigate any possible confusion about which one is being called.

  3. When returning the number of e.g. microseconds in an interval as an integer value, all fractional components will be truncated:

    // 999999999 nanoseconds
    let nanoseconds = DispatchTimeInterval.nanoseconds(999999999)
    
    // 0 seconds
    let seconds = DispatchTimeInterval.seconds(from: nanoseconds)
    

    This may or may not be the behavior you intend, especially if you were to convert a high-precision value (like nanoseconds) to a low-precision value (like seconds) and then back to a high-precision value (like microseconds) again.

  4. It may be more semantically consistent to return Int.max instead of 0 for .never. Put another way, I would have used a value of 0 to represent a DispatchTimeInterval of .now—were it to exist—to indicate that something should happen as soon as possible. Similarly, I would probably use a value of Int.max to indicate that something should happen as far in the future as possible.

Honestly, though? The more I think about it, the more I think the following approach may be better:

extension DispatchTimeInterval {

    static func seconds(from interval: DispatchTimeInterval) -> DispatchTimeInterval {
        switch interval {
        case .seconds(let s):       return .seconds(s)
        case .milliseconds(let ms): return .seconds(ms / 1_000)
        case .microseconds(let μs): return .seconds(μs / 1_000_000)
        case .nanoseconds(let ns):  return .seconds(ns / 1_000_000_000)
        case .never:                return interval
        }
    }

    static func milliseconds(from interval: DispatchTimeInterval) -> DispatchTimeInterval {
        switch interval {
        case .seconds(let s):       return .milliseconds(s  * 1_000)
        case .milliseconds(let ms): return .milliseconds(ms)
        case .microseconds(let μs): return .milliseconds(μs / 1_000)
        case .nanoseconds(let ns):  return .milliseconds(ns / 1_000_000)
        case .never:                return interval
        }
    }

    static func microseconds(from interval: DispatchTimeInterval) -> DispatchTimeInterval {
        switch interval {
        case .seconds(let s):       return .microseconds(s  * 1_000_000)
        case .milliseconds(let ms): return .microseconds(ms * 1_000)
        case .microseconds(let μs): return .microseconds(μs)
        case .nanoseconds(let ns):  return .microseconds(ns / 1_000)
        case .never:                return interval
        }
   }

    static func nanoseconds(from interval: DispatchTimeInterval) -> DispatchTimeInterval {
        switch interval {
        case .seconds(let s):       return .nanoseconds(s  * 1_000_000_000)
        case .milliseconds(let ms): return .nanoseconds(ms * 1_000_000)
        case .microseconds(let μs): return .nanoseconds(μs * 1_000)
        case .nanoseconds(let ns):  return .nanoseconds(ns)
        case .never:                return interval
        }
    }
}

This is nice because it allows you to easily convert between units while still preserving .never semantics. If you didn't want to use enumeration case patterns—and deal with their sometimes inscrutable syntax—to extract the associated values, you could try adding an initializer to Int:

extension Int {
    init?(_ interval: DispatchTimeInterval) {
        switch interval {
        case .seconds(let s):       self.init(s)
        case .milliseconds(let ms): self.init(ms)
        case .microseconds(let μs): self.init(μs)
        case .nanoseconds(let ns):  self.init(ns)
        case .never:                return nil
        }
    }
}

Note that, because this is failable, it still preserves .never semantics.

Now consider this call site:

let seconds = DispatchTimeInterval.seconds(1) // seconds(1)
seconds.microseconds // 1000000

Versus this one:

let seconds = DispatchTimeInterval.seconds(1) // seconds(1)
let microseconds = DispatchTimeInterval.microseconds(from: seconds) // microseconds(1000000)
Int(microseconds) // 1000000

I personally prefer the latter given the variable naming convention here, but I recognize that it could be a bit confusing given less descriptive variable names:

let interval = DispatchTimeInterval.microseconds(1000000) // microseconds(1000000)
Int(interval) // 1000000

Of course, we could always just use an if case let statement instead:

let interval = DispatchTimeInterval.seconds(1)

if case let .microseconds(μs) = DispatchTimeInterval.microseconds(from: interval) {
    μs // 1000000
}

This may be a bit verbose, but it's not too bad and I think it works regardless of how your variables are named.

Hope this helps! :)

1 Like

The use case is throttling dispatch queues, by allowing the user to pass in a DispatchTimeInterval. That's something I'm keen to keep as it lifts the burden from the user of doing the time maths. All this DispatchTimeInterval extension code is something I'd make use of inside my throttling code.

This is a clean way of creating a new instance that's converted from the original, but I want to provide a uniform way of converting with and without the side effect of the creation of a new instance.

Thanks for the detailed response. It's helped me understand DispatchTimeInterval more, as I've never actually used the type before and I think I've misunderstood why it exists. I think if they'd named .never as .forever (or .maximum) I would have perhaps grokked it more easily, because I was interpreting .never as .none and that's incorrect.

I've come to believe that DispatchTimeInterval is not the correct tool for the job for my particular use case, for a number of reasons. It looks like DispatchTimeInterval exists to facilitate a more declarative style of API than to actually represent a time interval that can be applied and manipulated in varying contexts. It reminds me very much of what the Swift Package Manager team are doing with their API (a declarative approach).

What am I trying to do? Well I was intending to use DispatchTimeInterval to describe an interval of time by which to throttle a queue. This can be useful for limiting the request rate made by clients to a backend service (my use case at least). In this context .never really does seem similar to .none, ie no throttling. Just for that reason alone it's enough to discard it as the correct solution. But as you say, Int's are going to lose precision and there is no need for that if they're declared as Double.

Most who replied seem quite keen on using static functions to provide the conversion of an instance from one time unit to another. I personally don't believe this sort of functionality should be static. I think it belongs as a method on the instance. This becomes obvious when you want to convert an instance to a new time unit but don't want to have the side effect of creating a new second instance. Apple do have guidelines on method names for these scenarios, so I will probably stick with them. I think that's the only way to provide a uniform experience for users who are used to Foundation functionality like sorting arrays where you have sort() and sorted() that perform the same functionality, just that one has a side effect of creating a new array and the other one doesn't.

I'm just a bit unsure whether to add a new constructor to Int or simply implement a intValue() method on the interval type, as I see it's so prevalent in the SDKs.

Thanks very much for your explanations and advice, it's helped me avoid a mistake by using DispatchTimeInterval for my needs. :smiley:

enums are already static (and stateless), so it's not possible to change the value of the instance.

Since you're working with Time I would recommend using TimeInterval. That way any extensions you add will be constrained to the Time use case and won't apply to Int/Float more generally.

Anyway your use case sounds interesting! Good luck! :slightly_smiling_face:

I realise enums are value types. I'm talking about reassigning to self in a mutating method which is completely doable...

enum TriStateSwitch {
    case off, low, high
    mutating func next() {
        switch self {
        case .off:
            self = .low
        case .low:
            self = .high
        case .high:
            self = .off
        }
    }
}
var ovenLight = TriStateSwitch.low
ovenLight.next()

I have enums that need more than one associated type. In most cases the enum is an Int but also needs to have a String or UIColor associated with each case. I've used names like toString() or toColor(), which end up being a switch statement on self. Also, router enums used with AlamoFire usually have path(), asURLRequest(), var method and a couple others.

So using the 'to or 'as' prefixes on the method names is common but it's probably better to use the simpler variable names when it makes sense.

The names with suffix 'Value' like stringValue, intValue come from NSNumber in Foundation, which is very old-timey.

1 Like