[design] apply function
APPLY is proving to be a challenge to design. There are too many ways to do it, some of which compete for a particular interface model. Most importantly, we have to consider how easy it will be to use correctly, as well as how flexible it is for power users.
An important aspect is that we need to compare apples to apples. If we add an example for one model, we have to include versions of it for all other models for comparison. The examples may fall into different use case scenarios.
We've been all over the map on this, and need to consolidate to avoid losing our minds further.
See: https://github.com/greggirwin/red-hof/blob/master/apply.md#use-cases
We should have examples for each.
Dynamic arguments are the norm, of course but refinements are not, and they may also specify args (params) to go with them. That's the pain point in Red, without apply
because you end up dispatching with either
or case
and using hard-coded paths for the refinements.
Function extension
Tracing calls
sys-find: :find
my-find: func spec-of :sys-find [
; dump args, do extra stuff.
; call sys-find
]
Passing around a set of refinements+arguments. Simliar to Extend.
dup: function [
{Returns a new block with the fill value duplicated n times.}
value
count [integer!] "Negative numbers are treated as zero"
/only
/string "Return a string instead of a block"
][
series: copy either string [""] [[]]
append/dup/:only series value count
]
repend': func [
{Appends a reduced value to a series and returns the series head}
series [series!]
value
/only "Insert block types as single values (overrides /part)."
/dup "Duplicate the inserted values."
count [integer!]
][
value: reduce :value
res: append/:dup/:only series value count
]
Programmatic call construction. Call a func with dynamic arguments/refinements known from some data.
Should we also consider indirection of dynamic elements. e.g., rather than writing the paths or propagating refinements from the spec, could you do this (as a mezz):
apply-ex: func [fn [word!] refs [path! block!] args [block!]][
; Do some AOP tricky stuff
fn: to path! head insert copy refs fn
res: apply fn args
; AOP post processing
]
apply-ex 'append [/dup/only] [series value count]
This only adds a different syntax compared to NR§2.1/c*.
Does this fall under /tracing?
This is where we document and name each interface option. e.g.
Get-word refinements are evaluated. If true, eventual optional arguments will be fetched.
foo/:ref1/:ref2/ref3/... <arg1> <arg2> ...
Looks like a regular func call, but refinements can be get-words (like in a regular path) and are evaluated rather than being treated as literal/fixed/truthy. Their values come from the environment/context. Supported internally by semi-sweet.
path! <args> ; free ranging
append/dup/:only series value count ; /dup is fixed, /only is dynamic
<syntax>
All args are required, in correct slots by spec order, but trailing false/none can be omitted.
apply function! block!
apply :foo [<arg1> <arg2> ...]
<syntax>
Fixed arity version of straight sugar. Refinements go in the path. Their values come from the environment/context. Args, required or optional, go in the block.
apply [lit-word! lit-path!] block!
apply 'append/dup/:only [series value count]
<syntax>
Can we call the first two both dyna-path, because they are effectively the same except for the second being fixed arity?
Or maybe we don't name these at all, because they are the default.
Fixed arity. Refinements go in the path. Values (on/off) for dynamic refinements go in block. Args, required or optional, go in the block.
apply [lit-word! lit-path!] block!
apply 'append/dup/:only [series value count true]
<syntax>
Fixed arity. Word, not path. Everything goes in the block.
Requires /some
refinement.
apply/some [lit-word! lit-path!] block!
apply/some 'append [series value /dup true count block? value]
<syntax>
As we can't seem to build consensus on any set of models, perhaps we can start with which models do have consensus (e.g. raw and straight-sugar). From there we decide what use cases those don't fit, and look for the next best model that fills those gaps. We also need to prioritize the case cases.
This is where we list examples, noting their use case category, and what each model looks like for it. That means actual code. With luck we can keep it dense, because each one doesn't have to be run separately. It's about reading and comparing the alternatives.
From this, we should be able to see what models apply best for each use case, as well as determining if a model is redundant and can be done better using another model.
Each model implemented in the native R/S code of apply
has to offer a benefit. The goal being to keep the R/S code small and test mezzanine wrappers for other models to see if their performance is acceptable.
Doc's canonical example. Decides internally to use a refinement.
NOTE: Old model names are in comments until new model names are defined.
output: copy []
emit: func [value][
; original
either block? value [append output value][append/only output value]
; a.1)
only: block? value
append/:only output value
; a.2)
only: block? value
apply 'append/:only [output value]
; b) NR§1)
apply/all 'append [output value false none block? value]
; ; c.1) (OLD)
; apply 'append/:only [output value block? value]
; ; c.2) (OLD)
; apply/safer 'append/:only [output value (block? value)]
; c.1) (NEW)
apply 'append/:only [output value block? value]
apply/safer 'append/:only [output value (block? value)]
; c.2) (NEW)
apply/some :append [output value /only block? value]
apply/some/safer :append [output value /only (block? value)]
; NR§2.1 This doesn't make fixed refinements distinct
only: block? value
apply :append [output value /only]
; BB4.2) BM§op-ref-2)
apply 'append [output value /only block? value]
]
; Variant where /dup is also applied used if arg is a block.
; a.1
emit: func [value][
; dupe values 3 times if they are blocks
only: dup: block? value
; if NOT a block, count *IS* consumed, which differs from a normal func call.
append/:only/:dup output value 3
; If the behavior worked like a regular func, we'd have to do this.
;res: append/:only/:dup output value 3
;res
]
; c.2
emit: func [value][
; dupe values 3 times if they are blocks
apply/safe 'append/:only/:dup [output value (block? value) (block? value) 3]
]
Uses a local value for a required arg, an apply-unrelated refinement, a fixed refinement, and a propagated dynamic refinement.
NOTE: Old model names are in comments until new model names are defined.
dup: function [
{Returns a new block with the fill value duplicated n times.}
value
count [integer!] "Negative numbers are treated as zero"
/only
/string "Return a string instead of a block"
][
series: copy either string [""] [[]] ; applies to all versions
; a.1
append/dup/:only series value count
; a.2
apply 'append/dup/:only [series value count]
; b NR§1
apply/all 'append [series value false none only true count]
;?? Are refinement args enforced as `logic!`, Or is literal `/dup` evaluated as truthy, and still OK?
; The following c* question being, if a refinement is fixed, not dynamic, does its arg(s)
; have to fall in the same order as refinements in the path.
; ; c.1 (OLD)
; apply 'append/dup/:only [series value block? value count] ; Are both of these legal?
; apply 'append/dup/:only [series value count block? value]
; ; c.2 (OLD)
; apply/safer 'append/dup/:only [series value (block? value) count] ; Are both of these legal?
; apply/safer 'append/dup/:only [series value count (block? value)]
; c.1) (NEW)
apply 'append/:only/dup [series value block? value count] ; fixed /dup not in arg block?
apply/safer 'append/:only/dup [series value (block? value) count]
; c.2) (NEW)
apply/some :append [series value /only block? value /dup true count]
apply/some/safer :append [series value /only (block? value) /dup true count]
; NR§2.1 This doesn't make fixed refinements distinct
dup: true
apply :append [series value /dup count /only]
apply :append [series value /only /dup count]
; BB4.2) BM§op-ref-2)
apply 'append [series value /only only /dup true count]
]
Propagate all args and refinements
NOTE: Old model names are in comments until new model names are defined.
repend': func [
{Appends a reduced value to a series and returns the series head}
series [series!]
value
/only "Insert block types as single values (overrides /part)."
/dup "Duplicate the inserted values."
count [integer!]
][
value: reduce :value
; a.1
res: append/:dup/:only series value count
; a.2
apply 'append/:dup/:only [series value count]
; b NR§1
apply/all 'append [series value false none only dup count]
; c.1
apply 'append/:dup/:only [series value dup count only] ; Are both of these legal?
apply 'append/:dup/:only [series value only dup count]
apply 'append/:dup/:only [series value dup only count] ; No way to catch reversed logics, right?
; c.2
apply/safer 'append/dup/:only [series value (block? value) count] ; Are both of these legal?
apply/safer 'append/dup/:only [series value count (block? value)]
; NR§2.1 This doesn't make fixed refinements distinct
apply :append [series value /dup count /only]
apply :append [series value /only /dup count]
; BB4.2) BM§op-ref-2)
apply 'append [series value /only only /dup dup count]
]
More refinements here, many of which propagate, and one of which propagates through two different apply calls. No refinement args.
alter': function [
"Alternate a value (append/remove), returning the toggled state (on/off)"
input [series! bitset!] "If value is found, remove it; otherwise, append it (modified)"
value "Must be an integer, char, or string if input is a bitset"
/only "Treat block and typeset values as single values."
/case "Perform a case-sensitive match; ignored for bitset inputs."
/same "Use same? as comparator."
/last "Find the last occurrence of value, from the tail."
/reverse "Find the last occurrence of value, from the current index."
/match "Match at current index only."
][
; a.1
pos: find/:only/:case/:same/:last/:reverse/:match input value
; body body body body
append/:only input value
; body body body body
; a.2
pos: apply 'find/:only/:case/:same/:last/:reverse/:match [input value]
; body body body body
apply 'append/:only [input value]
; body body body body
;!! NOTE: get-word args in the following examples can be plain words.
; b NR§1
; series value /part length /only /case /same /any /with wild /skip size /last /reverse /tail /match]
pos: apply/all :find [input value false none :only :case :same false false none false none :last :reverse false :match]
;pos: apply/all :find [input value false none only case same false false none false none last reverse false match]
; Nobody wants to write this, or read it, but generated calls using this
; model will look like this when debugging.
;pos: apply/all :find [input value false none true true true false false none false none true true false true]
; body body body body
; series value /part length /only /dup count
apply/all :append [input value false none :only]
;apply/all :append [input value false none only]
; body body body body
; c.1
; series value /part length /only /case /same /any /with wild /skip size /last /reverse /tail /match]
apply 'find/:only/:case/:same/:last/:reverse/:match [input value :only :case :same :last :reverse :match]
; body body body body
; series value /part length /only /dup count
apply/all :append/:only [input value :only]
; body body body body
; c.2 (same as c.1 since all args are single values)
; series value /part length /only /case /same /any /with wild /skip size /last /reverse /tail /match]
apply/safer 'find/:only/:case/:same/:last/:reverse/:match [input value :only :case :same :last :reverse :match]
; body body body body
; series value /part length /only /dup count
apply/safer :append/:only [input value :only]
; body body body body
; NR§2.1 This doesn't make fixed refinements distinct
apply :find [input value /only /case /same /last /reverse /match]
; body body body body
apply :append [input value /only]
; body body body body
; BB4.2) BM§op-ref-2)
apply 'find [input value /only :only /case :case /same :same /last :last /reverse :reverse /match :match]
; body body body body
; series value /part length /only /dup count
apply 'append [input value /only :only]
; body body body body
; context carryover (#5)
series: input ;-- can also be renamed in the function spec?
apply :find 'local
; for append a.1) is preferable
]
toggle: :alter'
Dynamic refinement simple case.
capture-face
definition: https://codeberg.org/hiiamboris/red-view-test-system/src/commit/571f9d8e0725120e29d3c5afac07c0c40c53ece1/visuals.red#L91
unordered key/value pairs with word shortcuts (original mezz impl):
img: apply capture-face [
face: layout
real: real
with: not real img: img
whole: whole
]
or
img: apply capture-face [
real whole
face: layout
with: not real img
]
a.1)
with: not real ;-- blocks usage of `with` function in the calling function's body
with': :system/words/with ;-- workaround
img: capture-face/:real/:with/:whole layout img
BB4.2) BM§op-ref-2)
img: apply capture-face [
layout
/real real
/with not real img
/whole whole
]
Dynamic refinement trickier case.
send-request
definition: https://gitlab.com/rebolek/castr/-/blob/master/client-tools.red#L361
unordered key/value pairs with word shortcuts (original mezz impl):
response: apply send-request [
method
link: url
data: post
content: data
with: yes args: compose/only [...]
]
a.1)
data2: data ;-- preserve data from being overridden
data: post
send-request/:data/with method url data2 compose/only [...]
data: data2 ;-- restore data for further usage
BB4.2) BM§op-ref-2)
response: apply send-request [
method url
/data post data
/with yes compose/only [...]
]
Function is unknown, defined in the object by the user, and may or may not support used refinements, and declares them in any order.
original code:
spec: spec-of :draw
on?: find spec /on
window?: find spec /window
draw: case [
all [on? window?] [draw/window/on xy1 xy2 canvas fill-x fill-y]
window? [draw/window xy1 xy2 ]
on? [draw/on canvas fill-x fill-y]
'else [draw ]
]
using current (slow) mezz apply
implementation:
apply draw [window xy1 xy2 on canvas fill-x fill-y]
a.1) b)
not applicable here - code will be the same as the original
BB4.2) BM§op-ref-2) - only works if this mode supports /quiet refinement
apply/quiet draw [
/window window xy1 xy2
/on on canvas fill-x fill-y
]
context carryover (#5) - as a replacement for /quiet
;-- defined outside somewhere - to limit the set of carried arguments to only those known by the caller
proxy-draw: func [draw /window xy1 xy2 /on canvas fill-x fill-y] [apply draw 'draw]
;-- actual call:
proxy-draw/window/on :draw xy1 xy2 canvas fill-x fill-y
Lots of subjectivity here, so we need a way to cast votes as data, not prose and commentary.
Both Red and Red/System are published under the BSD license. The runtime is published under the BSL license.