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.

Reply via email to