MrPowerShell

The Joy Of Loop Labels

The Joy Of Loop Labels

Why you might want to use loop labels.

Loop labels are a powerful and underused part of many programming languages.

In PowerShell, they take the form :name, and can come before any loop statement

:OneIn1kb foreach ($n in 1..1kb) { $n }

Loops do not need to have labels, but I'm starting to think that it's pretty darn good practice to include them.

Let's look at two major reasons for why:

Performance

Let's start with the real reason most languages have loop labels of some form.

Performance.

Imagine we want to enforce multiple exclusions of some condition.

Let's write it without loop labels first

foreach ($file in $files) {
    if ($exclude) {
        $included = $true
        foreach ($exclusion in $Exclude) {
            if ($file.FullName -like $exclusion) {
                $included = $false                    
            }                        
        }
        if (-not $included) {
            break
        }
    }
    $file
}

There are actually two performance problems with this code.

The first is that we are "over-looping" thru exclusions.

After all, if the file is excluded, it's excluded.

It does not matter if it is excluded because it matches five wildcards or one.

That file is excluded.

We can improve the code a little bit by breaking out of the inner loop.

foreach ($file in $files) {
    if ($exclude) {
        $included = $true
        foreach ($exclusion in $Exclude) {
            if ($file.FullName -like $exclusion) {
                $included = $false
                break
            }                        
        }
        if (-not $included) {
            break
        }
    }
    $file
}

This code is a little faster, but it still feels messy.

See how we're using one loop to break out of another?

We're also using just a few extra cycles to set a variable, update it, check it, and jump out of the main loop.

Now let's see how we can get a faster and cleaner exclusion with a loop label:

:nextFile foreach ($file in $files) {
    if ($exclude) {            
        foreach ($exclusion in $Exclude) {
            if ($file.FullName -like $exclusion) {
                continue nextFile
            }                        
        }            
    }
    $file
}

Isn't that much cleaner?

Now the moment we find a file has been excluded, we continue onto the next file.

Less code, fewer variables, and better performance!

There's one more benefit this brings, and it's the real reason I felt compelled to write this article.

Readability

Instead of such a short loop, let's imagine a bigger one.

Let's imagine we're doing tons of things in each step of the loop, including a few nested loops.

foreach ($file in $files) {
    
    DoStuff        
    
    DoMoreStuff
    DoMoreStuff
    DoMoreStuff
    
    DoEvenMoreStuff
    DoEvenMoreStuff
    DoEvenMoreStuff
    DoEvenMoreStuff

    foreach ($line in Get-Content $file) {
        DoSomethingWithEachLine
    }

    if ($file.Length % 2) {
        continue
    }
}

The further down we want to break or continue out of the loop, the harder this gets to follow.

The more inner loops we have, the harder this gets to follow.

When we're trying to read our code and figure out how it's working, this isn't exactly helpful.

We have to keep thinking "where am I?" and "where is this going?"

If we gave things a label instead, it becomes a lot more easy to see:

:nextFile foreach ($file in $files) {
    
    DoStuff        
    
    DoMoreStuff
    DoMoreStuff
    DoMoreStuff
    
    DoEvenMoreStuff
    DoEvenMoreStuff
    DoEvenMoreStuff
    DoEvenMoreStuff

    foreach ($line in Get-Content $file) {
        DoSomethingWithEachLine
    }

    if ($file.Length % 2) {
        continue nextFile
    }
}

Because we have a logical loop label, instead of a raw break or continue, we know where we are going.

The larger your loop, the more useful this will be.

The more inner loops you have, the more useful loop labels will be.

As a good rule of thumb, I think you should consider adding loop labels when:

Hopefully this short post helps everyone see the Joy of Loop Labels.

Hope this Helps!


View Source
<#
.SYNOPSIS
    The Joy Of Loop Labels
.DESCRIPTION
    Why you might want to use loop labels.
.NOTES
    [Loop labels](https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_break?wt.mc_id=MVP_321542#using-a-labeled-break-in-a-loop) 
    are a powerful and underused part of many programming languages.

    In PowerShell, they take the form :name, and can come before any loop statement

    ~~~PowerShell
    :OneIn1kb foreach ($n in 1..1kb) { $n }
    ~~~

    Loops do not need to have labels, but I'm starting to think that it's pretty darn good practice to include them.

    Let's look at two major reasons for why:

    ### Performance

    Let's start with the _real_ reason most languages have loop labels of some form.

    Performance.

    Imagine we want to enforce multiple exclusions of some condition.

    Let's write it without loop labels first

    ~~~PowerShell
    foreach ($file in $files) {
        if ($exclude) {
            $included = $true
            foreach ($exclusion in $Exclude) {
                if ($file.FullName -like $exclusion) {
                    $included = $false                    
                }                        
            }
            if (-not $included) {
                break
            }
        }
        $file
    }
    ~~~

    There are actually two performance problems with this code.

    The first is that we are "over-looping" thru exclusions.

    After all, if the file is excluded, it's excluded.
    
    It does not matter if it is excluded because it matches five wildcards or one.

    That file is excluded.

    We can improve the code a little bit by breaking out of the inner loop.

    ~~~PowerShell
    foreach ($file in $files) {
        if ($exclude) {
            $included = $true
            foreach ($exclusion in $Exclude) {
                if ($file.FullName -like $exclusion) {
                    $included = $false
                    break
                }                        
            }
            if (-not $included) {
                break
            }
        }
        $file
    }
    ~~~
    

    This code is a little faster, but it still feels messy.
    
    See how we're using one loop to break out of another?

    We're also using just a few extra cycles to set a variable, update it, check it, and jump out of the main loop.

    Now let's see how we can get a faster and cleaner exclusion with a loop label:

    ~~~PowerShell
    :nextFile foreach ($file in $files) {
        if ($exclude) {            
            foreach ($exclusion in $Exclude) {
                if ($file.FullName -like $exclusion) {
                    continue nextFile
                }                        
            }            
        }
        $file
    }
    ~~~

    Isn't that much cleaner?

    Now the moment we find a file has been excluded, we continue onto the next file.

    Less code, fewer variables, and better performance!

    There's one more benefit this brings, and it's the real reason I felt compelled to write this article.

    ### Readability

    Instead of such a short loop, let's imagine a bigger one.

    Let's imagine we're doing tons of things in each step of the loop, including a few nested loops.

    ~~~PowerShell
    foreach ($file in $files) {
        
        DoStuff        
        
        DoMoreStuff
        DoMoreStuff
        DoMoreStuff
        
        DoEvenMoreStuff
        DoEvenMoreStuff
        DoEvenMoreStuff
        DoEvenMoreStuff

        foreach ($line in Get-Content $file) {
            DoSomethingWithEachLine
        }

        if ($file.Length % 2) {
            continue
        }
    }
    ~~~

    The further down we want to break or continue out of the loop, the harder this gets to follow.
    
    The more inner loops we have, the harder this gets to follow.

    When we're trying to read our code and figure out how it's working, this isn't exactly helpful.

    We have to keep thinking "where am I?" and "where is this going?"

    If we gave things a label instead, it becomes a lot more easy to see:

    ~~~PowerShell
    :nextFile foreach ($file in $files) {
        
        DoStuff        
        
        DoMoreStuff
        DoMoreStuff
        DoMoreStuff
        
        DoEvenMoreStuff
        DoEvenMoreStuff
        DoEvenMoreStuff
        DoEvenMoreStuff

        foreach ($line in Get-Content $file) {
            DoSomethingWithEachLine
        }

        if ($file.Length % 2) {
            continue nextFile
        }
    }
    ~~~

    Because we have a logical loop label, instead of a raw `break` or `continue`, we know where we are going.
    
    The larger your loop, the more useful this will be.

    The more inner loops you have, the more useful loop labels will be.

    As a good rule of thumb, I think you should consider adding loop labels when:

    * It makes the code quicker.
    * You use loops within loops.
    * The loop is more than a dozen lines long.

    Hopefully this short post helps everyone see the Joy of Loop Labels.

    Hope this Helps!

    ---
#>
param()

# Get my help
$myHelp = Get-Help $MyInvocation.MyCommand.ScriptBlock.File

# My synopsis becomes the page title
$title = $myHelp.Synopsis
# and my description becomes the page description.
$description = $myHelp.Description.text -join [Environment]::NewLine
# My notes are in markdown.
$notes = $myHelp.alertset.alert.text -join [Environment]::NewLine

# If we have page metadata, copy title and description
if ($page -is [Collections.IDictionary]) {
    $page.Title = $title
    $page.Description = $description
}


# Make one big markdown out of our title, description, and notes
@"
# $($title)

## $($description)

$notes
"@ | 
    # convert it from markdown
    ConvertFrom-Markdown |
    # and output the HTML
    Select-Object -ExpandProperty Html


#region View Source 
# Provide the source within a `<details>` element.
"<details>"
"<summary>View Source</summary>"
"<pre><code class='language-powershell'>$([Web.HttpUtility]::HtmlEncode($MyInvocation.MyCommand.ScriptBlock))</code></pre>"
"</details>"
#endregion View Source