2
\$\begingroup\$

This [Objective-]C-preprocessor macro currently handles single-variable if-let-not-nil and if-let-is-type, but doesn't handle multiple simultaneous tests and doesn't handle where clauses.

I prefer functions to macros, but I don't see any way to pass a local variable name to a function such that it would create a code block which can use that name internally when being compiled. Is there any way to make this a function?

This does not handle commas in code block: this can be worked around with () brackets, e.g. foo = @[1, 2]; becomes foo = (@[1, 2]);, but I'd prefer something that doesn't surprise anyone using the macro, especially as the compiler error when commas are not wrapped in brackets is unhelpful.

#define IF_LET(tempType, temp, optional, code) \
if ((optional)!=nil && [optional isKindOfClass:tempType.class]) { \
    tempType *temp = (tempType*)optional;\
    {code}\
}

example usage:

for (SKNode *node in touchedNodes) {
    IF_LET(Tile, tile, node, {
        // Tile (capital) is one of my own classes
        // tile (lowercase) is defined at this point by the macro and only available within this code block
        SKAction *scale = [SKAction scaleBy: 1.2 duration: 0.15];
        SKAction *action = ([SKAction sequence:@[scale, scale.reversedAction]]);
        [tile runAction:action];
    })
}

SKLabelNode *t = nil;
IF_LET(SKLabelNode, temp, t, {
    // doesn't run: t was nil
})

t = [SKLabelNode labelNodeWithText:@"some text"];
IF_LET(NSObject, temp, t, {
    // this block runs, letting you do stuff with the local scope variable
    // NSObject *temp = (NSObject*)t;
}) else {
    // else block available for use, but
    // this one doesn't run because t is not nil and is a kind of NSObject
}

IF_LET(NSArray, temp, t, {
    // doesn't run, t is of wrong type
})

How can I improve this macro?

\$\endgroup\$
1
\$\begingroup\$

My suggestion would be: Don't do it. The main disadvantages (in my opinion) are:

  • As you noticed, commas inside the macro argument are not handled, leading to "strange" error messages:

    for (SKNode *node in touchedNodes) {
        IF_LET(Tile, tile, node, { 
            // Use of undeclared identifier 'IF_LET'
            SKAction *scale = [SKAction scaleBy: 1.2 duration: 0.15];
            SKAction *action = [SKAction sequence:@[scale, scale.reversedAction]];
            // Too many arguments provided to function-like macro invocation
            [tile runAction:action];
        })
    }
    

    It is not obvious how this must be fixed, and enclosing all Objective-C message calls feels unnatural.

  • Macros are not known to the debugger. As a consequence, you cannot set a breakpoint at a certain line inside the block – the debugger will stop at the end of the block instead.

  • For the same reason you cannot single-step through the block statements.

Some of your intended applications have alternatives in pure Objective-C, which are (almost) as good.

Example 1: Your code

for (SKNode *node in touchedNodes) {
    IF_LET(Tile, tile, node, {
        SKAction *scale = [SKAction scaleBy: 1.2 duration: 0.15];
        SKAction *action = ([SKAction sequence:@[scale, scale.reversedAction]]);
        [tile runAction:action];
    })
}

can be replaced by

for (Tile *tile in touchedNodes) {
    if ([tile isKindOfClass:Tile.class]) {
        SKAction *scale = [SKAction scaleBy: 1.2 duration: 0.15];
        SKAction *action = ([SKAction sequence:@[scale, scale.reversedAction]]);
        [tile runAction:action];
    }
}

The fast-enumeration for statement does not care how about the type of the loop variable, you only have to verify its class before accessing it.

Example 2: Your code

SKLabelNode *t = nil;
IF_LET(SKLabelNode, temp, t, {
    // doesn't run: t was nil
    [temp setText:@"text"];
})

can be replaced by (the even shorter)

SKLabelNode *t = nil;
[t setText:@"text"];

because sending Objective-C message to nil is a no-op.

Example 3: Your code

t = [SKLabelNode labelNodeWithText:@"some text"];
IF_LET(NSObject, temp, t, {
    // this block runs, letting you do stuff with the local scope variable
    // NSObject *temp = (NSObject*)t;
}) else {
    // else block available for use, but
    // this one doesn't run because t is not nil and is a kind of NSObject
}

is equivalent to

t = [SKLabelNode labelNodeWithText:@"some text"];
if (t != nil) {
    // Local scope for if-case
} else {
    // Local scope for else-case
}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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