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.

Reply via email to