Stream: API design

Topic: Datatype for representing epochs


view this post on Zulip Sam Mohr (Jan 02 2025 at 15:47):

Brought up with the #show and tell > roc-realworld initial exploration discussion:

What's the best backing type for storing time epochs? From the chrono docs for timestamp_nanos_opt:

An i64 with nanosecond precision can span a range of ~584 years. This function returns None on an out of range DateTime.

The dates that can be represented as nanoseconds are between 1677-09-21T00:12:43.145224192 and 2262-04-11T23:47:16.854775807.

So except for where you're trying to use a timestamp to represent dates in history, anything in the recent past/future is totally covered with i64. I personally think you could use u64 because now and forward is what we care about for app dev, but I can see usage of either.

That being said, if i64 covers almost 600 years, then using i128 would cover any year in the past, present, or future that would ever need to be represented for anything. You'd _probably_ be safe using it to represent all datetime types and crashing on invalid years like 1 trillion BC (though even that would still be in range!).

view this post on Zulip Sam Mohr (Jan 02 2025 at 15:51):

So I'd say:

view this post on Zulip Sam Mohr (Jan 02 2025 at 15:52):

I expect Roc should just use I128 everywhere to be safe?

view this post on Zulip Sam Mohr (Jan 02 2025 at 15:52):

@Luke Boswell does this answer your question?

view this post on Zulip Brendan Hansknecht (Jan 02 2025 at 15:52):

I think I64/I128 is useful so you can do a - b without worrying about accidentally producing a negative

view this post on Zulip Richard Feldman (Jan 02 2025 at 15:53):

yeah I jut wonder about whether "negative duration" is a good concept to have or not

view this post on Zulip Richard Feldman (Jan 02 2025 at 15:53):

I assume it's fine, but maybe it's desirable to know it's nonnegative for some reason?

view this post on Zulip Brendan Hansknecht (Jan 02 2025 at 15:54):

I feel like max efficiency isn't needed here so I128 seams fine. Then it can be used datetimes and time measurements instead of splitting the ecosystem

view this post on Zulip Sam Mohr (Jan 02 2025 at 15:54):

I think we're already comfortable with "negative duration" because we don't measure from now, we measure from 54 years ago

view this post on Zulip Brendan Hansknecht (Jan 02 2025 at 15:56):

yeah I jut wonder about whether "negative duration" is a good concept to have or not

That's totally fair.

If we want to use this type for datetimes, negative is needed anyway for before 1970.

If it is only for time measurements, I agree that unsigned makes more sense

view this post on Zulip Sam Mohr (Jan 02 2025 at 16:00):

I'm not sure I agree that you'd want unsigned unless you wanted to save space.

view this post on Zulip Sam Mohr (Jan 02 2025 at 16:00):

Signed adds more safety (but not perfect safety, of course!) around math on the underlying epoch

view this post on Zulip Sam Mohr (Jan 02 2025 at 16:02):

And I can't think of a situation where I'd want to limit the cardinality of my times to "1970 and later" at the type level instead of just having a proper constraint in the DB or something

view this post on Zulip Richard Feldman (Jan 02 2025 at 16:08):

well Instant and Duration can be different in that way

view this post on Zulip Richard Feldman (Jan 02 2025 at 16:09):

Instant should certainly be signed

view this post on Zulip Richard Feldman (Jan 02 2025 at 16:09):

Duration I can see arguments both ways for whether it should be signed

view this post on Zulip Richard Feldman (Jan 02 2025 at 16:09):

(semantically, not thinking about performance or anything like that)

view this post on Zulip Sam Mohr (Jan 02 2025 at 16:09):

Oh, good point! I was thinking of instant for both of those words

view this post on Zulip Sam Mohr (Jan 02 2025 at 16:09):

Durations probably should not be negative!

view this post on Zulip Sam Mohr (Jan 02 2025 at 16:10):

So yes, I128 for Instant and U64 or U128 (probably the latter) for Duration

view this post on Zulip Brendan Hansknecht (Jan 02 2025 at 16:37):

I guess eventually Instant will be a nominal type. Then instant.elapsed(other) will return a Duration which is a U128 (maybe also nominal for methods like .to_ms().

view this post on Zulip Brendan Hansknecht (Jan 02 2025 at 16:37):

In a world where Instant is just structural Duration will ultimately be the same type cause users will just do instant - other

view this post on Zulip Richard Feldman (Jan 02 2025 at 16:48):

oh I definitely think both of them should be nominal

view this post on Zulip Richard Feldman (Jan 02 2025 at 16:48):

in general for things where units matter, nominal is great for avoiding mixing up your units

view this post on Zulip Richard Feldman (Jan 02 2025 at 16:48):

like accidentally adding together two Instants, which is almost certainly never what you want :sweat_smile:

view this post on Zulip Brendan Hansknecht (Jan 02 2025 at 16:50):

Ah yes, Dec 3rd, 1994 + Jan 1st, 1970 == Dec 3rd, 1994

view this post on Zulip Jasper Woudenberg (Jan 02 2025 at 18:21):

If durations can't be negative, wouldduration_a - duration_b would always return a positive number?

If so, it creates a situation where the order of + and - operations with durations matters, as below.

a = Duration.from_ms 2
b = Duration.from_ms 8
c = Duration.from_ms 5

expect
    // Fails. LHS: 5, RHS: 11
    a + b - c == a - c + b

view this post on Zulip Sam Mohr (Jan 02 2025 at 18:24):

I'd expect it to do whatever 3 - 5 does which is presumably crash

view this post on Zulip Sam Mohr (Jan 02 2025 at 18:25):

math binops exist because doing left.minus(right) every time sucks, and left.minus(right)? every time isn't worth it either, so we just expect it to crash

view this post on Zulip Sam Mohr (Jan 02 2025 at 18:26):

But we have minus_checked(...) for those safety-aware cases

view this post on Zulip Richard Feldman (Jan 02 2025 at 19:05):

yeah this is true in general of unsigned integers, and it's a tradeoff that comes with the upside of guaranteeing that they're never negative

view this post on Zulip Isaac Van Doren (Jan 03 2025 at 01:42):

I think durations should definitely be signed. It would be a very bad user experience to compare two durations and get a crash. It's more acceptable when subtracting raw unsigned integers because you know what type you're working with and that crashing is part of the game. With a custom type, you would have no reason to expect that today.minus(tomorrow) would explode without reading the docs.

To me a negative duration seems very intuitive and useful. It's just representing the distance and direction between two points in time.

view this post on Zulip Richard Feldman (Jan 03 2025 at 01:42):

today and tomorrow would be Instants, though, not Durations :thinking:

view this post on Zulip Richard Feldman (Jan 03 2025 at 01:43):

Isaac Van Doren said:

It would be a very bad user experience to compare two durations and get a crash. It's more acceptable when subtracting raw unsigned integers because you know what type you're working with and that crashing is part of the game.

this is a good point

view this post on Zulip Isaac Van Doren (Jan 03 2025 at 01:43):

Right, I was imagining that today.minus(tomorrow) would be a duration though

view this post on Zulip Richard Feldman (Jan 03 2025 at 01:43):

the downside of it being signed is that you can't guarantee it's nonnegative, so there might be some scenarios you have more difficulty ruling out

view this post on Zulip Richard Feldman (Jan 03 2025 at 01:43):

but the downside of it being unsigned is that it might crash if you use it wrong

view this post on Zulip Richard Feldman (Jan 03 2025 at 01:43):

which is a more serious downside

view this post on Zulip Sam Mohr (Jan 03 2025 at 01:44):

I think it'd be better to expose a Instant.absolute_delta(Instant) -> Duration and maintain unsigned Durations

view this post on Zulip Isaac Van Doren (Jan 03 2025 at 01:44):

the downside of it being signed is that you can't guarantee it's nonnegative, so there might be some scenarios you have more difficulty ruling out

It seems fine to just have to check if the duration is positive if you need to

view this post on Zulip Isaac Van Doren (Jan 03 2025 at 01:45):

I think it'd be better to expose a Instant.absolute_delta(Instant) -> Duration and maintain unsigned Durations

The trouble with this is that it will make adjusting dates really annoying if you actually do want a negative quantity

view this post on Zulip Sam Mohr (Jan 03 2025 at 01:47):

Why do you need a negative quantity? You can just do Instant.move_forwards(Duration) -> Instant and Instant.move_backwards(Duration) -> Instant

view this post on Zulip Sam Mohr (Jan 03 2025 at 01:47):

I'm approaching this from a "cardinality first, API shape second" and negative durations don't fit into that first box for me

view this post on Zulip Sam Mohr (Jan 03 2025 at 01:48):

Elm is big on descriptive APIs, it seems totally prudent to have a descriptive API for this stuff

view this post on Zulip Isaac Van Doren (Jan 03 2025 at 01:48):

Right I could do that, but that is a lot clunkier than myInstant.adjust(myDuration) which handles either direction

view this post on Zulip Sam Mohr (Jan 03 2025 at 01:49):

Sure

view this post on Zulip Sam Mohr (Jan 03 2025 at 01:50):

I don't have strong feelings on it. Not the way I'd do it, but that approach works, so it's okay with me

view this post on Zulip Brendan Hansknecht (Jan 03 2025 at 01:52):

Warning, it is very important to know when durations are negative. It is a sign that the underlying clock changed. So you need to know to throw out that data.

view this post on Zulip Isaac Van Doren (Jan 03 2025 at 01:53):

Yeah that's a good point

view this post on Zulip Brendan Hansknecht (Jan 03 2025 at 01:53):

Cause there is no guarantee to that end comes after start when doing:

start = Instant.now!()
end = Instant.now!()

view this post on Zulip Richard Feldman (Jan 03 2025 at 02:58):

oh that's a great point

view this post on Zulip Richard Feldman (Jan 03 2025 at 02:59):

that's enough to convince me duration should be signed :+1:

view this post on Zulip Sky Rose (Jan 03 2025 at 03:14):

I have a concrete use case for negative durations:
In my work, I regularly deal with comparing multiple times, and seeing how close they are. (Specifically, "Did the train come on time?", comparing the scheduled time to the predicted arrival time to the time the train actually showed up.)

The train could either be early or late. This would be way easier to do with signed subtraction and negative durations, rather than having unsigned durations and having to maintain a separate "early or late" enum.

view this post on Zulip Isaac Van Doren (Jan 03 2025 at 03:27):

That's a great example

view this post on Zulip Isaac Van Doren (Jan 03 2025 at 03:29):

That reminds me that stage monitors (displays that only the people on stage can see) will often display the amount of time remaining for a particular segment and then switch negative once a segment starts going too long.

view this post on Zulip Richard Feldman (Jan 03 2025 at 03:45):

cool, ok it seems like Rust got this one wrong

view this post on Zulip Richard Feldman (Jan 03 2025 at 03:45):

so then the question is I64 or I128

view this post on Zulip Sam Mohr (Jan 03 2025 at 03:46):

If your Instant is I128, seems like the same for Duration only makes sense

view this post on Zulip Sam Mohr (Jan 03 2025 at 03:47):

If you don't know what to use for instant, then I128 is 99.9999999% safe for any usage and I64 is 99.9% safe

view this post on Zulip Richard Feldman (Jan 03 2025 at 03:49):

yeah there's always a performance argument for I64, which I think would mainly come up if you're storing a bunch of these in memory, like a time series or something

view this post on Zulip Richard Feldman (Jan 03 2025 at 03:49):

but maybe for use cases like that you're storing a plain integer anyway and just converting to Instant or Duration one at a time, briefly

view this post on Zulip Sam Mohr (Jan 03 2025 at 03:51):

A Instant.to_i64() : Instant -> Result I64 [Overflow] method should be sufficient for those that want small storage, and Instant.from_i64() : I64 -> Instant is infallible

view this post on Zulip Sam Mohr (Jan 03 2025 at 03:51):

We should default to safe unless there's a big cost

view this post on Zulip Ayaz Hafiz (Jan 03 2025 at 04:35):

for high performance time series (trading use case) my experience is you store offsets relative to some known time, and your TS is usually fixed an intervals, so you often elide explicit intervals anyway. I would think I128 is fine in general

view this post on Zulip Pit Capitain (Jan 05 2025 at 08:13):

Re negative Durations: maybe the name "Duration" isn't quite right? I think what we're talking about is an "InstantDifference"? For me, a "Duration" is never negative. I can't think of a better name for this concept, but the folks here are very good in proposing alternatives...

view this post on Zulip Anton (Jan 06 2025 at 10:33):

Interesting


Last updated: Jul 06 2025 at 12:14 UTC