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