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)

Reply via email to