Jump to content
 

DRY and Lambda Functions


N9WXU
 Share

Recommended Posts

  • Member

One important rule for good software is Do not Repeat Yourself.  This rule is often referred to under the acronym DRY.  Most of the time, your code can be made DRY by refactoring repeating blocks of code into a function.

This is a good practice but it can lead to a lot of small functions.  You undoubtedly will keep your API's tidy by making these functions static and keeping them close to where they are needed but I have recently been working on a lot of C++ code and I have a new tool in my programming toolbox that I would like to share with you.  The LAMBDA function.

Essentially a lambda is a function that you define inside of another function.  This lambda function has function scope so it can only be used in the function that defines it.  This can be very useful and will do two things to help keep your code maintainable.

  1. It keeps your code readable by forcing you to define a function close to where it is needed.
  2. It encourages you to keep the function short & sweet because you will not be tempted to make it a "general purpose solution".

Here is an example.

I was tasked to implement a serializer where data would be sent on the serial port.  This was a binary protocol and it included a special character for start of frame (SOF) and end of frame (EOF).  Because the SOF and EOF characters could appear in the actual data, there was an additional data link escape (DLC) character sequence that would expand into the SOF, EOF and DLC.  For added fun, there is a checksum that is generated BEFORE the character padding.

<SOF><DATA><CHECKSUM><EOF>

Here is a function that can decode this message.

#define MAXIMUM_MESSAGE_SIZE 255

void dataLink::receiveData(char b)
{
    const char SOF = 0x1A;
    const char EOF = 0x1B;
    const char DLC = 0x1C;

    static enum  {findSOF, getData} theState;
    static int messageIndex=0;
    static char checksum = 0;
    static char receivedMessage[MAXIMUM_MESSAGE_SIZE];

    switch(theState)
    {
        case findSOF:
            if(b == SOF)
            {
                theState = getData;
                messageIndex = 0;
                checksum = 0;
                memset(receivedMessage,0,sizeof(receivedMessage));
            }
            break;
        case getData:
        {
            static bool dlc_last = false;
            if(dlc_last)
            {
                dlc_last = false;
                switch(b)
                {
                    case 1:
                        receivedMessage[messageIndex++] = 0x1A;
                        checksum += 0x1A;
                        break;
                    case 2:
                        receivedMessage[messageIndex++] = 0x1B;
                        checksum += 0x1B;
                        break;
                    case 3:
                        receivedMessage[messageIndex++] = 0x1C;
                        checksum += 0x1C;
                        break;
                }
            }
            else
            {
                switch(b)
                {
                    case EOF:
                        theState = findSOF;
                        if(checksum == 0)
                        {
                            //*********************
                            // Do something with the new message
                            //*********************
                        }
                        break;
                    case DLC:
                        dlc_last = true;
                        break;
                    default:
                        receivedMessage[messageIndex++] = b;
                        checksum += b;
                        break;
                }
            }            
            break;
        }
    }
}

This function receives a byte and using a few states, creates a checksum validated array of decoded bytes representing the message.  I will not explain each line as the details of this function are really not very important.  As my code reviewer you should instantly notice that there are 4 sections of nearly identical code that are repeated.  In other words, this is not DRY.  My first inclination would be to attempt to reorder the decisions so the update of the message array and checksum was done once.  This method of course works quite well in this case but I wanted a simple contrived example to show off the lambda function.

#define MAXIMUM_MESSAGE_SIZE 255

void dataLink::receiveData(char b)
{
    const char SOF = 0x1A;
    const char EOF = 0x1B;
    const char DLC = 0x1C;

    static enum  {findSOF, getData} theState;
    static int messageIndex=0;
    static char checksum = 0;
    static char receivedMessage[MAXIMUM_MESSAGE_SIZE];

    auto output_byte = [&](char b) { receivedMessage[messageIndex++] = b; checksum += b; };

    switch(theState)
    {
        case findSOF:
            if(b == SOF)
            {
                theState = getData;
                messageIndex = 0;
                checksum = 0;
                memset(receivedMessage,0,sizeof(receivedMessage));
            }
            break;
        case getData:
        {
            static bool dlc_last = false;
            switch(b)
            {
                case EOF:
                    theState = findSOF;
                    if(checksum == 0)
                    {
                        //*********************
                        // Do something with the new message
                        //*********************
                    }
                    break;
                case DLC:
                    dlc_last = true;
                    break;
                default:
                    if(dlc_last)
                    {
                        dlc_last = false;
                        switch(b)
                            case 1: output_byte(0x1A); break;
                            case 2: output_byte(0x1B); break;
                            case 3: output_byte(0x1C); break;
                    }
                    else
                    {
                        output_byte(b);
                    }
                    
                    break;
            }
            break;
        }
    }
}

Now you can see that I moved the work of inserting the byte into the array and updating the checksum into a function called output_byte.  This function is defined inside the receiveData function.  The syntax has a square bracket followed by parenthesis.  The Ampersand inside the brackets indicates that the function has access to all the variables inside receiveData.  This makes the function very simple and easy to verify by inspection.

Of course you could have made the output_byte function a private member function of the class.  But you would have needed to add the checksum and the index variables to the class as well.  That increases the complexity of the class.  By using the lambda, the function can be made DRY, and readable, and the function does not leak information or variables into any other code.  This makes the function much simpler to maintain or potentially refactor in the future.

BTW, I tested this function by building the project in the Arduino environment on a SAMD21.  The actual Arduino IDE is very limited but when you use VIsual Studio Code and PlatformIO you get full debug on an ATMEL ICE with the Arduino frameworks.  This makes developing interesting applications VERY fast.

lambda_demo.zip

Link to comment
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.

 Share

 


×
×
  • Create New...