Skip to content

Latest commit

 

History

History
139 lines (113 loc) · 13 KB

reshape.md

File metadata and controls

139 lines (113 loc) · 13 KB

This mezz was made to counter the limitations of COMPOSE.
Inspired also by Ladislav Mecir's BUILD

Examples

The following 3 snippets create a string for program identification, e.g. "Program-name 1.2.3 long description by Author", omitting Description and Author parts when those are not specified:

method code
reshape
form reshape [
@(pname) @(ver) @(desc)
"by" @(author) /if author
#"^/"
]
compose
form compose [
(pname) (ver)
(any [desc ()])
(either author [rejoin ["by "author]][()])
#"^/"
]
build
form build/with [
!pname !ver ?desc ?author #"^/"
][
!pname: pname
!ver: ver
?desc: any [any [desc ()]
?author: either author [rejoin ["by "author]][()]
]

These snippets create Draw code for a box with given parameters (pens, thickness, radius), resulting in smth like fill-pen blue box 1x1 99x49 5:

method code
reshape
reshape [
push [
pen @(pen) /if pen
fill-pen @(fill-pen) /if fill-pen
line-width @(line)
box @(mrg) @(size - mrg) @(radius)
]
]
compose
compose/deep [
push [
(either pen [compose [pen (pen)] ]] [[]])
(either fill-pen [compose [fill-pen (fill-pen)]]] [[]])
line-width (line)
box (mrg) (size - mrg) (radius)
]
]
build
build/with [
push [
?pen
?fill-pen
line-width !line
box !mrg !size-mrg !radius
]
][
?pen: either pen [build [pen ins pen]] [[]]
?fill-pen: either fill-pen [build [fill-pen ins fill-pen] [[]]
!line: line
!mrg: mrg
!size-mrg: size - mrg
!radius: radius
]

These snippets create Draw code for a tooltip with an arrow, based on the make-box code from examples above. matrix is used to flip the arrow vertically if tooltip is displayed above the content. Arrow is disabled if tooltip was moved due to lack of space in the window to display it where requested:

method code
reshape
reshape [
@(make-box/round/margin box/size 1 none none 3 1x1 + m)
push [
matrix [1 0 0 -1 0 @(box/size/y)] /if o <> 0x0
shape [move @(m + 4x1) line 0x0 @(m + 1x4)]
] /if o: box/origin ;-- or no arrow
@[drawn]
]
compose
o: box/origin
?matrix: either o <> 0x0 [compose/deep [matrix [1 0 0 -1 0 (box/size/y)]]] [[]]
?arrow: either o [
compose/deep [
push [
?matrix
shape [move @(m + 4x1) line 0x0 @(m + 1x4)]
]
] [[]]
]
compose/deep [
@(make-box/round/margin box/size 1 none none 3 1x1 + m)
?arrow
@[drawn]
]
build
;) maybe I'm just not proficient enough with this function?
o: box/origin
build/with [
!box
?arrow
!drawn
][
!box: make-box/round/margin box/size 1 none none 3 1x1 + m
!drawn: drawn
matrix: either o <> 0x0 [build [matrix [1 0 0 -1 0 ins box/size/y]]] [[]]
?arrow: either o [
build [
push [
ins matrix
shape [move ins (m + 4x1) line 0x0 ins (m + 1x4)]
]
] [[]]
]
]

And these snippets build a test function used in old implementation of FOR-EACH that checks if values ahead conform to the constraints in the spec.
Spec may have type & value constraints, or none of these.
Result will look like unless ..checks.. [continue] if checks are required, and empty [] otherwise:

method code
reshape
test: []
if filtered? [
test: reshape [
types-match? old types /if use-types?
values-match? old values values-mask :val-cmp-op /if use-values?
]
test: reshape [
unless
all @[test] /if both?: all [use-types? use-values?]
@(test) /if not both?
[continue]
]
]
compose
test: []
if filtered? [
type-check: [types-match? old types]
values-check: [values-match? old values values-mask :val-cmp-op]
test: compose [
(pick [type-check []] use-types?)
(pick [values-check []] use-values?)
]
if all [use-types? use-values?] [
test: compose/deep [all [(test)]]
]
test: compose [unless (test) [continue]]
]
build
test: []
if filtered? [
test: build/with [
unless :!test [continue]
][
!test: build/with [
:!type-check :!values-check
][
!type-check: pick [
[types-match? old types]
[]
] use-types?
!values-check: pick [
[values-match? old values values-mask :val-cmp-op]
[]
] use-values?
]
if all [use-types? use-values?] [
!test: build [all only !test]
]
]
]

As can be seen, RESHAPE shows the intent behind the code more clearly in complex scenarios.

Syntax

reshape is newline-aware dialect.

reshape has only two grammar tokens, used in 4 variants:

rule grammar description example result
insertion @[expression...] Evaluated and always inserted as a single value @[append [1] 2] [1 2]
splicing @(expression...) Evaluated and spliced, except when result is none! or unset! @(append [1] 2) 1 2
line exclusion content... /if expression All content on the line is included/excluded if the result of expression evaluation is true/false 1 @(2 + 3) /if word? 'x 1 5
section exclusion (at the line start) /if expression Everything after this line and up to next single /if on the line (or up to the end) is included/excluded if the result of expression evaluation is true/false
/if word? 'x
1 @(2 + 3)
/if false
@(4 + 5)
1 5

Grammar may be altered using /with refinement, e.g.: reshape/with [@substitute @include] [...data...]. Note that the argument to /with in this case comes before the data to process.

Implementation notes:

  • reshape processing is always deep (descends into any-list! values). Lists following the @ token are an exception - they are just evaluated, not reshaped (for performance reasons mainly).
  • line exclusion and section exclusion may be used together: a line is excluded if either of the exclusion conditions are met
  • excluded sections are completely skipped, so all expressions in them remain unevaluated, while expressions on excluded lines are still evaluated and may produce side effects (a performance tradeoff that should not be relied upon)
  • expressions on the line are evaluated before the /if that follows the line (also a performance tradeoff that should not be relied upon), and this includes reshaping of the nested lists
  • newline markers between @ and next list, as well as between /if and the end of its expression do not matter to reshape, so @ and /if may appear on their own lines
  • no other tokens should appear after /if expression - it's invalid to have them, though it's not verified currently (for performance reasons)
  • performance of reshape is about 10% of compose/deep as it's written in pure Red, but still may be significanly improved with REP #133

An overview of the previous designs

Let's start with COMPOSE limitations:

  • Expressions used in it are often long and they make the code very messy. It becomes hard to tell how the result will look like.
    E.g. compose [x (either flag? [[+ y]][]) + z] -- go figure it will look like [x + z] or [x + y + z] in the end
    This can be circumvented by making preparations, although the number of words grows fast:
      ?+y: either flag? [[+ y]][[]]
      compose [x (?+y) + z]
    
  • It uses parens, so if one wants to also use parens in the code, code gets uglified.
    E.g. parse compose [skip p: if ([((index? p) = i)])] -- seeing this immediately induces headache ;)
    Plus it's a source of bugs when one forgets to uglify a paren, especially in big code nested with blocks.
  • There's no way to conditionally include/exclude whole blocks of code without an inner COMPOSE call
    E.g. compose [some code (either flag? [compose [include (this code) too]][])] -- compose/deep won't help here
    Also sometimes when one conditionally includes the code, one may want to prepare some values for it:
    E.g. compose [some code (either flag? [my-val: prepare a block compose [include (my-val) too]][])] -- this totally destroys readability (and not always can be taken out of compose expression easily, when there's a lot of conditions depending one on the other)
  • Sometimes there's a need to compose the code used in conditions (not the included code itself!) before evaluating them.
    E.g. compose [some code (do compose [either (this) (or that) [..][..]])] -- top-level compose/deep won't help again

What I like about COMPOSE is:

  • Parens visually outline both start and end points of substitution: very are easy to tell apart from the unchanging code.
  • Parens are very minimalistic, which also helps readability in easy cases. And it's also easy to implement.

Ladislav's BUILD has some advantages over it:

  • One can freely use parens as they have no special meaning, and their content will be deeply expanded as well.
  • With preparation code moved into the /with block, expression itself can be even cleaner than it's COMPOSE variant:
    build/with [x :?+y + z] [?+y: either flag? [[+ y]][]]

But it also has it's drawbacks:

  • Extensibility of it is not any better than defining global helpers for compose
  • /with block in practice becomes bigger than it's COMPOSE variant's preparation code. This happens because /with builds an object out of the block, and object constructor does not collect words deeply, so they have to be declared at top level first:
      build/with [...][
          x: y: none
          either flag? [x: 1 y: 2][x: 2 y: 3]
      ]
    
    Another reason for the bloat, is because BUILD can't substitute words not declared in the object without ins or only (which are way less readable).
    E.g. one has to duplicate already set values in the object:
      x: my-value
      build/with [... !x ...] [!x: :x]
    
    So while it keeps the build-expression readable, it does not reduce the overall complexity. It just moves complexity from the expression into the /with block.
  • Apart from simple words, there's no visual hint where each substitution starts or ends.
    E.g. build [ins copy/part find block value back tail series then some code] -- tip: ins eats it up to then, but you have to count arity in your mind to know that ;)
  • ins and only (or any other user-defined transformation functions) are incompatible with operators
    E.g. build [x + ins pick [y y'] flag? + z] -- will try to evaluate flag? + z and will fail. One can write build [x + (ins pick [y y'] flag?) + z] instead, but when building tight loops code, or frequently used function bodies, an extra paren matters. Besides that will only work for inserting a single value, not whole slices of code.

The purpose of RESHAPE is to address all these limitations.

Key design principles of RESHAPE

  • It's code consists of 2 columns: expressions to the left and conditions to the right. This separation helps keep track of both the expression under construction, and conditions, and be able to connect both easily. For that to work without extra separators tokens, I had to make it new-line aware.
    E.g.:
      this code is always included
      this code is included          /if flag?: this condition succeeds
      this is an alternate code      /if not flag?    ;) included if the last condition failed
    
  • Unlikely coding patterns are used to minimize the need to escape anything: @[...] @(...) /if ... -- fat chance you will encounter these in normal Red.
    @ substitution marker is chosen because it visually stands out, which is important in bigger blocks.
    [] indicates that block is inserted as a block (aligns with common /only meaning), () indicates splicing.
  • User-defined grammar allows to nest reshape calls - useful when inner code must be reshaped sometime later
  • Expressions to be substituted are wrapped in parens/block so their limits are clearly visible and contrast with the rest.
    E.g. @(copy/part find block value back tail series) then some code -- can be used pretty much like compose and stay readable.
  • /if controls what lines to include or not, eliminating the need for preparation code, at least in straighforward scenarios.
  • There's section-level and line-level exclusion conditions so one can control inclusion on both levels.
  • Deep processing of input, so that no extra calls to reshape are needed to expand nested blocks/parens (nested calls are rather ugly).
  • Input is always copied deeply, so one can use static blocks.