Pascal Proulx created FREEMARKER-130:
----------------------------------------
Summary: Excluding keys from hashes (without_keys/exclude_keys)
Key: FREEMARKER-130
URL: https://issues.apache.org/jira/browse/FREEMARKER-130
Project: Apache Freemarker
Issue Type: New Feature
Components: engine
Reporter: Pascal Proulx
In several cases I need to pass a hash to a macro or function while excluding
certain keys. I've had to workaround this various ways including: adding an
extra parameter to hash-accepting function of keys to exclude during iteration
(then every function has to implement key exclude), to a transform that creates
a new SimpleHash copy without the offending keys. My last workaround was to
include in the original hash itself (through concatenation) a list of exclude
keys for the target macro to exclude itself and which is excluded itself, to
avoid extra macro parameters. It works, but ugly.
This could be done more cleanly with a built-in implemented similar to hash
concatenation. Suggested form:
{code:html}
myHash?without_keys(["key1", "key2"])
myHash?without_keys("key1", "key2") (if varargs possible; list form more
versatile though)
{code}
Other names: ?exclude_keys...
Most relevant example is when passing the new .args variable added by Daniel
from one macro to another using ?with_args. Example based on real usage:
{code:html}
<#macro m1 a="" b="" c="" attr...>
<#-- Let's say we want to add a class -->
<#if !attr?is_hash><#local attr = {}></#if>
<#local class = attr.class!>
<div<#list attr?without_keys("class") as k, v> ${k}="${v?html}"</#list>
class="red<#if class?has_content> ${class}</#if>"><#nested/>: ${a} ${b}
${c}</div>
...
</#macro>
<#macro m2 a="" b="" d="" e="" attr...>
<@m1?with_args(.args?without_keys("d", "e")) c=3><#nested/> <i>${d}
${e}</i></@>
</#macro>
<@m2 a=1 b=2 d=4 id="my-div" style="display:block;">Values</@m2>
{code}
Like hash concatenation it could be implemented immutably as a hash wrapper
(based on ConcatenatedHashEx) that returns nothing on hash get operations and
skips iteration if the queried key is within the Set (java, internally) of
passed keys to exclude. The downside is the extra key check for performance.
Maybe it could be optimized a different way (see _Extra 3_) but this is
probably acceptable in common cases.
This also becomes convenient if you are looping a hash with keys you want to
exclude. Consider:
{code:html}
<#list myHash as k, v>
<#if k != "key1" && k != "key2">
...
</#if>
</#list>
{code}
can be simplified to:
{code:html}
<#list myHash?without_keys("key1", "key2") as k, v>
...
</#list>
{code}
which is succinct _and_ instantly readable (roughly same performance?).
_Extra 1:_ Since you'd have ?without_keys for key excludes, you could also have
?with_keys or maybe ?only_keys or ?include_keys for key includes, though I
haven't used it as much (I think I did somewhere - some of my functions have an
include/exclude keys mode).
_Extra 2:_ Optionally you could also "easily" support a substract operator
between two hashes, though I didn't have a particular need for this form and is
more clumsy for the purposes above:
{code:html}
<#assign newHash = myHash - {"key1":"value1", "key2":"value2"}>
{code}
Internally this could simply reuse the hash wrapper for `?without_keys` by
storing as the excluded Set keys a copy of the keys from the second hash. But
this form is probably more relevant to cases where the second hash is not
inlined that way and you want a logical set substract (i.e. Java Map
"removeAll") operation between two hashes. The inline form like that works
analogous to the concatenation operation so it would be familiar to users, but
if you only want to exclude predefined keys you have to assign empty values or
empty strings that have no purpose and initialize a needless hash. Worse if
they come from a non-hardcoded list. So I prefer ?without_keys for my purposes.
_Extra 3:_ The form:
{code:html}
?with_args(.args?without_keys("d", "e"))
{code}
does have needless performance overhead that could be avoided if the two calls
could be combined into `?with_args` somehow (such that the excluded keys are
passed to `?with_args` so you skip the wrapping hash), but I'm not sure this is
syntactically possible in a clear way (I use some map-processing functions that
take a second arg which are keys to exclude, but it's not that readable for
functions, so the functions end up taking map of arguments...) or worth the
effort. e.g. only way it's readable is something like:
{code:html}
?with_args(.args, exclude=["d", "e"])
{code}
though you can see how this would be faster. Alternatively I imagine internally
you could optimize the original some other way such as having ?with_args detect
when its argument is a wrapper produced by ?without_keys and special case
during iteration. This is basically nitpicking.
Of course these are only suggestions including the name and implementation and
based on cursory past overview of the Freemarker source. The extras are for
completeness's sake while my brain is working.
--
This message was sent by Atlassian Jira
(v8.3.4#803005)