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 <christheke...@gmail.com> 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 s3c...@gmail.com 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 elixir-lang-core+unsubscr...@googlegroups.com. > 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 elixir-lang-core+unsubscr...@googlegroups.com. To view this discussion visit https://groups.google.com/d/msgid/elixir-lang-core/CAGnRm4K13bRvRG7bJtO4M%2BHNoDR-Babs1H-QKLyWg2qGLQMRCw%40mail.gmail.com.