> 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/8abc9a2a-fbe7-4a88-9526-75d14d5fee10n%40googlegroups.com.