"match True" statement

Within COBOL, a logic control statement referred to as EVALUATE TRUE is commonly used.
It functions similarly to switch-case statements found in other languages or the match-case statement found in Python, except for its ability to resolve logical expressions within its cases.
See the example below;

EVALUATE TRUE
WHEN (TRANSACTION_AMOUNT > MINIMUM_AMOUNT) AND (PAYMENT_METHOD = ‘CASH’)
{do something}
WHEN (TRANSACTION_AMOUNT < MINIMUM_AMOUNT) AND (PAYMENT_METHOD = ‘CARD’)
{do something}
END-EVALUATE

Note the WHEN conditions are what is evaluated and TRUE is what we’re “matching” to.
The magic is in the conditions themselves. In the event TRANSACTION_AMOUNT = 10.00, MINIMUM_AMOUNT = 5.00 and PAYMENT_METHOD = ‘CASH’, the first WHEN condition is resolved as TRUE, and subsequently its code is executed. Otherwise, the next WHEN condition is resolved, and so on.

This functionality doesn’t exist within Python match-case statements. While there are other methods that can be used to emulate this, I think it would be a useful enhancement to the language. This would be an example of how it might look in Python;

match True:
case (trans_amount > min_amount and pay_method.is_cash()):
{do something}
case (trans_amount < min_amount and pay_method.is_card()):
{do something}

How is this different from if...elif...else?

7 Likes

In no uncertain terms, it’s not.

In fact, the EVALUATE statement didn’t even exist until COBOL-85. However, its introduction made for much cleaner code that’s easier to read.

In the case where there are several conditions, each with its own gnarly branches of nested if statements, a match True statement could be used and make the logic a little more concise.

Python has exactly what you need:

if trans_amount > min_amount and pay_method.is_cash():
    {do something}
elif trans_amount < min_amount and pay_method.is_card():
    {do something}
5 Likes

(Hm, for some reason your post was auto-moderated or I would have seen it earlier. Possibly if you were copy-pasting into the text box it thought you were spamming)

That seems pretty subjective. It doesn’t look cleaner or more concise to me, nor is it easier to read. And it introduces an additional layer of indentation compared to if statements.

In my experience, when conditionals start to get really gnarly that means it’s time to write separate functions for the branches.

3 Likes

This is the way I do when I want to make clear which optional combinations are [allowed / mutually exclusive / input errors / etc…]

match (trans_amount > min_amount, pay_method.is_cash(), pay_method.is_card()):
    case (True, True, False):
        do_something_1()
    case (False, False, True):
        do_something_2()
    case _:
        do_something_else()
3 Likes

Sorry but having to eyeball which Boolean value maps to which condition above makes this pattern downright unreadable to me.

I’d rather store the results of the tests in reasonably named variables and use the if-elif-else construct to handle different combinations of truth values.

5 Likes

You are right about reasonably named variables.
The if-elif-elif-elif-else block looks more like sequential checks, it makes it more tedious to tell if we reach the correct check without it being intercepted before.
The match-case is not exactly perfect neither… because the “case _” (or any case involving a _), if placed above the other cases, intercepts the check. Yet the sanitization of checks sequentiality feels easier to me within the match-case blocks.

1 Like

Match checks are still tested sequentially, though, aren’t they? So what’s the difference?

6 Likes

It only conveys a “less sequential” meaning to the treatment of the checks…
I don’t know how this relates to the point raised by OP. (?)

1 Like

That seems like a bad thing, since it’s wrong. But I don’t agree that it does–as long as one understands how match-case is working.

2 Likes

I will get a bit off-topic here …
Assume you require a function that needs to input two arguments out of three, for example, a linspace starting at zero, taking the end, the step or the number of points.

def linspace0(step=None, end=None, N=None):
    match (step, end, N):
        case (None, None, None) | (None, None, _) | (None, _, None) | (_, None, None):
            print('not enough arguments provided')
        case (None, _, _): step = end / N
        case (_, None, _): end = N * step
        case (_, _, None): pass
    return list(range(0, end, step))

Of course, I can count the number of Nones first, and process, but reading the code will imply more code logic reading.
The snippet above provides a more “tabulated” cases handling, where the “math” logic is emphasized.
(Notice the first case must be handled first, this is the caveat coming from the sequential nature of the match-case)
Can you get with a form both more compact, readable, and self-explanatory than this ?

2 Likes

It isn’t about compactness. The tabulated cases just aren’t readable to me because I have to tire myself constantly moving my focus back and forth between the match header and the cases to remind myself which column is mapped to which variable, because the line case (_, None, _): by itself doesn’t tell me anything.

I would rewrite your specific code example as:

def linspace0(step=None, end=None, N=None):
    if None not in (end, N): # or to be cute: end is not None is not N
        step = end / N
    elif None not in (step, N):
        end = N * step
    elif None in (step, end):
        print('not enough arguments provided')
    return list(range(0, end, step))

The only time a table of values makes sense is when you want to use the values as a key to dispatch a handler in contant time complexity:

But again you’d be trading readability for efficiency. I’d still prefer the if-statement version for understanding the flow of reasoning much better (code from the link in the comment above):

if unsafe_hash:
        # If there's already a __hash__, raise TypeError, otherwise add __hash__.
        if has_explicit_hash:
            hash_action = 'exception'
        else:
            hash_action = 'add'
    else:
        # unsafe_hash is False (the default).
        if has_explicit_hash:
            # There's already a __hash__, don't overwrite it.
            hash_action = ''
        else:
            if eq and frozen:
                # It's frozen and we added __eq__, generate __hash__.
                hash_action = 'add'
            elif eq and not frozen:
                # It's not frozen but has __eq__, make it unhashable.
                #  This is the default if no params to @dataclass.
                hash_action = 'none'
            else:
                # There's no __eq__, use the base class __hash__.
                hash_action = ''

It’s not universal that the if statements are more readable. I can scan the _hash_action cases table, and instantly see/trust that all cases are accounted for, and that we’re always assigning something to hash_action and doing nothing else. Doing a look-up for a given case is also trivial.

So on the balance I prefer it.

But you can write code as you prefer :wink:

3 Likes

if statements break down the decision process in a much more meaningful way, and that’s what makes it more readable to me. :slight_smile:

Not for me… but arguing about this would just be a “decision tree vs decision table” debate.

2 Likes

Guard clauses allow match statements to emulate pure if elif chains (using Ellipsis instead of True as the placeholder expression):

    match ...:
        case _ if (trans_amount > min_amount and pay_method.is_cash()):
            {do something}
        case _ if (trans_amount < min_amount and pay_method.is_card()):
            {do something}

It doesn’t avoid the repeated evaluation problem that a properly constructed decision tree can avoid, though.

I think all the examples are problematic because none of them explain the business logic.
What if the transaction amount is greater than the minium and payment is card?

Are cash and card the only two payment options?

I don’t have any thoughts or comments on the trees until I know what the forest is.

1 Like

?

Those are just examples, the actual use cases would be different.

The actual use cases matter, though, as many use cases are actually decision trees where flattening them to a tabular form results in clause duplication.

Expanding the full tree for the given example:

if pay_method.is_cash():
    if trans_amount >= min_amount:
        {do something}
    elif trans_amount < min_amount:
        {do something}
elif pay_method.is_card():
    if trans_amount >= min_amount:
        {do something}
    elif trans_amount < min_amount:
        {do something} 
else:
    {handle error case}

Flattening that is likely to result in redundant execution of the outer conditional checks (unless its turned into an actual data table, looking up the actions to take).