Skip to content

Instantly share code, notes, and snippets.

@greggirwin
Last active August 19, 2023 06:51
Show Gist options
  • 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

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