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

Reply via email to