A couple of ideas about collapsing nested if blocks

One feature I would like to see in Swift is the ability to collapse trailing nested braces. The most common real-world example I can think of is the infamous "flying wedge" pattern:

if expr1 {

    if expr2 {

        if expr3 {
            ...
        }
    }
}

(some might suggest that refactoring is the proper solution here, but sometimes this is the clearest and easiest way to express an algorithm).

If would be nice if there were someway to collapse all though trailing braces. For example, here's a possible suggestion:

if expr1
{*
    if expr2 {
    if expr3{
           ...
*}

This feature would also be useful when using trailing closures. For example, I have a pattern matching library (similar to RegEx builder, but it allows context-free languages and other features).

match(someStr)
{
     patFunc(parm)
     {
         patFunc2( parm )
         {
            ..
          }
      }
}

It would be nice to just write:

match(someStr)
{*
     patFunc(parm)
     {
         patFunc2( parm )
         {
            ..
*}

Comments?

I’ll be honest. Im not a big fan of this as it makes it less clear when scope ends.

That could solved by only allowing this when you have the “only closing brackets case”:

         }
    }
}

But that would make the number of cases where this proposal would apply much smaller.

Indeed, refactoring is probably in many cases the better solution.

3 Likes

The Lisp community already solved this problem a long time ago. The correct indentation style is actually this:

if expr1 {
    if expr2 {
        if expr3 {
            ... }}}
6 Likes

Ya gotta have all the braces so that you can command-hover. But you don't need separate lines for them. The code folding ribbon is for a quick view; the hovering is more surgical.

1 Like

Hovering is great for small constructs, but as they get larger and the corresponding opening brace is off the top of the screen, it becomes problematic. Some might argue that should code should be refactored and broken down into smaller pieces, however, that is not always the case. Consider the following implementation of a "factor" in an expression parser (using a pattern matching library I've developed):

//----------------------------------------------
//
// Expression-
//
// Parser for an arithmetic expression parser.
// Demonstrates how to extend the Pattern class.
//
// Grammar:
//
// Expr   -> addOp
//
// addOp  -> mulOp addop'
// addOp' -> + mulOp addOp'
//        -> - mulOp addOp'
//        -> [empty]
//
// mulOp  -> factor mulOp'
// mulOp' -> * factor mulOp'
//        -> / factor mulOp'
//        -> [empty]
//
// factor -> value
//        -> - factor
//        -> + factor
//        -> ( Expr )
//
// For more information on this
// grammar, see "Compilers: Principles,
// Techniques, and Tools" by Aho, Sethi,
// and Ullman (the "Dragon Book").

public class Expression: Pattern
{
   // exprStk holds temporary values
   // during expression parsing:
   
   var   exprStk = Stack<Double>()

  
   // factor-
   // Handles floating-point literal constants
   // and parenthetical expressions.
   //
   // Note: the parenthetical expressions
   // are the magic part of this example.
   // This demonstrates recursion, meaning
   // the pattern-matching code supports
   // context-free languages, making this
   // pattern matching library more powerful
   // than the standard Swift regular expression
   // library (RegexBuilder).
   //
   // Handles the productions:
   //
   // factor -> value
   //        -> - factor
   //        -> + factor
   //        -> ( Expr )

   
   open func factor( _ tail: @escaping PatClosure = {true} )->Bool
   {
      let saveStart  = startIndex
      let saveEnd    = endIndex
      var result     = false
      let val        = Ref(.dbl)
      val.d          = 0.0

      
      assert(
            startIndex >= matchStr.startIndex
         && startIndex <= matchStr.endIndex
         && endIndex   >= matchStr.startIndex
         && endIndex   <= matchStr.endIndex,
         "factor: String index bounds error"
      )
       
       result =
         zeroOrMore(.whitespace)
         {
            // This code demonstrates alternation using
            // both Boolean expressions and the choiceOf
            // method. Alternative 1 uses a nested
            // alternative (via choiceOf).
            
            // Alternative 1: a floating-point value:
            
            floatingPoint( val )
            {
               exprStk.push( val.d )
               return !failFence && tail() && !failFence
            }
               
            // Alternative 2: a parenthetical expression:
            
            || c("(")
               {
                  let value=Ref(.dbl)
                  let result =
                     expr( value )
                     {
                        zeroOrMoreWS(){
                           c(")")
                           {
                              exprStk.push( value.d )
                              return tailGate( tail )
                           }
                        }
                     }
                  return result
               }
               
            // Alternative 3: negation operation:
            
            || c("-")
               {
                  let value=Ref(.dbl)
                  let result =
                     zeroOrMoreWS()
                     {
                        factor()
                        {
                           value.d = self.exprStk.pop()!
                           self.exprStk.push( -value.d )
                           return self.tailGate( tail )
                        }
                     }
                  return result
               }
               
            // Alternative 3: '+' prefix operation:
            
            || c("+")
               {
                  let result =
                     zeroOrMoreWS()
                     {
                        factor()
                        {
                           return self.tailGate( tail )
                        }
                     }
                  return result
               }
               
                  
         }
         
      return restoreIndexes(
         result,
         saveStart,
         saveEnd
      )
   } // end faster

// Other non-terminals handled down here, snipped for conciseness.

In pattern matching code using this scheme, each pattern matching function (which has a trailing closure) introduces a new closing parethesis at the end. In some patterns I've created (granted, expr is not one of them), I've had a dozen or more trailing closing braces. Furthermore, unlike nested statements, indentation isn't always an appropriate style -- these are not nested statements (I typical add indentation only to make it easier to keep track of the closing braces, not to indicate any structure in the algorithm).
For example, here's a pattern that matches strings from the regular language ( ^a{4}[aA][aA]{3}$)

   p = Pattern()
   pResult  :Bool =
      p.match( "aaaaAAAA" ){
        p.nChar( "a",  n:4 ) {
        p.nChari( "a", n:1 ) {
        p.nChari( "a", n:3 ) {
        p.eos() // Note: default trailing closure is {true} here
        }}}
      }

Dealing with the trailing braces becomes problematic; just because you have to count the braces at the end. It would be much nicer to write something like:

   p = Pattern()
   pResult  :Bool =
      p.match( "aaaaAAAA" ){*
        p.nChar( "a",  n:4 ) {
        p.nChari( "a", n:1 ) {
        p.nChari( "a", n:3 ) {
        p.eos() // Note: default trailing closure is {true} here
    *}

FWIW, each pattern matching function returns true if it, and every following function, succeeds. The reason for the trailing closures is to allow backtracking via recursive calls.

What if you define an operator so that

f { g { h } }

becomes

f |> g |> h
1 Like

Classic set of libraries that helped with JSON decoding before Codable defined similar operators. (As well as a curry function which is a hilarious stress test for the compiler.)

Even if you're not using the code folding ribbon to visualize where braces match up, which is Xcode's equivalent of Visual Studio's indentation lines, you can still collapse the code.

The only catch there is that you need something to mark the end of the construct.

Though I wrote:

 p = Pattern()
   pResult  :Bool =
      p.match( "aaaaAAAA" ){*
        p.nChar( "a",  n:4 ) {
        p.nChari( "a", n:1 ) {
        p.nChari( "a", n:3 ) {
        p.eos() // Note: default trailing closure is {true} here
    *}

it is perfectly legal to insert other non-pattern matching function calls in this sequence (indeed, this is one of the big advantages over RegEx Builder):

 p = Pattern()
   pResult  :Bool =
      p.match( "aaaaAAAA" ){*
        p.nChar( "a",  n:4 ) {
        p.nChari( "a", n:1 ) {
        p.nChari( "a", n:3 ) {
        p.eos(){
        // Add a semantic action here:
        print( "matched string" );
    *}

Without some trailing lexeme, the compiler couldn't differentiate this semantic action from code following the p.match() sequence.

Yeah but then we invented &&.