Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Use strong typing instead of ruby-like 1.seconds #4

Open
lilyball opened this issue May 11, 2015 · 8 comments
Open

Use strong typing instead of ruby-like 1.seconds #4

lilyball opened this issue May 11, 2015 · 8 comments

Comments

@lilyball
Copy link

Extending Int and Double with properties second, seconds, minute, etc. seems like a rather bad idea. There's two problems with this:

  1. 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.
  2. It's weakly-typed. You're still taking NSTimeInterval as your actual time type, and all it takes is for someone to accidentally leave off the .minutes and they'll get the wrong time. This isn't a huge issue, as NSTimeInterval is 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)) { ... }.

@DeFrenZ
Copy link

DeFrenZ commented May 12, 2015

I would make Duration a protocol and DurationInSeconds, DurationInMinutes, etc (no better names for now) as structs. Of course you would have operators between Duration that would add/compare them. In this way you can't possibly give minutes to a call asking for seconds

@lilyball
Copy link
Author

@DeFrenZ That's a lot of absurd complexity for a reduction in performance and flexibility. There is literally no reason to have distinct types.

In this way you can't possibly give minutes to a call asking for seconds

The Duration type already solves that problem. You cannot construct a Duration without explicitly specifying the units you're using to construct it.

@radex
Copy link
Owner

radex commented May 12, 2015

Thanks for the great feedback . Let me address your points:

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.

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 Duration type 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.

It's weakly-typed. You're still taking NSTimeInterval as your actual time type, and all it takes is for someone to accidentally leave off the .minutes and they'll get the wrong time. This isn't a huge issue, as NSTimeInterval is used everywhere to mean seconds and people are used to it, but we can still do better.

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.seconds seemed 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)

Extending Int and Double with properties second, seconds, minute, etc. seems like a rather bad idea.

I completely disagree with that. 5.minutes is a superior syntax to Duration(minutes: 5), IMHO. What the Double helpers should do, in an ideal world, is return a said Duration value, but again, out of scope.

@lilyball
Copy link
Author

But honestly, adding a global Duration type 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.

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 Duration type can disambiguate by saying SwiftyTimer.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.seconds is 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
Copy link
Owner

radex commented 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 Duration type can disambiguate by saying SwiftyTimer.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.

Good point.

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.

I honestly don't see your point. If Double#seconds et al returned some sort of a Duration value, 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
Copy link
Author

Care to elaborate why you think "2.seconds" is inferior to "Duration(seconds: 2)"?

Two reasons:

  1. 2.seconds is a silly attempt to be English-like without actually being English.
  2. The expression 2.seconds has nothing in it tying it to the Duration type. Why would I expect 2.seconds to necessarily return a Duration? 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 function static func seconds(seconds: NSTimeInterval) -> Duration you could even just say .seconds(2) whenever a Duration value is expected.

@radex
Copy link
Owner

radex commented May 14, 2015

2.seconds is a silly attempt to be English-like without actually being English.

Is it silly? I think it's nice.

The expression 2.seconds has nothing in it tying it to the Duration type. Why would I expect 2.seconds to necessarily return a Duration? It could just as easily return the radian value of 2/3600ths of a degree.

Oh, come on. i can't imagine many contexts where someone would think "seconds" means something else than seconds...

. And if you add a convenience function static func seconds(seconds: NSTimeInterval) -> Duration you could even just say .seconds(2) whenever a Duration value is expected.

Hmm, yeah, I could live with that. Seems like a totally reasonable compromise.

@DeFrenZ
Copy link

DeFrenZ commented 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 NSDate does (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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants