Stream: ideas

Topic: Name and Type signature for Num.clamp


view this post on Zulip Fábio Beirão (Jun 09 2023 at 12:39):

Hello everyone :wave:.

As per this discussion we noticed that roc's standard library (builtins) doesn't have a clamp equivalent. Documentation for clamp in Elm here.

What does clamp do? Clamp allows you to "constrain" an arbitrary number between a lower and an upper limit. 10 clamped between 20 and 100 is 20. 50 clamped between 20 and 100 is 50. 130 clamped between 20 and 100 is 100.

Naming

Is clamp a clear enough name?

Could we think of alternatives. for instance between , constrain, inRange ?


API Design

In terms of API design I can think of at least two approaches:

Approach 1 (equal to Elm)

API signature:

clamp : Num a, Num a, Num a -> Num a
clamp = \value, lower, higher -> ...

Usage: myValue |> clamp 0 100 -or- clamp myValue 0 100

Approach 2 (inspired by roc's List.sublist)

API signature:

clamp: Num a, { min: Num a, max: Num a } -> Num a
clamp = \value, { min, max } -> ...

Usage: myValue |> clamp { min: 0, max: 100 } -or- clamp myValue { min: 0, max: 100 }

For both these proposals I am a bit worried of name clashing with the new Num.min Num.max functions. That is why I would be okay to use argument names such as lower, higher.

Let me know what you think of these proposals and which one would be more "rociomatic" :rock_on:
Thank you!

view this post on Zulip Anton (Jun 09 2023 at 13:02):

Is clamp a clear enough name?

clamp may indeed not be familiar to non-native English speakers, some more suggestions limitTo, limitRange, limitToRange.

view this post on Zulip Anton (Jun 09 2023 at 13:02):

I prefer appoach 2. Name clashing may indeed be annoying with min and max. I think I prefer low and high as potential alternatives.

view this post on Zulip Fábio Beirão (Jun 09 2023 at 13:08):

:thinking: I wonder if we should worry about non-sensical inputs, such as 15 |> clamp { low: 20, high: 10 } <-- this would .. :thinking: return what :sweat_smile::sweat_smile:

view this post on Zulip Fábio Beirão (Jun 09 2023 at 13:09):

Which makes me think that with approach 1, clamp could be "self-healing"
In the sense that both these would work

15 |> clamp 10 20 == 15
15 |> clamp 20 10 == 15

view this post on Zulip Anton (Jun 09 2023 at 13:27):

Hmm, it could be reasonable to return a Result, because in the case that you are executing something like { low: 20, high: 10 } something is likely going wrong in your code and you want to be aware of it

view this post on Zulip Fábio Beirão (Jun 09 2023 at 13:41):

The alternative to not having the Result would be doing what Elm does where, if I read correctly, if you provide such non-sensical input, the output is the same as input, triggering the user to go check what is he doing wrong.

view this post on Zulip Fábio Beirão (Jun 09 2023 at 13:45):

On one hand I love the Result construction, but on the other hand they simply aren't free, both in terms of performance, as well as code flow.
We can see examples of this Result avoidance pattern for instance in List.set where it states: "If the given index is outside the bounds of the list, returns the original list unmodified."
We could have decided to use a Result [OutOfBounds] there, but this behavior is also fine (a user should always unit test). I think we only leverage Result when we can't possibly provide a sensible return value. For "clamp" return the user's original input is sensible enough, I think

view this post on Zulip Anton (Jun 09 2023 at 13:52):

Yeah, the performace argument is good. I wonder if we should add clamp and clampChecked. It seems that this function could be commonly used in data analysis, and low cost assistance in ensuring your analysis is correct seems valuable.

view this post on Zulip Kilian Vounckx (Jun 09 2023 at 13:55):

Fábio Beirão said:

The alternative to not having the Result would be doing what Elm does where, if I read correctly, if you provide such non-sensical input, the output is the same as input, triggering the user to go check what is he doing wrong.

Elm will not give back the original value. It will first check lower bound, and then upper bound. E.g. clamp 50 { low: 200, high: 100 } will return 200. clamp 250 { low: 200, high 100 } will return 100.

view this post on Zulip Kilian Vounckx (Jun 09 2023 at 13:57):

The same happens in rust in release mode. In debug mode there it will panic

view this post on Zulip Kilian Vounckx (Jun 09 2023 at 13:57):

In zig it is safety checked undefined behavior (using assert)

view this post on Zulip Kilian Vounckx (Jun 09 2023 at 13:58):

Haskell does the same as elm I think

view this post on Zulip Kilian Vounckx (Jun 09 2023 at 13:58):

I don't know what would be best, just throwing the info out here

view this post on Zulip Brendan Hansknecht (Jun 09 2023 at 14:11):

We also have the option to just limit to the range ignoring order. As in clampBetween x (10, 20) == clampBetween x (20,10). Not sure if that is more reasonable though.

view this post on Zulip Fábio Beirão (Jun 09 2023 at 14:21):

I think the underlying issue is that I am thinking of clamp as "restrict this number x between these two other numbers"
But when we invert the arguments to clamp, then the behavior becomes "prevent this number x from ever being between these two numbers"
Therefore maybe what emerges is indeed two functions instead of one

"restrictToInterval" (and here indeed (10, 20) is the same as (20, 10))
"restrictFromInternal" (and once again, (10, 20) versus (20, 10) would have the same outcome/desired behavior)

view this post on Zulip Fábio Beirão (Jun 09 2023 at 14:22):

If we go with the two functions with clear names and expectations, then I would be okay with the simpler Num a, Num a, Num a -> Num a signature for both of them

view this post on Zulip Brendan Hansknecht (Jun 09 2023 at 14:36):

Aside, we may want follow list.range and make it clear if each side is inclusive or exclusive.

view this post on Zulip Ajai Nelson (Jun 09 2023 at 14:39):

If we use a record, we could make the min and max optional.

For example, if you want to restrict num so that it's above some lowerBound, you could do this:

num |> Num.clamp { min: lowerBound }

I'm not sure if others feel this way, but I like that because using Num.max often feels wrong to me at first:

num |> Num.max lowerBound
# It always twists my brain for a second that a lower bound needs Num.max, not Num.min

The only problem is that it seems like right now, you can't define a function where optional fields refer to other arguments, but I'm not sure whether that was intentional decision:

# doesn't work
clamp = \x, { min ? x, max ? x } ->
    # ...

view this post on Zulip Anton (Jun 09 2023 at 14:43):

Aside, we may want follow list.range and make it clear if each side is inclusive or exclusive.

I may be missing something but how does a clamp with exclusive boundaries work?

view this post on Zulip Anton (Jun 09 2023 at 14:45):

"restrictToInterval" (and here indeed (10, 20) is the same as (20, 10))
"restrictFromInternal" (and once again, (10, 20) versus (20, 10) would have the same outcome/desired behavior)

restrictToInterval seems reasonable. I'm having trouble thinking of a usecase for restrictFromInternalthough.

view this post on Zulip Anton (Jun 09 2023 at 14:47):

num |> Num.clamp { min: lowerBound }

I do like the way that looks :)

view this post on Zulip Fábio Beirão (Jun 09 2023 at 14:47):

I don't know why, collision detection/prevention comes to mind :sweat_smile: as in restrict this entity from ever occupying the same coordinates as this other entity, but I might be stretching it a bit

view this post on Zulip Anton (Jun 09 2023 at 14:48):

Uhu, right now it does not seem like such a common operation that it justifies being a builtin

view this post on Zulip Fábio Beirão (Jun 09 2023 at 14:48):

I can also imagine a drag-drop scenario where you could use excludeFromInterval to prevent the user from entering a critical region of the UI

view this post on Zulip Anton (Jun 09 2023 at 14:53):

The only problem is that it seems like right now, you can't define a function where optional fields refer to other arguments, but I'm not sure whether that was intentional decision:

It's not my area of expertise but supporting that could be difficult.

view this post on Zulip Richard Feldman (Jun 09 2023 at 14:55):

I haven't tried it, but this might work?

clamp = \x, config ->
    { min ? x, max ? x } = config
    # ...

view this post on Zulip Anton (Jun 09 2023 at 15:06):

Yeah that works!

view this post on Zulip Richard Feldman (Jun 09 2023 at 15:09):

nice! Can you open an issue for that? I think it should be possible to make both work

view this post on Zulip Anton (Jun 09 2023 at 15:30):

restrictToInterval would come with a performance cost over clamp. The min and max is already set with clamp. With restrictToInterval you need to check which is which.

view this post on Zulip Brendan Hansknecht (Jun 09 2023 at 16:00):

I wouldn't worry about the perf cost. It should be branchless or can be made branchless. If the values to clamp by are constant, it will inline anyways. Also, someone can write the faster version manually if somehow clamp is the bottleneck of the program.

view this post on Zulip Brendan Hansknecht (Jun 09 2023 at 16:01):

I may be missing something but how does a clamp with exclusive boundaries work?

What is the value of restrictToInterval 1 {min: 1, max: 10}? Is it 1 or 2? As in, are we containing within or bringing to. Maybe that isn't actually a confusion in practice. Just came to mind due to the API.

view this post on Zulip Anton (Jun 09 2023 at 16:03):

Riiight, I think it'll be ok in practice

view this post on Zulip Fábio Beirão (Jun 09 2023 at 16:12):

I agree that the intervals should always be inclusive as in [ 1..10 ]. In other words: the output of restrictToInterval should always be one of the inputs

view this post on Zulip Brendan Hansknecht (Jun 09 2023 at 16:26):

Sounds good

view this post on Zulip Anton (Dec 16 2023 at 11:27):

restrictToInterval is being implemented, currently with the signature Num a, Num a, Num a -> Num a. Calls look like this: restrictToInterval 1 5 10. I think a record similar to List.range would be better here, so you can instantly see what's what in a call. I propose restrictToInterval 1 { startAt: 5, endAt: 10 }.

I don't think we should go exactly like List.range with Before, At, and After here for now.

view this post on Zulip Kilian Vounckx (Dec 16 2023 at 12:37):

Anton said:

I don't think we should go exactly like List.range with Before, At, and After here for now.

I don't think Before and After even make sense. Maybe with integers, but definitely not with floats. I guess you could take the next biggest representable float, but I doubt that is what a user would want.

view this post on Zulip Kilian Vounckx (Dec 16 2023 at 12:38):

Anton said:

restrictToInterval is being implemented, currently with the signature Num a, Num a, Num a -> Num a. Calls look like this: restrictToInterval 1 5 10. I think a record similar to List.range would be better here, so you can instantly see what's what in a call. I propose restrictToInterval 1 { startAt: 5, endAt: 10 }.

This was in the discussion above already I think. The question then is what happens if startAt is bigger than endAt?

view this post on Zulip Anton (Dec 16 2023 at 13:18):

what happens if startAt is bigger than endAt?

I think we should switch them inside the function if that's the case, so:

restrictToInterval 1 { startAt: 5, endAt: 10 } == 5
# and
restrictToInterval 1 { startAt: 10, endAt: 5 } == 5

view this post on Zulip Kilian Vounckx (Dec 16 2023 at 13:19):

Alright, the current implementation does this already, just without the explicit record. I'll change it

view this post on Zulip Kilian Vounckx (Dec 16 2023 at 13:20):

Should the names be startAt and endAt? I like low and high or min and max better

view this post on Zulip Richard Feldman (Dec 16 2023 at 13:24):

are we sure restrictToInterval is a better name than clamp?

view this post on Zulip Richard Feldman (Dec 16 2023 at 13:25):

this isn't a function I personally use often, but I get the impression that the people who do likely expect it to be called clamp :big_smile:

view this post on Zulip Kilian Vounckx (Dec 16 2023 at 13:29):

I use it mostly in game dev. I think for people who know it, clamp is indeed easier to find. But for users unfamiliar to it, I don't really know.
However I think the same is true for List.keepIf (instead of filter) for example. I don't have a strong preference, but I think we shouldn't just pick clamp because every other language uses it

view this post on Zulip Anton (Dec 16 2023 at 13:29):

I find it more important that it's easier to understand what the function does for readers vs easy to discover for existing clamp users.

view this post on Zulip Anton (Dec 16 2023 at 13:31):

Searching clamp in the docs should point you to restrictToInterval

view this post on Zulip Anton (Dec 16 2023 at 13:32):

Should the names be startAt and endAt? I like low and high or min and max better

I value consistency with List.range. I feel like we should crash or error if we use the names low/high or min/max and the order is wrong.

view this post on Zulip Kilian Vounckx (Dec 16 2023 at 13:34):

Anton said:

Searching clamp in the docs should point you to restrictToInterval

Which file do I have to edit to add it here?

view this post on Zulip Kilian Vounckx (Dec 16 2023 at 13:45):

Anton said:

I value consistency with List.range. I feel like we should crash or error if we use low/high or min/max and the order is wrong.

Is crashing really the best option? I feel like this could be an easy bug. Swapping the arguments also feels weird because they are named now, but I think it is still better than crashing.

view this post on Zulip Anton (Dec 16 2023 at 14:03):

Which file do I have to edit [...]

This one

view this post on Zulip Kilian Vounckx (Dec 16 2023 at 14:04):

Anton said:

Which file do I have to edit [...]

This one

yeah, but I mean in the repo :big_smile:

view this post on Zulip Anton (Dec 16 2023 at 14:05):

Opps, haha, bad paste, one sec

view this post on Zulip Kilian Vounckx (Dec 16 2023 at 14:05):

I found this one 'www/public/different-names/index.html', but not sure if it is generated

view this post on Zulip Anton (Dec 16 2023 at 14:06):

Yes, that's the one, it is not generated

view this post on Zulip Richard Feldman (Dec 16 2023 at 14:07):

Anton said:

Searching clamp in the docs should point you to restrictToInterval

I like this in theory, but the problem is that people don't type clamp, they type cl, see that there are 0 results, and stop typing :sweat_smile:

view this post on Zulip Kilian Vounckx (Dec 16 2023 at 14:07):

Anton said:

Yes, that's the one, it is not generated

I'll make the changes there than. In general I think there could be more functions in it

view this post on Zulip Richard Feldman (Dec 16 2023 at 14:07):

we could make some sort of alias system, but I could see it being confusing - like "why am I getting these results?"

view this post on Zulip Anton (Dec 16 2023 at 14:07):

If there are 0 results we should definitely feed the results with synonyms

view this post on Zulip Richard Feldman (Dec 16 2023 at 14:08):

I buy that, but sometimes there might be 1 (unrelated) result, especially if you're only typing 1-2 letters

view this post on Zulip Richard Feldman (Dec 16 2023 at 14:08):

we could always include the extra results, like below a horizontal line or something

view this post on Zulip Richard Feldman (Dec 16 2023 at 14:09):

but the design considerations are kinda tricky here to prevent confusion and/or clutter in the common case :sweat_smile:

view this post on Zulip Anton (Dec 16 2023 at 14:11):

like below a horizontal line or something

Side by side could work well I think, unconventional but seems right

view this post on Zulip Anton (Dec 16 2023 at 14:11):

Like a second column with synonyms

view this post on Zulip Richard Feldman (Dec 16 2023 at 14:14):

interesting! Is there enough room in the layout for that though? :thinking:

view this post on Zulip Anton (Dec 16 2023 at 14:17):

Definitely enough room on desktop, on mobile not though.

view this post on Zulip Kevin Gillette (Dec 16 2023 at 15:08):

I'm worried about flipping the operands if out of order, since that could indicate a logic/arithmetic error. That's very similar to Go's designers considering but rejecting Python-style negative indexing of lists (i.e. x[-1] returning the last elem), because of concerns that it'd mask off-by-one failures and so on (i.e. the programmer intended to reverse iterate using positive indices, but ended up starting at the second-to-last and ending, via -1 wrap around at the last).

view this post on Zulip Kevin Gillette (Dec 16 2023 at 15:09):

In terms of inclusive/exclusive, would this benefit from a tag based design?

restrictToInterval x { min: Include 5, max: Exclude 10, AdjustTo: NearestBound }

Where AdjustTo could be something like Min/Max/NearestBound/WrapAround/Literal x (which need not be in the range, in case a sentinel value is desired to mark out-of-range cases).

view this post on Zulip Kevin Gillette (Dec 16 2023 at 15:10):

I think clampToRange sounds a little more practical to me: "interval" is a bit mathematically formal, and programming languages tend to use range rather than interval for that notion. Clamp also sounds more like an adjustment to me, whereas restrict ambiguously invokes the notions of adjust or validate-without-change, depending on the reader.

view this post on Zulip Anton (Dec 16 2023 at 16:29):

I'm worried about flipping the operands if specified, since that could indicate a logic/arithmetic error.

That's been on my mind as well, it's reasonable to return a Result here...

view this post on Zulip Anton (Dec 16 2023 at 16:30):

would this benefit from a tag based design?

I'd like to start simple and expand later if there is demand.

view this post on Zulip Brendan Hansknecht (Dec 16 2023 at 16:32):

due to floats, I really think we shouldn't use tags

view this post on Zulip Brendan Hansknecht (Dec 16 2023 at 16:33):

Just should be a call to max and a call to min

view this post on Zulip Brendan Hansknecht (Dec 16 2023 at 16:35):

Maybe also a check if the range is valid or returning a result.

Though honestly, for usability, I think this would be better to do any of the following than return a result:

view this post on Zulip Brendan Hansknecht (Dec 16 2023 at 16:37):

from unity docs:

Note: if the minimum value is is greater than the maximum value, the method returns the minimum value.

view this post on Zulip Brendan Hansknecht (Dec 16 2023 at 16:37):

so must apply the max first, then apply the min which will overwrite the max.

view this post on Zulip Brendan Hansknecht (Dec 16 2023 at 16:39):

godot doesn't document this case, but its function should have the note:

Note: if the minimum value is is greater than the maximum value, the method returns the maximum value.

view this post on Zulip Brendan Hansknecht (Dec 16 2023 at 16:39):

so reversed from unity

view this post on Zulip Brendan Hansknecht (Dec 16 2023 at 16:43):

personally, I think crashing is the best answer here. Reversed is clearly an incorrect program state that we would prefer to alert the user of. That said, I don't think anyone will use a function like this if it returns a result. It is like division and addition where it needs to be convenient. Also, a significant portion of the time, it should be used with static parameters where order can't be wrong.

view this post on Zulip Anton (Dec 16 2023 at 17:04):

We could make the default function crashable and add a Checked variant that returns Result.
When using autocomplete (or viewing the docs) it's nice to have the Checked variant right under it, that also makes it clear the other one is unchecked.

view this post on Zulip Kilian Vounckx (Dec 16 2023 at 17:21):

That seems like a good compromise. I can update the PR tomorrow

view this post on Zulip Anton (Dec 16 2023 at 17:29):

I think clampToRange sounds a little more practical to me: "interval" is a bit mathematically formal, and programming languages tend to use range rather than interval for that notion.

I also prefer ToRange over ToInterval.

view this post on Zulip Sky Rose (Dec 19 2023 at 14:34):

I also agree with crashing rather than flipping the args or choosing the max or min.
If I call clamp x min max, then afterwards I expect it to be safe to assume that x >= min and that x <= max. Anything but a crash violates one of those guarantees.


Last updated: Jun 16 2026 at 16:19 UTC