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/10a41104-ea7d-4a68-b99a-b70727f0ad7an%40googlegroups.com.

Reply via email to