This repository was archived by the owner on May 1, 2026. It is now read-only.
Repository navigation
This repository was archived by the owner on May 1, 2026. It is now read-only.
Use strong typing instead of ruby-like 1.seconds #4
Copy link
Copy link
Open
Description
Extending Int and Double with properties second, seconds, minute, etc. seems like a rather bad idea. There's two problems with this:
- Anyone using your code in a project with other 3rd-party code that wants to do something similar will have a compile-time naming collision, making it impossible to use either one.
- It's weakly-typed. You're still taking
NSTimeIntervalas your actual time type, and all it takes is for someone to accidentally leave off the.minutesand they'll get the wrong time. This isn't a huge issue, asNSTimeIntervalis used everywhere to mean seconds and people are used to it, but we can still do better.
The better approach is to use an actual Duration type that requires the user to type the unit as part of the constructor. With the ruby-like approach you can just say NSTimer.after(1) { ... } but with a proper strong type there's no way to do this. I'd suggest something like
/// A type that represents a given duration.
public struct Duration: Comparable, Hashable, Printable, DebugPrintable {
/// The time interval of the `Duration`, in seconds.
let seconds: NSTimeInterval
/// The time interval of the `Duration`, in minutes.
var minutes: NSTimeInterval {
return seconds / 60
}
/// The time interval of the `Duration`, in hours.
var hours: NSTimeInterval {
return seconds / 3600
}
/// The time interval of the `Duration`, in milliseconds.
/// Sub-millisecond values are truncated.
var milliseconds: Int64 {
return Int64(seconds * 1_000)
}
/// The time interval of the `Duration`, in microseconds.
/// Sub-microsecond values are truncated.
var microseconds: Int64 {
return Int64(seconds * 1_000_000)
}
/// The time interval of the `Duration`, in nanoseconds.
var nanoseconds: Int64 {
return Int64(seconds * 1_000_000_000)
}
/// Construct a `Duration` for a given number of seconds.
public init(seconds: NSTimeInterval) {
self.seconds = seconds
}
/// Construct a `Duration` for a given number of minutes.
public init(minutes: NSTimeInterval) {
self.init(seconds: minutes * 60)
}
/// Construct a `Duration` for a given number of hours.
public init(hours: NSTimeInterval) {
self.init(seconds: hours * 3600)
}
/// Construct a `Duration` for a given number of milliseconds.
// Use Int64 because milliseconds are generally not floating-point
// values
public init(milliseconds: Int64) {
self.init(seconds: NSTimeInterval(milliseconds) / 1_000)
}
/// Construct a `Duration` for a given number of microseconds.
public init(microseconds: Int64) {
self.init(seconds: NSTimeInterval(microseconds) / 1_000_000)
}
/// Constructs a `Duration` for a given number of nanoseconds.
// How much tolerance does a timer actually support?
public init(nanoseconds: Int64) {
self.init(seconds: NSTimeInterval(nanoseconds) / 1_000_000_000)
}
public var description: String {
// TODO: Display human-readable string with multiple units
return toString(seconds)
}
public var debugDescription: String {
return "Duration(\(seconds))"
}
public var hashValue: Int {
return seconds.hashValue
}
}
public func +(lhs: Duration, rhs: Duration) -> Duration {
return Duration(seconds: lhs.seconds + rhs.seconds)
}
public func -(lhs: Duration, rhs: Duration) -> Duration {
return Duration(seconds: lhs.seconds - rhs.seconds)
}
// NB: Don't implement multiplication/division, that doesn't make any sense for
// durations. As such, we don't conform to IntegerArithmeticType either.
public func <(lhs: Duration, rhs: Duration) -> Bool {
return lhs.seconds < rhs.seconds
}
public func ==(lhs: Duration, rhs: Duration) -> Bool {
return lhs.seconds == rhs.seconds
}This way you can then say NSTimer.after(Duration(seconds: 1)) { ... }. You could also experiment with replacing all those initializers with static functions instead (e.g. static func seconds(seconds: NSTimeInterval)) so that way you can say NSTimer.after(.seconds(1)) { ... }.
Reactions are currently unavailable
Metadata
Metadata
Assignees
Labels
No labels
Activity
DeFrenZ commentedon May 12, 2015
I would make
Durationa protocol andDurationInSeconds,DurationInMinutes, etc (no better names for now) asstructs. Of course you would have operators betweenDurationthat would add/compare them. In this way you can't possibly give minutes to a call asking for secondslilyball commentedon May 12, 2015
@DeFrenZ That's a lot of absurd complexity for a reduction in performance and flexibility. There is literally no reason to have distinct types.
The
Durationtype already solves that problem. You cannot construct aDurationwithout explicitly specifying the units you're using to construct it.radex commentedon May 12, 2015
Thanks for the great feedback . Let me address your points:
I agree. Unfortunately a lot of Swift libraries right now have this problem, as people want to extend the standard library with things like Result, and all sorts of FP stuff…
But honestly, adding a global
Durationtype has the exact same problems. And for the purpose of this project, I'm fine with it. After all, I'm trying to show how I want the official standard library to be.This is mostly a made up problem. I like what static typing help us do, but let's not get crazy. No one's going to write
NSTimer.after(5)and think "5" means "5 minutes".I agree though that having a dedicated type for time intervals would be superior to representing them using
Double(and a silly type alias). This way, we could add a ton of functionality on dealing with time intervals right into the type.The reason why I didn't do that here is that it feels out of scope of this project. I just wanted to make a better API for NSTimer, and helpers like
2.secondsseemed like a quick win. Not sure I want to add new (not just extend) standard library-like types. (Though it would certainly make for an interesting project)I completely disagree with that.
5.minutesis a superior syntax toDuration(minutes: 5), IMHO. What theDoublehelpers should do, in an ideal world, is return a said Duration value, but again, out of scope.lilyball commentedon May 14, 2015
But it's not global. At least, not if your code is built as its own module. Anyone who wants to use two different modules that both use a
Durationtype can disambiguate by sayingSwiftyTimer.Duration. But you can't do that with extensions to a common type; the only way to disambiguate there is to use a separate swift file that only imports one of the two modules.As for "how I want the official standard library to be", I very strongly disagree that
2.secondsis something the official standard library should support. That is very much a Ruby-ism and only really makes sense in a dynamically-typed language (because you already have a fairly weak type system, so you're not really giving up anything by doing that). When you have a strong type system, using a strongly-typed approach is much better.radex commentedon May 14, 2015
Good point.
I honestly don't see your point. If Double#seconds et al returned some sort of a
Durationvalue, that would be strongly typed in my mind. You might not refer to Duration explicitly, but you would be checking for the right type.Care to elaborate why you think "2.seconds" is inferior to "Duration(seconds: 2)"?
lilyball commentedon May 14, 2015
Two reasons:
2.secondsis a silly attempt to be English-like without actually being English.2.secondshas nothing in it tying it to theDurationtype. Why would I expect2.secondsto necessarily return aDuration? It could just as easily return the radian value of 2/3600ths of a degree. Or perhaps there's some other reasonable interpretation of the name "seconds" as well.Duration(seconds: 2)is nice because it's explicit about the type. And if you add a convenience functionstatic func seconds(seconds: NSTimeInterval) -> Durationyou could even just say.seconds(2)whenever aDurationvalue is expected.radex commentedon May 14, 2015
Is it silly? I think it's nice.
Oh, come on. i can't imagine many contexts where someone would think "seconds" means something else than seconds...
Hmm, yeah, I could live with that. Seems like a totally reasonable compromise.
DeFrenZ commentedon May 14, 2015
@kballard thinking a bit about it I see your point. It is indeed the same data, just referenced in different manners. Just like
NSDatedoes (which is only a timestamp with a lot of helper methods/functions).I also agree that, even if it maybe looks a bit nicer and slightly easier to use, we should be extremely conservative with extending basic data types (at least when doing public libraries), even just for the sake of cluttering a common namespace. The free function does solve this problem and is still easy to use.