Skip to content

Instantly share code, notes, and snippets.

@greggirwin
Last active August 19, 2023 06:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save greggirwin/1f8c1a7f59b4d47cefd9267ae0ccb0af to your computer and use it in GitHub Desktop.
Save greggirwin/1f8c1a7f59b4d47cefd9267ae0ccb0af to your computer and use it in GitHub Desktop.
Step mezz func (new name for incr/decr) and demo script.
Red []
do %step.red
; Wrapper that hides the detail of whether a value is indirectly referenced.
arg-val: func [arg][
either any [any-word? arg any-path? arg] [get arg][arg]
]
test-step: func [arg /by amt][
print ["Arg: " mold arg]
if by [print ["By: " amt]]
if any-path? arg [print ["Root: " mold get first arg]] ; root of path
print [
"Before:" mold arg-val arg newline
"Result:" mold either by [step/by :arg amt][step :arg] newline
"After: " mold arg-val arg
]
if any-path? arg [print ["Root: " mold get first arg]] ; root of path
print ""
]
; Use different values, so show how STEP works.
; Scalars
n: 1 test-step 'n
; step/down 'n
test-step/by 'n 5
n: 1.2 test-step 'n
test-step/by 'n 3.4
p: 1x2 test-step 'p
test-step/by 'p 3x4
ch: #"A" test-step 'ch
test-step/by 'ch 3
t: 1.2.3 test-step 't
test-step/by 't 0.0.1
t: 1:2:3 test-step 't
test-step/by 't 0:0:15
pct: 1% test-step 'pct
test-step/by 'pct 0.5%
m: $1 test-step 'm
test-step/by 'm $0.50
d: 01-jan-2022
test-step 'd
test-step/by 'd 7
d: 01-jan-2022/12:00:00
test-step/by 'd 0:0:15
; Series
b: [a b c d e f]
test-step 'b
; step/down 'b
test-step/by 'b 3
b: [1 2 3 x 4 y 5 z [6 7 8]]
test-step 'b/1
test-step 'b/x
test-step 'b/z
test-step 'b/z/1
; step/down 'b/z/1
;print mold b
customers: [
gregg [
stats [visits 0]
]
]
name: 'gregg
test-step 'customers/:name/stats/visits
test-step/by 'customers/:name/stats/visits 4
;-------------------------------------------------------------------------------
;advance: function [
; series [word!] "Word referring to a series"
; /by "Advance by count, instead of 1"
; count [integer!]
;][
; op: either series? get value [:skip][:add]
; set value op get value amount
; :value ;-- Return the word for chaining calls.
;]
;tests: [
; scalars: [#"A" 2 3.4 5x6 7% 8.9.10 11:12:13 14-Feb-2022 $16.17]
; scalar-words: [char int float pair pct tuple time date money]
; set scalar-words scalars
;
; paths: [a/b/c 'a/b/c :a/b/c a/b/c:]
; path-words: [path lit-path get-path set-path]
; set path-words paths
;]
halt
Red []
Re: WRT: func ['name content][
; add content to system catalog, tagged with name.
]
WRT step {
The most common case will be to change state, but step does add meaning,
and has benefits, in my mind, when used with scalars. That can always
be added later, as it's a point of design contention on the value.
Benefits over manual `n: n + 1` approach:
- By default there is no amount value, so you can't get the + 1 part wrong.
- STEP is often used when looping, making loop variables stand out from
other `+/-` ops that might be in expressions.
- STEP generally means the amount is small. Another mental hint.
- If stepping sub-values, e.g. a pair's X or Y, it removes another thing
you could get wrong. `pos/x: pos/y + 1`
Other thoughts:
- If step updates a reference "in place", using a lit-word as the arg,
rather than a lit-arg, makes it look more like `set` calls, which is
nice.
- If step does not support scalar args, and only refs to update, the
lit-arg approach may be read differently, like a control func.
- If we use a lit-word! param how do you use a computed step arg? It's
just a big ugly to do, not impossible.
- It's not a terrible thing to use lit-word! params. They are a tool.
But they can make you think a bit more, especially in mixed expressions.
For STEP that may not be as much of an issue, but it may be used a lot,
and that means that many more cases where a non-evaluated arg isn't
directly visible when reading code.
- If the arg is not a lit-arg, and lit-words are used as args, that won't
break if the param is later changed to a lit-arg in STEP's spec.
- Should STEP return the word or the value? Returning the word means you
can chain with other word calls, but not directly with value calls,
like WHILE tests. The latter seems much more useful, because you already
have the word, and what else would you do with it?
}
step: function [
"Steps (increments) a value or series index by 1"
name [any-word! any-path!] "Value referenced must be a series or scalar"
;'name [any-word! any-path!] "Value referenced must be a series or scalar"
/down "Step in a negative direction (decrement)"
/by "Change by this amount, instead of 1; can't be used with both /down and tuples"
; Not all scalar step types make sense.
amount [integer! float! pair! percent! time! tuple! money!] ; = exclude [scalar!] [char! date!]
][
amount: any [amount 1]
if down [amount: negate amount]
; We use this more than once. No need to `get` it each time.
value: get name
; Type check
unless any [series? :value scalar? :value][
cause-error 'Script 'invalid-arg "Step name must refer to a series or scalar value"
;cause-error 'Script 'invalid-type [type? :value]
]
; This is to be smart about stepping percents by numbers. We do
; this, so the default STEP call is nicer. If we don't do this
; we can explain why `1% + 1 == 101%` but that's not a nice for
; the user, because every time percent is used, they have to use
; `/by`, which largely defeats the purpose of STEP. This is about
; "stepping" in the context of the value given.
if all [
percent? :value
any [integer? amount float? amount]
][
amount: 1% * amount ;-- Scale and cast amount to percent.
]
;op: either series? :value [:skip][:add] ;!! This doesn't work compiled.
;set name op :value amount
set name either series? :value [
skip :value amount
][
add :value amount
]
;name ;?? Return word for chaining calls?
]
;step: function [
; "Steps (increments) a value or series index by 1"
; value [scalar! any-word! any-path!] "If value is a word, it will refer to the new value"
; /down "Step in a negative direction (decrement)"
; /by "Change by this amount, instead of 1"
; amount [scalar!]
;][
; amount: any [amount 1]
; if down [amount: negate amount]
;
; ;?? Is this worth it?
; if integer? value [return add value amount] ;-- This speeds up our most common case by 4.5x
; ; though we are still 5x slower than just adding
; ; 1 to an int directly and doing nothing else.
;
; ; All this just to be smart about stepping percents by numbers.
; ; The question is whether we want to do this, so the default 'step
; ; call is inarguably nicer. If we don't do this it is easy to explain
; ; but not very nice. This is about "stepping" in the context of the
; ; value given.
; if all [
; any [integer? amount float? amount]
; ;1 = absolute amount ;!! This makes it hard to reason about.
; any [percent? value percent? attempt [get value]]
; ][amount: 1% * amount] ;-- Scale and cast amount to percent.
;
; case [
; scalar? value [add value amount] ;-- Same as n: n + i
; any [
; any-word? value
; any-path? value ;!! Check any-path before series.
; ][
; op: either series? get value [:skip][:add]
; set value op get value amount
; :value ;-- Return the word for chaining calls.
; ]
; ; Does this add enough value over `next/skip`?
; ;series? value [skip value amount] ;!! Check series after any-path.
; ]
;]
@hiiamboris
Copy link

Since we're on our own, will you agree on this implementation? I added increments of series items and removed error checks because it's not a routine and they're costly.

Red []

context [
	ref-type!: union any-word! any-path!
	
	set 'step function [
		"Steps (increments) a value or series index by 1"
		target [any-word! any-path! block! hash! vector! binary! image!]
		      "Value referenced must be a series or scalar (incl. within a series)"
		/down "Reverse step direction (decrement)"
		/by   "Change by this amount, instead of 1"
			amount [integer! float! pair! percent! time! tuple! money!] ; = exclude [scalar!] [char! date!]
	][
		amount: any [amount 1]
		name:   either find ref-type! type? target [target]['target/1]
		value:  get name
		set name case [
			series?  :value [skip value amount * pick [-1  1 ] down]
			percent? :value [add  value amount * pick [-1% 1%] down]	;-- 1% to avoid /by
			down [:value - amount]										;-- `-` for tuples
			'up  [:value + amount]
		]
	]
]

#include %assert.red
#include %localize-macro.red

#localize [#assert [
	x: 0
	1 = step 'x
	1 = x
	
	c: 10.20.30
	11.21.31 = step         'c
	10.20.30 = step/down    'c
	 0.10.20 = step/down/by 'c 10
	
	p: 90%
	91% = step         'p
	
	b: [k 3]
	4   = step         'b/k
	6   = step/by      'b/k 2
	3   = step/down/by 'b/k 3
	
	[3] = step 'b
	4   = step         b
	2   = step/by      b -2
	5   = step/down/by b -3
	
	b: [k [1 2 3]]
	[2 3] = step 'b/k
	3     = step b/k
]]

@greggirwin
Copy link
Author

"Costly" isn't a number. ;^) The error check costs ~0.07s per 100'000 calls here. I'm OK with that. What we get in return is vastly more helpful.

invalid argument: "Step name must refer to a series or scalar value"

vs

*** Script Error: + does not allow word! for its value1 argument
*** Where: +
*** Near : amount
*** Stack: step  

Yes, the overhead is ~1s for 1M calls, but if you know you're going to be looping anywhere near that many times, you probably already ruled out step.

>> n: 0 profile/show/count [[step 'n][n: n + 1]] 1'000'000
Count: 1'000'000
Time         | Time (Per)   | Memory      | Code
0:00:00.18   | 0:00:00      | 284         | [n: n + 1]
0:00:02.295  | 0:00:00      | 440         | [step 'n]

The performance for my version is even worse. :^)

These lines are a lot to tease apart for a very simple process. Saving lines is not a priority.

series?  :value [skip value amount * pick [-1  1 ] down]
percent? :value [add  value amount * pick [-1% 1%] down]	;-- 1% to avoid /by

This is, of course, a style call. I generally like code that I can read, understanding pieces as I go. So if I get confused, I can go back to the place where I got lost. That's harder to do the longer lines and expressions are. Of course, I've been known to break this rule myself.

What I like the least about it, though, is the one thing you really want.

	b: [k [1 2 3]]
	[2 3] = step 'b/k
	3     = step b/k

Here we have calls that:

a) would be very easy to mix up when reading.

b) have a variant that doesn't look like it takes a reference, but treats it like one.

I think using step 'b/k/1 is an acceptable tradeoff for literals, and @dockimbel will prefer it, since there aren't two implicit aspects now. ;^) It does mess up your pattern of having an interim func though. Your solution here is probably the cleanest implicit pattern if we want to support that. Having thought about it a bit, I can see the value there. Let me hybridize our versions for review.

I'll also exclude tuples and money, since I think we agreed those aren't important use cases.

What is a use case for binary!?

@greggirwin
Copy link
Author

Rats, I forgot that excluding the special case types makes things much uglier, especially for doc strings.

@greggirwin
Copy link
Author

I can hear you laughing, knowing I'm going to end up back at your version. ;^)

@hiiamboris
Copy link

Lol. Anyway, if you want to duplicate Red error checks, why not test for unset? For supported scalar types? For compatibility of target and amount types? If only that one error was the only one possible...

@hiiamboris
Copy link

What I would do it this case is use #assert, so friendly checks are there, but no cost in release version.

@greggirwin
Copy link
Author

On errors, as with most things, it's compromises. To me it seems like that particular error is a zillion times more likely than unset, and type errors can occur so many places in regular Red that they can be reasoned about. The misdirection used by step makes me think that + does not allow word! for its value1 argument will be very confusing to users. "+? Ah, stack says step is involved, but I gave it a word!, and it doesn't have a value1 parameter." That kind of thing.

One of the reasons I like runtime checks is because my R2 work probably ended up being 80/20 interpreted vs compiled, in production.

Since I gave rough performance numbers, and when I think step won't be used, what specific aspects there do you disagree with?

We're always going to push and pull on performance vs user friendliness, but I lean toward the latter, because it's generally so abhorrent in programming today. This often leaks out to users, and what has helped me in the past is making it as easy as possible for users to send me information about a problem that makes it faster and easier for me to fix. In this case I think about how atomic the error info is. e.g. if a user saw that error and knew to copy and send it to us, maybe they send only *** Script Error: + does not allow word! for its value1 argument. What do we do with that, compared to the other one?

@hiiamboris
Copy link

I'm not disagreeing I'm proposing #assert as a solution designed for this case, and that can be turned off globally.

@hiiamboris
Copy link

What is a use case for binary!?

It's just zero-cost to include.

@greggirwin
Copy link
Author

It's just zero-cost to include.

It's funny how we run up against this in so many ways. That it's easier to allow things that may make no sense, because restricting it adds more complexity. Like the tuple restriction, and what errors we specialize versus those we just let occur naturally. e.g. /down and tuples just being mutually exclusive, and see if anyone ever hits that and address it then.

@hiiamboris
Copy link

In my impl they're not mutually exclusive because of ops usage.

@greggirwin
Copy link
Author

Yes, I understand that. But it adds a different bit of logic, because of edge cases where not all scalars support all actions equally. Just a fact of life, and trying to make types behave rationally while running up against implementation issues. e.g. there's no reason tuples couldn't support negative values (aside from looking a little ugly); it's an implementation detail leaking out to some extent. Now (I know this is OT design chat), if we say that tuples have that limitation (and we do), then we cover those limitations with higher level logic when it makes sense, as you did using subtract instead of negate. Then we have the fun of how much we live with those limits, and how creatively they are worked around in different design spaces. It's on the fringe of the step topic per my earlier comment about carroyover logic for tuples. But that's a future conversation.

@greggirwin
Copy link
Author

Glancing at my old tuple stepper, it errors on overflow, rather than failing silently. That's actually a pertinent question for step IMO, because the range is so small.

@hiiamboris
Copy link

Silent overflow is often preferred, like e.g. in Galen's case of color interpolation with easing funcs.

@greggirwin
Copy link
Author

@hiiamboris your version needs to handle percent /by steps.

p: 10%  step/by 'p 5%
== 10.05%

@greggirwin
Copy link
Author

What about this version, which keeps your anonymous reference support, but decompresses the code. The extra typesets were there for playing, but now I kind of like them, because it normalizes the logic (and is more efficient :^).

context [
	tgt-type!: make typeset! [any-word! any-path! block! hash! vector! binary! image!]
	amt-type!: exclude scalar! make typeset! [char! date!]
	ref-type!: union any-word! any-path!
	dst-type!: union series! scalar!

	set 'step function [
		"Steps (increments) a value or series index by 1"
		target [tgt-type!] "Value referenced must be a series or scalar (incl. within a series)"
		/down "Reverse step direction (decrement)"
		/by "Change by this amount, instead of 1"
			amount [amt-type!]					; Not all scalar step types make sense.
	][
		amount: any [amount 1]					; Until we have DEFAULT.
		if down [amount: negate amount]			; /down + negative amount = step positive.
		name:   either find ref-type! type? target [target]['target/1]	; Support anonymous targets
		value:  get name						; We use this more than once, so cache it.

		unless find dst-type! type? :value [	; Type check
			cause-error 'Script 'invalid-arg "Step name must refer to a series or scalar value."
		]
		
		; This is to be smart about stepping percents by numbers. We do
		; this, so the default STEP call is nicer. If we don't do this
		; we can explain why `1% + 1 == 101%` but that's not a nice for
		; the user, because every time percent is used, they have to use
		; `/by`, which largely defeats the purpose of STEP. This is about
		; "stepping" in the context of the value given.
		if all [
			percent? :value
			any [integer? amount  float? amount]
		][
			amount: 1% * amount					;-- Scale and cast amount to percent.
		]
		
		set name either series? :value [
			skip :value amount
		][
			add  :value amount
		]
	]

]

@greggirwin
Copy link
Author

I played with adding a refinement for anonymous ref support, but I liked that even less. It's an option, if @dockimbel thinks the difference between step 'b/k and step b/k is too subtle.

@greggirwin
Copy link
Author

greggirwin commented Aug 17, 2023

This also doesn't support stepping down by tuple.

@hiiamboris
Copy link

@hiiamboris
Copy link

I also think your notion of percents addition is confusing. You treat 1 and 1.0 as percentage of the original value, but 1% not as such. If anything, 1% should be treated as percentage.

@hiiamboris
Copy link

Generally less code, less logical branches, less rules to remember => the better.

@hiiamboris
Copy link

I wonder if step without by on anything other than integers is even a real use case or a product of our imagination...

@greggirwin
Copy link
Author

Thanks for the wiki link. I'm OK with this being interpreted right now./

On percents, scaling and math will always present cases where we have to stop and think about how it's working. My bias is explained in the big comment in the middle of the func for this context. :^) So we have to decide what to do with non-percents. But percents should be easy, right?

>> 1% + 0.125%
== 1.125%

>> p: 1%
== 1%
>> step/by 'p 0.125%
== 1.00125%

>> p: 1%
== 1%
>> step/by 'p 0.125
== 1.125%

>> 1% + 0.125
== 13.5%

IMO that won't make sense to anyone, or lead to correct use of percents and non-percents.

I wonder if step without by on anything other than integers is even a real use case or a product of our imagination...

Integers are by far the most common case, and why I had a short-circuit check in an old version. I'm happy to start with the subset of [integer! percent! time!], because I've used all of those myself in the past. then see if people ask for others. I want to say I also used money! but am much less certain about that. More likely that it stepped through an array of money values, so series use covers that. So we can narrow the use cases, but that won't reduce the code in this case.

@greggirwin
Copy link
Author

Or we can cut it down to only integers, put it to use, and go from there. I probably won't be writing amortization schedules or camera controllers anytime soon. :^)

@greggirwin
Copy link
Author

greggirwin commented Aug 17, 2023

We can then also remove vector! binary! image!, right, or change to any-block!?

@greggirwin
Copy link
Author

OTOH, for a few extra lines of code we have a really general func that is easy to reason about and may be used creatively and effectively in the wild.

@hiiamboris
Copy link

IMO that won't make sense to anyone, or lead to correct use of percents and non-percents.

That's why I don't like special handling of percents.

I'm happy to start with the subset of [integer! percent! time!], because I've used all of those myself in the past. then see if people ask for others.

Wasn't my point. My point is, perhaps for everything other than integers we may require /by, and that would also free us from imagined problems like how should percents be summed.

@greggirwin
Copy link
Author

perhaps for everything other than integers we may require /by

view [progress 0% rate 0:0:0.1 on-time [step 'face/data]]

Now you will argue that it should still use /by because that scenario is always calculated based on something. Which just points out that /by is useful, here or somewhere else. Step is the domain of incrementing/increasing and the opposite, so it's a good place for it. But let's extrapolate further. We've talked about quantity! as a possible datatype. Could it possibly be useful there? e.g., in the U.S. if you have a measurement in feet, but you want to step it inch by inch. Should it be dialected, like my idea for split, or is it constrained enough that a couple refinements will do better? I can't say. But I do feel comfortable saying that it's another tool for thinking, if we want it to be.

@greggirwin
Copy link
Author

I'm also looking at this with range and loop/repeat in mind.

@pekr
Copy link

pekr commented Aug 19, 2023

I have replied to the Element channel, so for the record:

  • I don't need nor like /down, for me, negative value usage is more useful, transparent
  • For code like n: n + 1 the most boring part is to initialise the value most often to zero. And you have to do it outside of the loop, e.g. n: 0 loop 10 [n: n + 1 print ["Something" n]]. I want step to take care for that, or having some default mechanism in the language.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment