3
$\begingroup$

It is useful to be able to provide default values in pattern-matching, for example, if an element has a key but no corresponding value, you can take the value to be zero:

Cases[
 {
  {a, b, {key, value1}, c},
  {e, {key, value2}, e},
  {o, p, {key}, t},
  {e, f, g}
  },
 {key, value_ : 0} :> value,
 {2}
 ]

But I would like to go further, and say, if a key-value pair fails to materialize, give 0 anyway:

Cases[
 {
  {a, b, {key, value1}, c},
  {e, {key, value2}, e},
  {o, p, {key}, t},
  {e, f, g}
  },
 {key, value_ : 0} : 0 :> value,
 {2}
 ]

This is a syntax error in Mathematica (documentation below). It would be nice to have this power anyway, and it seems well-defined. Is there some trick that can be done to enable this?

Edit: This is how the procedure of pattern-matching with optional values is carried out in my mental model. When the computer is given

ReplaceAll[
 {key},
 {key, value_ : 0} :> value
 ]

It looks foremost for pairs of the form {key,_}. If it finds such a pair, it replaces it with the value in the slot. If it does not find such a pair, then it looks instead for {key}. If it finds this, it replaces it with the default (0) of the pattern appearing in the slot.

ReplaceAll[
 {key},
 {key, value_ : 0} : (This doesn't matter) :> value
 ]

Then what I thought should happen for the above code is that, upon failing to match either {key, _} or {key}, the pattern should finally match any form of expression and place the default of value on the right. The term appearing on the right-hand-side of the outer Optional having no relevance as it isn't reference in the right-hand-side of the RuleDelayed.

Is it wrongheaded to expect this to be possible? I would like for it to be, because there is some complex destructuring that I would like to do in other scenarios that cannot otherwise be handled. For example, the destructured Optional may not be the outer-most piece of the Pattern.

enter image description here

$\endgroup$
8
  • 1
    $\begingroup$ So, what would your desired out put be? $\endgroup$
    – lericr
    Dec 3, 2023 at 18:01
  • $\begingroup$ {value1, value2, 0,0} $\endgroup$
    – Diffycue
    Dec 3, 2023 at 18:02
  • 2
    $\begingroup$ Can you explain why? I'm confused. Which element corresponds to that second 0? $\endgroup$
    – lericr
    Dec 3, 2023 at 18:04
  • 1
    $\begingroup$ Well, I hate to say it, but yes, I think your expectation is wrongheaded. When patterns are matching, we need to bind things. To say that the default for the larger expression doesn't matter doesn't make sense to me, because for the rule to work we'd need to bind something to value, but by definition, there's nothing for it to bind to if we're in the larger default case. $\endgroup$
    – lericr
    Dec 3, 2023 at 19:10
  • 1
    $\begingroup$ Or to put it another way, you're trying to create a sort of fall-through default at the top level, but it doesn't make sense to use Optional at that level, because you've got nothing to match against at that point. You're falling through through to empty space. $\endgroup$
    – lericr
    Dec 3, 2023 at 19:13

4 Answers 4

4
$\begingroup$

Edited because I didn't realize that key was intended to be literal.

test = {{a, b, {key, value1}, c}, {e, {key, value2}, e}, {o, p, {key}, t}, {e, f, g}};

FirstCase[#, {key, val_} :> val, 0] & /@ test
(* {value1, value2, 0, 0} *)

I used FirstCase because I wasn't sure if there was a guarantee that there would be at most one match per item (and maybe I shouldn't have assumed that you wanted one result per item anyway). And also, FirstCase allows you to specify a default.

$\endgroup$
1
  • $\begingroup$ Thank you! Maybe I can clarify better; my question isn't really about Cases but about patterns. I will make a better effort to specify my question; sorry I wasn't thinking clearly enough before. $\endgroup$
    – Diffycue
    Dec 3, 2023 at 18:25
3
$\begingroup$

Edited because I didn't realize that key was intended to be literal.

Okay, how about pattern matching at a higher level?

Cases[test, {___, a : {key, _} : {Null, 0}, ___} :> a[[2]]]
$\endgroup$
3
  • $\begingroup$ This is definitely interesting, thank you. There is a tradeoff, however, in that we have given up on referring to the optional value by name and instead refer to it by its place in a larger expression, whose name is immaterial. In this sense we haven't benefited from destructuring. $\endgroup$
    – Diffycue
    Dec 3, 2023 at 18:52
  • 1
    $\begingroup$ Okay. I think it's the semantics of Cases that's making this confusing. It's hard to imagine the need for a default for Cases, because then you'd expect a result for every non-match, and what does that mean? You got two matches at level 2; how can we expect Cases to know that you want exactly four results? It's not looking at level 2 in a hierarchical way, it's just literally looking at level 2 as its own thing. $\endgroup$
    – lericr
    Dec 3, 2023 at 19:03
  • $\begingroup$ I will add an example without Cases. $\endgroup$
    – Diffycue
    Dec 3, 2023 at 19:11
2
$\begingroup$

Looking at your new example:

ReplaceAll[{key}, {key, value_ : 0} : "blah" :> value]

This is just a syntax error, because of how : is used as a special form. The : is problematic, because it can denote either Optional or just Pattern. In this case, it can't be resolved to a valid pattern or a valid optional. So, we need to be explicit:

ReplaceAll[{key}, Optional[{key, value_ : 0}, "blah"] :> value]

This can now be parsed, but if you read the resulting error message, you'll see the problem:

Optional pattern Optional[{key,value_:0},blah] has no enclosing pattern so no optional value can be matched.

There's no higher level pattern in which this bit is optional. Optional is supposed to be part of a larger pattern. Your expectation is something like "if you match anything else, then use this outer default", but the pattern doesn't "match anything else", it just fails to match, and in failing to match, there's no sub-part to assign the default value to.

$\endgroup$
1
  • $\begingroup$ If you're interested, I've figured it out. I am writing my answer now. $\endgroup$
    – Diffycue
    Dec 3, 2023 at 23:04
1
$\begingroup$

One solution, although not very elegant, is to use Alternatives and manipulate the assignment that shows up in the rule. The way this works is that it replaces all the pattern variables which show up in the assignment with wrapper[{default value,variable}]. Then it does the pattern match. If the variable is not bound, we get wrapper[{default value}]. Finally, it takes the last element of the list appearing within wrapper: the default value if no variable were bound, and the pattern-matched value otherwise. The pattern-match to nothing is done with Alternatives and a blank pattern.

AscertainDefaults[pattern_] :=
 Cases[
  pattern,
  k_Optional :> Thread[{k[[2]], k[[1]][[1]]}], All
  ]

SetDefaultsReplaceAll[
  expr_, pattern_, assignment_
  ] :=
 Block[
  {ammendedAssignment, wrapper, result},
  ammendedAssignment = assignment /. Table[
     Last[default] -> wrapper[default],
     {default, AscertainDefaults[pattern]}
     ];
  result = expr /. pattern -> ammendedAssignment;
  result /. wrapper[{x___}] :> Last[{x}]
  ]

test = {{a, b, {key, value1}, c}, {e, {key, value2}, e}, {o, p, {key},
     t}, {e, f, g}};
pattern = {({key, value_ : 0} | {key} | _) ...};
assignment = value;

With this in place, we get the desired behavior using

SetDefaultsReplaceAll[
   #,
   pattern,
   assignment
   ] & /@ test

where the map was as necessary as the level specification of Cases in the original.

The annoying aspect is that the evaluator makes one fight to grab the names of the variables appearing in the pattern, so I haven't figured out how to do this where the Rule appears as a Rule, instead of a pattern and assignment separately.

A more difficult case: this one places all the quantities of apples, bananas, and pears in order for each collection appearing in the data. If a collection does not contain one of the types of quantities, then it replaces it with the default value 0.

data = {{{13, pears}, {4, apples}, {8, bananas}}, {{2, bananas}, {4, 
     apples}, {19, pears}}, {{5, pears}}, {{10, pears}, {6, 
     bananas}, {2, apples}}, {{19, pears}}, {{8, pears}, {6, 
     apples}}, {{7, bananas}}};
pattern = {({a_ : 0, apples} | {b_ : 0, bananas} | {p_ : 0, pears}) ...};
assignment = {a, b, p};
SetDefaultsReplaceAll[
 data,
 pattern,
 assignment
 ]

Output:

{{4,8,13},{4,2,19},{0,0,5},{2,6,10},{0,0,19},{6,0,8},{0,7,0}}
$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.