Project

General

Profile

Feature #14784

One-sided Comparable#clamp

Added by zverok (Victor Shepelev) 4 months ago. Updated 2 months ago.

Status:
Open
Priority:
Normal
Assignee:
-
Target version:
-
[ruby-core:87241]

Description

Proposal

Allow "one-sided" clamp to limit only upper bound (and, ideally, only lower too).

Proposed implementation: allow clamp(begin..end) call sequence (without deprecating clamp(begin, end)), to take advantage from open-ended ranges with clamp(begin..).

Reasoning about range

I looked through #clamp discussion, but couldn't find there why syntax clamp(b, e) was preferred to clamp(b..e). The only one I could think of is possible confuse of how clamp(b..e) and clamp(b...e) behaviors should differ.

The problem becomes more important with the introduction of open-ended ranges. I believe this is pretty natural:

some_calculation.clamp(0..)    # now, I use clamp(0, Float::INFINITY)
timestamp.clamp(Date.today..)  # now, I typically use clamp(Date.today..INFINITE_FUTURE_DATE) with custom defined constant

Counter-arguments:

  1. This is "one-sided", you can't do clamp(..Date.today). To this I can answer than from my experience "clamping only minimum" is more frequent, and if you need to clamp only maximum, most of the time there is some "reasonable minumum". Another idea is that maybe this is a proof why "start-less" ranges are necessary, after all, doubted here
  2. Why not just leave current clamp(b, e) and allow clamp(b)? Answer: because when you see clamp(10), is it clamp(10, nil), or clamp(nil, 10) (yes, logically it is the first argument that is left, but from readability point of view it is not that obvious). Possible alternative: clamp(min: 0, max: 10), where you can omit any of two.
  3. Why do you need one-sided clamp at all? Because alternatives is much more wordy, making reader think:
# with clamp
chain.of.calculations.clamp(0..)

# without clamp
v = chain.of.calculations
v < 0 ? 0 : v

# or, with yield_self (renamed to then)
chain.of.calculations.then { |v| v < 0 ? 0 : v }

Both alternatives "without #clamp" shows intentions much less clear.

History

#1 [ruby-core:87244] Updated by shevegen (Robert A. Heiler) 4 months ago

Considering that Ranges allow a ruby hacker to omit the end value,
for infinity/endless, since about ... a month or so, I think your
example makes sense in this regard, e. g.

begin .. end

being the same as:

begin ..

or
begin..

Perhaps also the converse, but I have to admit that
all these examples look very strange to my eyes. Like:

clamp(..Date.today)

I always look at it as if something is missing. Personally
I prefer explicit "begin .. end".

The:

clamp(min: 0, max: 10)

seems to be a nice API, in my opinion. At the least the names "min"
and "max" appear explicit and make sense (to me).

I agree, mostly for consistency, that if endless range has been
accepted, being able to do so via #clamp may seem a logical
continuation (to me). I am mostly neutral to the issue though,
as I do not (yet) use clamp in my own ruby code.

#2 [ruby-core:87248] Updated by nobu (Nobuyoshi Nakada) 4 months ago

zverok (Victor Shepelev) wrote:

  1. Why do you need one-sided clamp at all? Because alternatives is much more wordy, making reader think:

Why not [chain.of.calculations, 0].max?

#3 [ruby-core:87251] Updated by zverok (Victor Shepelev) 4 months ago

nobu (Nobuyoshi Nakada)

Why not [chain.of.calculations, 0].max?

Because this chain.of.calculations in reality could be something like

[paragraphs
  .group_by(&:speaker)
  .map { |speaker, paragraphs| paragraph.sort_by(&:length).first }
  .sort_by { |para| para.words.count }
  .first.speaker.name.length, 3].max

Jumping to start and end of this super-huge array while writing and reading is a pain.

So, now I'll probably write something like

paragraphs
  .group_by(&:speaker)
  .map { |speaker, paragraphs| paragraph.sort_by(&:length).first }
  .sort_by { |para| para.words.count }
  .first.speaker.name.length
  .yield_self { |len| [len, 3].max }

...which is OK-ish, but I never really liked how [value, MIN].max represents the idiom "limit this number to minimum possible value". The [value, MIN] somehow represents two values as equally important, while in fact one of them is "the main calculated value", and another one is "just one parameter of the calculation".

So, for me, this looks 200% better:

paragraphs
  .group_by(&:speaker)
  .map { |speaker, paragraphs| paragraph.sort_by(&:length).first }
  .sort_by { |para| para.words.count }
  .first.speaker.name.length
  .clamp(3..)

#4 [ruby-core:87976] Updated by akr (Akira Tanaka) 2 months ago

I feel this proposal is needlessly big: it needs range support for Comparable#clamp and startless range.
I think just supporting nil for 1st and 2nd argument of Comparable#clamp is enough.

Also available in: Atom PDF