massivefermion / birl

datetime handling for gleam
https://hex.pm/packages/birl
Apache License 2.0
72 stars 9 forks source link

Include monotonic time in the datetime and time types #3

Closed lpil closed 1 year ago

lpil commented 1 year ago

Hello!

Currently if one want to use dates for order the programmer needs to be aware of the problems with wall time and that it can be backwards, etc, and know that in this situation they should instead use the instant type instead of or in addition to.

I think we could remove this sharp edge by copying Go's novel approach of combining both into a single type, resulting in one difficult to mis-use type that likely does what you want regardless of how deep your understanding is of time.

The go documentation has some good information on their system here: https://pkg.go.dev/time#Time

Thanks, Louis

massivefermion commented 1 year ago

To be honest I mostly got it from here where SystemTime and Instant are separate. But of course, I agree with you. I'll try to come up with something that makes sense and is also convenient to use.

massivefermion commented 1 year ago

I think there is a problem with what Golang is doing. it seems like the Time type in Golang contains both wall time and monotonic time and it uses the wall time if e.g. it needs to generate a date string and uses the monotonic time for time calculations. But what if I need to do time calculations on wall time? There is also the disadvantage that this makes things vague. Of course this vagueness means developers don't need to know much about the issue, but it also means you're limiting developers who do know enough about the issue. So to be honest I don't like Golang's approach. Of course there is the option of calculating the wall time from monotonic time(you just need to know the starting point of the monotonic time), but this has the disadvantage that maybe the developer needs something that agrees with the datetime in the OS. So although I agree with you that having both wall time and monotonic time in the same type is convenient, I think it's too limiting for people who know what they're doing.

lpil commented 1 year ago

Sorry, I'm not understanding what you mean.

it needs to generate a date string and uses the monotonic time for time calculations. But what if I need to do time calculations on wall time?

It uses both when adding or removing from dates, except for operations where it cannot work with monotonic time, in which case it strips them. Wall time is always used. Here's where it says this:

If Time t has a monotonic clock reading, t.Add adds the same duration to both the wall clock and monotonic clock readings to compute the result. Because t.AddDate(y, m, d), t.Round(d), and t.Truncate(d) are wall time computations, they always strip any monotonic clock reading from their results. Because t.In, t.Local, and t.UTC are used for their effect on the interpretation of the wall time, they also strip any monotonic clock reading from their results. The canonical way to strip a monotonic clock reading is to use t = t.Round(0).

https://pkg.go.dev/time

There is also the disadvantage that this makes things vague.

Could you expand upon this? I don't understand what you mean by vague here. To me it seems more precise as it always has more information than the approach there the type only has one or the other.

Of course there is the option of calculating the wall time from monotonic time

This isn't possible, by definition you can't translate from one into the other as they don't progress forward at the same rate, and the non-monotonic clock may not always progress forwards. That's the reason why there are multiple different clocks one can use.

massivefermion commented 1 year ago

Sorry, seems like I'm confused and I don't exactly understand how Golang is handling it. It seems like the Time type keeps both wall time and monotonic time. And when you call add and subtract on it, it adds and subtracts to and from both of those values. But then it uses wall time for "wall time computations" and monotonic time for "monotonic time computations". Is that how it works? If it is, then what is the definition of "wall time computation" and "monotonic time computation"? How do you decide which to use for a certain function?

There is also this:

If Times t and u both contain monotonic clock readings, the operations t.After(u), t.Before(u), t.Equal(u), and t.Sub(u) are > carried out using the monotonic clock readings alone, ignoring the wall clock readings. If either t or u contains no > monotonic clock reading, these operations fall back to using the wall clock readings.

But both t and u are of type Time. In what situations do they have monotonic readings and in what situations do they not?

And this:

later time-telling operations use the wall clock reading, but later time-measuring operations, specifically comparisons and subtractions, use the monotonic clock reading.

Why is that? Why comparisons and subtractions use monotonic clock reading? How is that decided? Why do I not get a choice in this matter? What if I need to do comparisons and subtractions using the wall time? I just can't? Why?

Honestly I'm really confused. The way I see it, people should use the DateTime type by default because most of the time that's what you need, because most of the time when you're handling date and time, you're trying to implement some business logic that involves actual clocks and calendars which translates to wall time.

But there are other use cases, like benchmarking, that need monotonic time, which as far as I know is the minority of the use cases. But then the developer knows what they're doing and knows that the wall time wouldn't work for their use case. So it really seems to me that it makes sense for wall time and monotonic time to be separate.

I guess that's what I mean when I say Golang's approach is vague. Because those concepts of time are totally different concepts used in totally different circumstances, so it doesn't seem right to use one type to represent both of them and if you do, then you're not actually talking in a language that the developer understands.

EDIT: I tried to read the source code to clear my confusions but I couldn't get access to it. I hit an error with and without my vpn. Smells like a sanction thing again 😒

lpil commented 1 year ago

If it is, then what is the definition of "wall time computation" and "monotonic time computation"? How do you decide which to use for a certain function?

Wall time computations are ones that involve wall time, monotonic time computation are the ones that involve monotonic time. We would always try to do both, but degrade to only wall time for those computations in which monotonic time is not available, such as when one of the times is from the outside world (like from a timestamp in a JSON string rather than from the program itself).

But both t and u are of type Time. In what situations do they have monotonic readings and in what situations do they not?

It would depend upon where the time has come from. If we generate it with time.now() then it would hold both wall and monotonic time, if it is from the outside world (user input, database, etc) it'll be a degraded time value with only wall time.

Why is that? Why comparisons and subtractions use monotonic clock reading? How is that decided? Why do I not get a choice in this matter? What if I need to do comparisons and subtractions using the wall time? I just can't? Why?

It is done that way because preserves the ordering properties that we as humans expect from time, so we don't have to remember to do it ourselves in the situations in which it matters.

If you wish to only work with wall time (this is normally not what you want) then you can do it by removing the monotonic value from the time value as the documentation explains.

Honestly I'm really confused. The way I see it, people should use the DateTime type by default because most of the time that's what you need, because most of the time when you're handling date and time, you're trying to implement some business logic that involves actual clocks and calendars which translates to wall time.

But there are other use cases, like benchmarking, that need monotonic time, which as far as I know is the minority of the use cases. But then the developer knows what they're doing and knows that the wall time wouldn't work for their use case. So it really seems to me that it makes sense for wall time and monotonic time to be separate.

Monotonic time is not about precision and it may not be more accurate for recording elasped time than wall time, it only guarentees that it advances in a single direction. It is not for niche tasks like benchmarking, it is primarily for ordering, which I believe to be the most commonly used time property in programming. Far more common than working with calendars.

I think this confusion is good evidence that time is much more complicated than commonly understood and that it has many pitfalls, and as such we should design the time type to have the properties that people expect time to have, and if they wish to have some niche behaviour (such as time going backwards) then that should be the opt in.

massivefermion commented 1 year ago

I made some changes and I think we can close this issue. And with #2 also probably closed, I'm ready for a new version so that we can move on to other issues. Let me know if I misunderstood something and the code still needs to change regarding this issue. Thanks for the feedback!