Ran into this again today. On further inspection, rounding down is not a problem in the current implementation of the duration handling functions.
I've submitted a PR to support floats for keywords only to push this discussion further: https://github.com/elixir-lang/elixir/pull/15400 On Sunday, June 22, 2025 at 6:49:54 PM UTC-5 José Valim wrote: > Regardless of how the discussion goes, I agree we should consistently > round up. A pull request is welcome! > > > *José Valimhttps://dashbit.co/ <https://dashbit.co/>* > > > On Sun, Jun 22, 2025 at 12:38 PM Christopher Keele <[email protected]> > wrote: > >> > I think it's fine for to_timeout to support a *domain* broader than >> durations. Otherwise it should be Duration.to_timeout rather than >> Kernel.to_timeout. >> >> Annendum: that is, since to_timeout already supports a *range* broader >> than durations alone, it is not surprising that it is capable of producing >> values outside the domain of durations. I do not view it as inconsistent. >> >> On Sunday, June 22, 2025 at 2:32:53 PM UTC-5 Christopher Keele wrote: >> >>> > The existing behavior (which presumably hasn't impacted anyone) is >>> actually to *truncate* microseconds down to milliseconds >>> >>> TIL! I understand how we got here (with the underlying impl >>> <https://www.erlang.org/doc/apps/erts/erlang.html#convert_time_unit/3> >>> using >>> an algo that effectively floors >>> <https://github.com/erlang/otp/blob/d9454dbccbaaad4b8796095c8e653b71b066dfaf/erts/preloaded/src/erlang.erl#L5243-L5244>, >>> >>> I'm guessing to avoid overhead of doing a div/round themselves), but I >>> think that's surprising behaviour *in the context of timeouts* (as >>> opposed to the general purpose of the erlang function, arbitrary time unit >>> conversion). As José states in the linked issue: >>> >>> > We could support this, but it would be important to truncate up (i.e. >>> ceiling), as timeouts guarantee a minimum time until it is triggered. >>> >>> I agree that microsecond resolution won't likely matter to most >>> applications, but it is telling that by trying to avoid floats today, we >>> are already doing the "wrong" rounding for timeout purposes at the lowest >>> resolution. >>> ------------------------------ >>> >>> More thoughts against the argument against: >>> >>> > We already support multiple units and you can easily convert from one >>> to the other, so I'd rather write to_timeout(minute: 30) than rely on the >>> impreciseness of floats >>> I wrote up my own use-cases where floats would be useful, but it read >>> more or less word-for-word identical to Tyler's extended example, including >>> the consequent: I don't normally bother with to_timeout for non-literal >>> inputs for this reason. I will augment his argument further: >>> >>> > In real life, of course this [rate limit] value comes from an >>> application config variable >>> >>> In practice, a lot of my timeouts are more dynamic still: they come from >>> API rate limit headers or similar runtime backpressure metadata from >>> external systems. Even when they are provided as integers, I may have to >>> produce floats myself (ex by using division to convert an integer limit or >>> rate into a time span, or multiplying integer limits against an internally >>> tracked float timedelta) in the computing of the correct timeout to use. >>> >>> > to_timeout is meant to take durations and durations do not accept >>> float, so that would make it inconsistent >>> >>> I think it's fine for to_timeout to support a domain broader than >>> durations. Otherwise it should be Duration.to_timeout rather than >>> Kernel.to_timeout. >>> >>> > It feels that, once we add this feature, we would need to add huge >>> disclaimers to the function saying "beware of floats" and explain the >>> rounding up behaviour, which makes me wonder what is the benefit of >>> supporting it in the first place. >>> >>> It feels like we ought to add a disclaimer about the current >>> implementation today, broadcasting that durations and even integer inputs >>> will *round down* at microsecond resolution. If we're adding a float >>> rounding disclaimer already, it feels like we should implement the desired >>> *rounding >>> up* behaviour, at which point there is little reason to not support >>> floats anyways. >>> On Saturday, June 21, 2025 at 7:06:09 AM UTC-5 [email protected] wrote: >>> >>>> I'm a big fan of Elixir 1.17's new to_timeout/1 function. However, I >>>> find it unnecessarily restrictive for it to only accept integer values in >>>> its keyword lists. Consider a simple case like: >>>> >>>> to_timeout(hour: 0.5) >>>> >>>> A lot of the value of the function seems to be in letting devs say >>>> "here's what I've got, *you* tell me how to turn it into a timeout()". >>>> >>>> *An extended example* >>>> >>>> Another example where the user experience provided by supporting floats >>>> is much better: >>>> >>>> You have a rate limit of 1350 requests per hour for some third party >>>> API. (In real life, of course this value comes from an application config >>>> variable so that non-developers on the team can change it in prod without >>>> needing a code change.) You would like to do a Process.sleep/1 after >>>> each request to ensure you stay under the rate limit. With float support, >>>> you can do to_timeout(hour: 1 / 1350). Without float support, you >>>> can't just convert to 0.044 minutes or 2.67 seconds... you'll need to go >>>> all the way to milliseconds. And don't forget that final integer >>>> conversion! >>>> >>>> to_timeout(millisecond: ceil(1 / 1350 * 60 * 60)) >>>> >>>> At this point, why even bother with the to_timeout function? >>>> >>>> *Objections* >>>> >>>> Originally <https://github.com/elixir-lang/elixir/issues/14579>, I had >>>> anticipated an objection based on a possible correctness issue—for >>>> instance, do you represent 2/3 seconds as 666 or 667 milliseconds? >>>> However, >>>> these concerns strike me as overblown, since the very nature of a timeout >>>> implies some system, somewhere, is going to call you back after a wait. >>>> Unless you're on a real-time operating system, even if you ask for exactly >>>> 666 milliseconds, you might get your callback in 667 or even 1,667 >>>> milliseconds if the system is under heavy load. On the other hand, if >>>> you're building something like a pacemaker and every microsecond truly >>>> counts, you wouldn't be using a timeout() value (limited to >>>> millisecond precision) in the first place. >>>> >>>> I stand by the idea that since timeouts guarantee a minimum, not a >>>> maximum time you'll wait, no one who asks for (say) 2/3 of a second will >>>> be >>>> shocked when they wait a minimum of 667 milliseconds. It's hard for me to >>>> imagine anyone ever noticing. >>>> >>>> Following that, José pointed out the following objections: >>>> >>>> 1. to_timeout is meant to take durations and durations do not >>>> accept float, so that would make it inconsistent >>>> 2. It feels that, once we add this feature, we would need to add >>>> huge disclaimers to the function saying "beware of floats" and explain >>>> the >>>> rounding up behaviour, which makes me wonder what is the benefit of >>>> supporting it in the first place. We already support multiple units and >>>> you >>>> can easily convert from one to the other, so I'd rather write >>>> to_timeout(minute: 30) than rely on the impreciseness of floats >>>> >>>> >>>> To #1 I'd say that while I love that the Duration *struct* always >>>> represents its time period consistently, I see no reason whatsoever that >>>> Duration.new/1 shouldn't also support floats, especially given that >>>> Duration already supports microsecond precision. >>>> >>>> To #2, I don't believe we need huge disclaimers, unless such >>>> disclaimers are already necessary when using a Duration with to_timeout. >>>> The existing behavior (which presumably hasn't impacted anyone) is >>>> actually >>>> to *truncate* microseconds down to milliseconds: >>>> >>>> iex> to_timeout(%Duration{microsecond: {999, 3}}) >>>> 0 >>>> >>>> iex> to_timeout(%Duration{microsecond: {1999, 3}}) >>>> 1 >>>> >>>> However we implement float handling here, I'd argue it should be >>>> consistent with Duration's handling of microseconds. (I'm don't have >>>> strong >>>> feelings as to whether that means to_timeout needs to start doing the >>>> ceiling of the number of milliseconds when given a Duration, or whether >>>> floats should be truncated to milliseconds. Again, I don't think anyone's >>>> going to notice either way.) >>>> >>>> *Prior art* >>>> >>>> JavaScript of course does not differentiate between floating point and >>>> integer values, so when the setTimeout function accepts a millisecond >>>> wait time, you can pass a non-whole value without issue. However, MDN >>>> indicates that browsers store the timeout as 32-bit integer >>>> milliseconds >>>> <https://developer.mozilla.org/en-US/docs/Web/API/Window/setTimeout#:~:text=Browsers%20store%20the%20delay%20as%20a%2032-bit%20signed%20integer%20internally> >>>> >>>> internally, so sub-millisecond precision is not respected. However, I >>>> can't >>>> find any documentation about whether it truncates, rounds, or takes the >>>> ceiling of floating point millisecond values (neither on the MDN site, in >>>> the spec for the web API >>>> <https://html.spec.whatwg.org/multipage/timers-and-user-prompts.html#dom-settimeout>, >>>> >>>> nor in the Node.JS docs). >>>> >>>> Python's time.sleep() >>>> <https://docs.python.org/3/library/time.html#time.sleep> accepts >>>> floating point seconds, and the precision of the sleep depends on the >>>> operating system, ranging from nanoseconds to microseconds. >>>> >>>> Ruby's sleep() >>>> <https://docs.ruby-lang.org/en/master/Kernel.html#method-i-sleep> also >>>> accepts floating point seconds, with an unspecified precision. >>>> >>>> Go's time.sleep() <https://pkg.go.dev/time#Sleep> accepts integer >>>> nanoseconds, but various recommendations I'm seeing around the web suggest >>>> getting those nanosecond values by multiplying floating point second >>>> values >>>> the time.Second constant (the number of nanoseconds in a second), which >>>> would result in automatic truncation to nanoseconds. >>>> >>>> PHP's usleep() <https://www.php.net/manual/en/function.usleep.php> >>>> officially >>>> accepts integer milliseconds, but if you pass a floating point value, it >>>> will automatically truncate it. >>>> >>>> Java's Thread.sleep() >>>> <https://docs.oracle.com/javase/8/docs/api/java/lang/Thread.html#sleep-long-> >>>> >>>> accepts integer milliseconds, and potentially also integer nanoseconds. >>>> >>> -- >> You received this message because you are subscribed to the Google Groups >> "elixir-lang-core" group. >> To unsubscribe from this group and stop receiving emails from it, send an >> email to [email protected]. >> To view this discussion visit >> https://groups.google.com/d/msgid/elixir-lang-core/23acd21c-7aba-4c69-a0c4-cf4e92de9335n%40googlegroups.com >> >> <https://groups.google.com/d/msgid/elixir-lang-core/23acd21c-7aba-4c69-a0c4-cf4e92de9335n%40googlegroups.com?utm_medium=email&utm_source=footer> >> . >> > -- You received this message because you are subscribed to the Google Groups "elixir-lang-core" group. To unsubscribe from this group and stop receiving emails from it, send an email to [email protected]. To view this discussion visit https://groups.google.com/d/msgid/elixir-lang-core/5bb3662a-b8c4-4431-a12f-a71a1684d04en%40googlegroups.com.
