• Let functions with only one variable declaration can be defined on one line.

    Let ( ~someVar = FunctionalArea » Tablename::fieldName ; If ( ~someVar = "Active"; True; False ) )
  • Example #1 Let function with indenting - Brackets on dedicated lines.

    Let (
            [
                ~privateVariable = List ( "one" ; "two" ; "three" );
                $localVariable = Substitute ( ~privateVariable ; [ ¶ ; "," ] );
                $$globalVariableTopValue = GetValue ( ~privateVariable ; 1 )
            ];
                "Your Let function result is " & $localVariable
    )
  • Example #2 Let function with indenting - Initial bracket on same line as Let function with inline notes.

    Let ( [
                // Notes about variables below
                ~privateVariable = List ( "one" ; "two" ; "three" );
                $localVariable = Substitute ( ~privateVariable ; [ ¶ ; "," ] );
     
                /*
                  Use extra lines and embedded block comments
                  if more information is needed to describe
                  what is going on with the calculation logic!
                */
                $$globalVariableTopValue = GetValue ( ~privateVariable ; 1 )
            ];
                "Your Let function result is " & $localVariable
    )
  • Let functions with multiple variables use both opening and closing brackets on their own lines
    Note: both opening and closing brackets should be indented to stand out.

    Let (
            [
                variable = expression
            ];
                "result is indented 2 tabs"
    )
    

    good

    Let ( [

    acceptable

    Let (
    [
    

    bad

    Because the standard started out with the opening bracket on the same line as the function name, yet does not impede readability, it can be considered the shorthand version and perfectly acceptable.

  • Closing Let variable declarations end on their own line. This indicates the start of the result.

    ];

    good

    endOfFunction ) ];

    bad

  • Calculation scoped variables use camelCase and are identified by a preceding variable indicator of ~ (tilde). The ~ character in these standards represents the private scope.
    This makes it easy to distinguish calculation variables from custom function arguments, $variables, and Table::fieldNames

    ~someVariable

    good

    someVariable

    bad

  • Use present tense verbs or adjectives to indicate Boolean variable status on both calculation and locally scoped variables.

    $hasReturns
    ~isTrailing
    $containsSpaces
    not ~containsEmailAddress
    
  • No labels

23 Comments

  1. Anonymous

    I disagree with placing the bracket for multi-variable let statements on the same line as the function name. With very large calculations, the extra empty line with nothing but an opening bracket at the start creates a unique and recognizable shape distinct from any other function, making it easier to distinguish and identify variable declarations in Let() statements, as opposed to arguments and results from other functions. Let() is worth that distinction.

    1. I'm willing to debate this one. Let's get some code in here and take a look. Does anyone have a mega long function with multiple Lets and embedded Lets?

      Personally, I try to keep my code as short and modular as possible. When considering this option, I determined that the quick identification of the word Let was indication enough that it started the variable declaration. It was where it ended that was the problem.

      I do agree with line spaces, I just wonder if adding one actually adds that much when you've already determined you're in a Let statement and you know that the first part is declaration.

      1. I've got a function for ya. It's big, but it doesn't have Lets embedded in other Lets; you can't have everything. Here's with opening Let brackets on one line:

        Case (
        	//first call to UUID.New, set-up NICAddress for parsing
        	not $UUID.step ;
        		Let ( [
        			$UUID.NICAddress = Substitute ( GetValue ( Get ( SystemNICAddress ); 1 ) ; ":" ; "" ) ;
        			var.NICIsRandom = not Length ( $UUID.NICAddress );
        			$UUID.node =
        				Case(
        					not var.NICIsRandom ; 0;
        					/*else*/ Right ( Random ; 15 ) //use random number for NICAddress when a real one is unavailable
        				);
        			$UUID.i = 0;
        			$UUID.step =
        				Case(
        					not var.NICIsRandom ; 1; //Parse NICAddress next
        					/*else*/ 2 //Skip to concatenating information
        				)
        		];
        			UUID.New ( tableID )
        		);
        
        	//Parse NICAddress from hexadecimal to a FileMaker number
        	$UUID.step = 1;
        		Let ( [
        			$UUID.NICDigit = Middle ( $UUID.NICAddress ; 12 - $UUID.i ; 1 );
        			$UUID.NICDigit = Position ( "0123456789abcdefg" ; $UUID.NICDigit ; 1 ; 1 ) - 1; //convert digit to number
        			$UUID.node = $UUID.NICDigit * (16 ^ $UUID.i) + $UUID.node; //add digit to number
        			$UUID.step =
        				Case(
        					$UUID.i < 11; //not done parsing NICAddress
        					1; //keep parsing
        
        					2 //else, advance to next step
        				) ;
        			$UUID.i = $UUID.i +1
        		];
        			UUID.New ( tableID )
        		);
        
        	//Concatenate information
        	$UUID.step = 2;
        		Let ( [
        			var.timestamp = GetAsNumber ( Get ( CurrentHostTimeStamp ) );
        			var.tableID = Filter ( tableID ; "0123456789" );
        			var.tableID =
        				Case(
        					var.tableID > 0 ; Mod ( var.tableID ; 125 ); //% 5^3
        					/*else*/ Mod ( Right ( Random ; 3 ) ; 125 ) //use a random tableID when none is provided
        				); //end Case()
        			var.recordID = Get ( RecordID );
        			var.recordID =
        				Case(
        					var.recordID ≠ 0 ; Mod ( var.recordID ; 78125 ); //% 5^7
        					/*else*/ Mod ( Right ( Random ; 5 ) ; 78125 ) //use a random recordID when none is available
        				);
        			var.NICAddress = Right ( "000000000000000" & $UUID.node ; 15 );
        
        			//clear variables
        			$UUID.NICAddress = "";
        			$UUID.node = "";
        			$UUID.i = "";
        			$UUID.step = "";
        			$UUID.NICDigit = ""
        		];
        			var.timestamp & "-" &
        			var.NICAddress & "-" &
        			Right ( "0000000" & (var.tableID * 78125 + var.recordID) ; 7 )
        		)
        )
        

        And here's with opening Let brackets on the next line:

        Case (
        	//first call to UUID.New, set-up NICAddress for parsing
        	not $UUID.step ;
        		Let (
        		[
        			$UUID.NICAddress = Substitute ( GetValue ( Get ( SystemNICAddress ) ; 1 ) ; ":" ; "" );
        			var.NICIsRandom = not Length ( $UUID.NICAddress );
        			$UUID.node =
        				Case(
        					not var.NICIsRandom ; 0;
        					/*else*/ Right ( Random ; 15 ) //use random number for NICAddress when a real one is unavailable
        				);
        			$UUID.i = 0;
        			$UUID.step =
        				Case(
        					not var.NICIsRandom ; 1; //Parse NICAddress next
        					/*else*/ 2 //Skip to concatenating information
        				)
        		];
        			UUID.New ( tableID )
        		);
        
        	//Parse NICAddress from hexadecimal to a FileMaker number
        	$UUID.step = 1;
        		Let (
        		[
        			$UUID.NICDigit = Middle ( $UUID.NICAddress ; 12 - $UUID.i ; 1 );
        			$UUID.NICDigit = Position ( "0123456789abcdefg" ; $UUID.NICDigit ; 1 ; 1 ) - 1; //convert digit to number
        			$UUID.node = $UUID.NICDigit * (16 ^ $UUID.i) + $UUID.node; //add digit to number
        			$UUID.step =
        				Case(
        					$UUID.i < 11; //not done parsing NICAddress
        					1; //keep parsing
        
        					2 //else, advance to next step
        				);
        			$UUID.i = $UUID.i +1
        		];
        			UUID.New ( tableID )
        		);
        
        	//Concatenate information
        	$UUID.step = 2;
        		Let (
        		[
        			var.timestamp = GetAsNumber ( Get ( CurrentHostTimeStamp ) );
        			var.tableID = Filter ( tableID ; "0123456789" );
        			var.tableID =
        				Case(
        					var.tableID > 0 ; Mod ( var.tableID ; 125 ); //% 5^3
        					/*else*/ Mod ( Right ( Random ; 3 ) ; 125 ) //use a random tableID when none is provided
        				); //end Case()
        			var.recordID = Get ( RecordID );
        			var.recordID =
        				Case(
        					var.recordID ≠ 0 ; Mod ( var.recordID ; 78125 ); //% 5^7
        					/*else*/ Mod ( Right ( Random ; 5 ) ; 78125 ) //use a random recordID when none is available
        				);
        			var.NICAddress = Right ( "000000000000000" & $UUID.node ; 15 );
        
        			//clear variables
        			$UUID.NICAddress = "";
        			$UUID.node = "";
        			$UUID.i = "";
        			$UUID.step = "";
        			$UUID.NICDigit = ""
        		];
        			var.timestamp & "-" &
        			var.NICAddress & "-" &
        			Right ( "0000000" & (var.tableID * 78125 + var.recordID) ; 7 )
        		)
        )
        

        The key thing to remember is that, with large calculations, you're probably navigating the code by visual shape as much as verbal content. Can you still identify the variables in a Let statement when you're scanning too fast to read the word "Let"? Let() already has a pretty unique shape with a middle delimiter at the same indent level as the open and close of the function:

        ZZZ ( [
        	asdlkfjh...
        ];
        	qwpoeriu...
        ) &
        
        Case (
        	azxmznxbcv;
        		alskdjfh...
        	//default
        		qwoeiruy...
        ) &
        
        
        If( asdlfkj;
        	aslkdfjh;
        	/*else*/ asdkljfh
        ) &
        
        YYY (
        [
        	aslkdjfh...
        ];
        	mnzbxvc...
        )
        
        

        I think it isn't really worth standardizing either placement for the opening bracket, since it's the middle bracket that distinguishes Let() so clearly from other functions. That said, if we decide that we really do want to standardize one form over the other, having the opening bracket on the following line distinguishes a Let() function marginally more.

        1. Aha - here's the thread I was looking for!

          I'm seconding Jeremy's recommendation for having the inner brackets for Let () to be on separate lines with matching indentation levels (one deeper than the Let () declaration). This is mostly for getting TextMate to do proper code folding, but it also helps distinguish the code areas as two separate entities as they really are in practice.

          1. I've not had code so long in FileMaker that I've needed folding personally, but I'm game to join consensus.

            I think the indent makes more sense in the amount of white spaces is creates. Other languages don't worry about white space as long as the code is readable.

            I'll update the page. Also, Perren, if you forked the bundle on github then just let me know if you want a pull. I'll look at your fork changes and get them in when I have the time.

  2. A Let function should nearly always contain a Case () function. This allows the two areas of the function to be more easily reused, as many of the definitions will be common to many Let () functions, and likewise (perhaps much less so) for the Case code. Perhaps structure like this:

    Let ( [

      cmp = C__Company::nameCompany ; 

      per = C_Person::cFirstLast 

    ] ;

    Case (

      not isempty (cmp) and not isempty (per) ; cmp & " " & per ; 

      not isempty (cmp) ; cmp ; 

      not isempty (per) ; per ;

      "Broken"

    ) // end Case

    ) // end Let

    1. Your suggestion sounds more like a preference than a convention. We can start a new space on the wiki for suggested solutions to common issues.

      Also, one minor gripe. If you're going to follow the conventions outlined here, can you post code using the conventions? I know most of us are going to take code from solutions we are working on. But the goal here is consistency and clarity. (smile)

      per and cmp are shorthand code for yourself because you are the one familiar with your solution. If you wanted me to "open and understand" your solution then you need to be more verbose.

      Let ( [
      
        var.company = FunctionalArea » Company::nameCompany; 
        var.person = FunctionalArea » Person::cFirstLast 
      
      ];
      
        Case (
          not isempty ( var.company ) and not isempty ( var.person );
            var.company & " " & var.person; 
      
          not isempty ( var.company );
            var.company; 
      
          not isempty ( var.person );
            var.person ;
      
          "Broken"
      
        ) // end Case
      
      ) // end Let
      

      Note: for posting code you can use the {noformat} wrapped around code.

  3. In attempt to make code even easier to scan. I'm wondering if using the @ symbol might not be helpful.

    It's VERY easy to distinguish and clearly identifies a calculation scoped variable. Here's some complex code to compare...

    Before

    Let( [
      $$ERROR.VAR.EVAL = ""; // Reset global error variable for evaluation
    
      var.empty = IsEmpty ( contents ); // test for empty contents
      var.semicolons_exist = ( ValueCount ( contents ) = PatternCount ( contents ; ";¶" ) ) or ( ValueCount ( contents ) - 1 = PatternCount ( contents ; ";¶" ) );
      var.contents = If ( not var.semicolons_exist ; Substitute ( contents ; ¶ ; ";¶") ; contents ); // Add semicolons if needed
      var.trailing_semicolon = Right ( var.contents ; 1 ) = ";"; // Clean off the trailing semicolon if present
      var.contents = If ( var.trailing_semicolon; Left ( var.contents ; Length( var.contents ) - 1 ); var.contents );
    
      var.script = Get ( ScriptName );
      var.begin  = "(" ; // beginning of parameters definition
      var.break  = "," ; // regular parameters separator (and)
      var.end    = ")" ; // end of parameters definition
    
      var.parameters = Middle ( var.script ;
                         Position ( var.script ; var.begin ; 1 ; 1 ) +1;
                         Position ( var.script ; var.end ; Length ( var.script ) ; -1 ) - Position ( var.script ; var.begin ; 1 ; 1 ) -1
                      ); // raw parameters
      // ONLY ALLOWED CHARACTERS FOR SCRIPT NAME PARAMERS ARE THE FOLLOWING (minus the ¶)
      var.parameters = Filter ( Substitute ( var.parameters ; var.break ; ¶ ) ; "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_¶" );
      var.expected = "$" & Substitute ( var.parameters ; ¶ ; "¶$" );
      var.matching = FilterValues ( Substitute( var.contents; "="; ¶ ) ; var.expected ); // matching parameters found in ScriptName
      var.matching = If ( Right ( var.matching; 1 ) = "¶" ; Left ( var.matching ; Length ( var.matching) - 1) ); // replace extra return added by FilterValues function
      var.missingParams = If ( $checkScriptName = True ; var.matching ≠ var.expected ; False )
    ];
    
      If ( Evaluate( "Let ( [" & var.contents & "]; False )" ) = "?" // generates an error or
           or var.empty or var.missingParams; // empty contents or missing expected parameters
    
          // Return error result
          Let ( [
            $$ERROR.VAR.EVAL = Case (
              var.empty;
                "EMPTY PARAMETER SET";
            
              var.missingParams;
                "MISSING EXPECTED PARAMETERS PER SCRIPT NAME";
            
              List ( "ERROR »"; var.contents )
            );
        
                $checkScriptName = "" // reset the $checkScriptName variable (after setting error)
            
            ];
                False
          );
              
          Let ( $checkScriptName = "" ; True )
      )
    
    )
    

    After

    Let( [
      $$ERROR.VAR.EVAL = ""; // Reset global error variable for evaluation
    
      @empty = IsEmpty ( contents ); // test for empty contents
      @semicolons_exist = ( ValueCount ( contents ) = PatternCount ( contents ; ";¶" ) ) or ( ValueCount ( contents ) - 1 = PatternCount ( contents ; ";¶" ) );
      @contents = If ( not @semicolons_exist ; Substitute ( contents ; ¶ ; ";¶") ; contents ); // Add semicolons if needed
      @trailing_semicolon = Right ( @contents ; 1 ) = ";"; // Clean off the trailing semicolon if present
      @contents = If ( @trailing_semicolon; Left ( @contents ; Length( @contents ) - 1 ); @contents );
    
      @script = Get ( ScriptName );
      @begin  = "(" ; // beginning of parameters definition
      @break  = "," ; // regular parameters separator (and)
      @end    = ")" ; // end of parameters definition
    
      @parameters = Middle ( @script ;
                         Position ( @script ; @begin ; 1 ; 1 ) +1;
                         Position ( @script ; @end ; Length ( @script ) ; -1 ) - Position ( @script ; @begin ; 1 ; 1 ) -1
                      ); // raw parameters
      // ONLY ALLOWED CHARACTERS FOR SCRIPT NAME PARAMERS ARE THE FOLLOWING (minus the ¶)
      @parameters = Filter ( Substitute ( @parameters ; @break ; ¶ ) ; "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_¶" );
      @expected = "$" & Substitute ( @parameters ; ¶ ; "¶$" );
      @matching = FilterValues ( Substitute( @contents; "="; ¶ ) ; @expected ); // matching parameters found in ScriptName
      @matching = If ( Right ( @matching; 1 ) = "¶" ; Left ( @matching ; Length ( @matching) - 1) ); // replace extra return added by FilterValues function
      @missingParams = If ( $checkScriptName = True ; @matching ≠ @expected ; False )
    ];
    
      If ( Evaluate( "Let ( [" & @contents & "]; False )" ) = "?" // generates an error or
           or @empty or @missingParams; // empty contents or missing expected parameters
    
          // Return error result
          Let ( [
            $$ERROR.VAR.EVAL = Case (
              @empty;
                "EMPTY PARAMETER SET";
            
              @missingParams;
                "MISSING EXPECTED PARAMETERS PER SCRIPT NAME";
            
              List ( "ERROR »"; @contents )
            );
        
                $checkScriptName = "" // reset the $checkScriptName variable (after setting error)
            
            ];
                False
          );
              
          Let ( $checkScriptName = "" ; True )
      )
    
    )
    

    QUESTION: Does it make to too muddy and hard to scan? After looking at it for a bit it seems to make it harder to read.

    1. I confuse "@foo" naming with commenting conventions from other languages. 

      I made a conscious effort today to use the "var.foo" in a few Let statements and it truly made the world of difference.

      No to "@foo". :)

  4. Anonymous

    I prefer the @foo, always struggled with var. partly because it is three keys on the left and one one the right

    What about #foo??

    1. #foo might get confused with the #-prefixed script parameter functions getting moved to Best Practices. Would _foo work?

      1. Anonymous

        I second the motion. _foo works with me.

  5. Anonymous

    Hi guys:

    I've been having problems with the "var." naming myself as it's often hard to see the variables by just glancing at the code. The underline prefix isn't much better in that respect. I've used the hash for many years and loved it. Recently I moved away from that as, at least here, the hash is becoming a standard for some cf names. So...I went to the bullet (option-8 on the mac). I realize that this is high ASCII and may cause problems on the pc (haven't tested), but it's certainly very easy to see the variables in the code.

    Nick Chapin

    1. In order to make things workable on both Win an Mac, I'd prefer to stay away from any characters that required more than 10 fingers on Windows. (smile)

      I can see that visually picking out var. from within a bunch of code can be initially difficult, but eventually the pattern is easy to pick up. While we've used a good chunk of the available ascii characters, it probably wouldn't hurt to come up with a "shorthand" version of var. for those of us who are lazy coders. (wink)

      I've used the underscore myself as a prefix and it just seemed to make things look really ugly to me - so I personally stopped using it.

      One thing to consider is using the percent %. In other languages, such as PHP, the percent is usually used for substitution within strings. Since calc variables are typically substitutions within code we could switch the current use of % on unstored or display calcs.

      This would mean we would need a prefix for display calcs. They would become something like display.invoiceCount vs. the current %invoiceCount. And a calc would look like this

      Let ( [
      	%field = FunctionalArea » Tablename::fieldName;
      	%date = Get ( CurrentDate );
      	%time = Get ( CurrentTime );
      ];
      	Substitute (
      		%field;
      		[ "[[date]]" ; %date ];
      		[ "[[time]]" ; %time]
      	)
      )
      

      We can also hunt for another character or character combination to use...

      1. No characters that require the Shift key please!

        I've been using "var.foo" for the last couple weeks and personally it's working well. If we start hunting for something that requires keystroke modifiers it should be in areas where a lesser percentage of the day to day coding is done, not inside Let ().

        I could see shortening it to "v.foo" or some other single lower case character no problem.

      2. I like the underscore to identify simple variables, it does not look too ugly for my eyes. The percent % has different meanings in other program languages. E. g. in Visual Basic, the percent sign % is used as a type-declaration character representing an Integer. This might confuse Visual Basic developers, assuming that %date would be a numeric (integer) value.

        I strongly vote against a prefix like var. or similar. The name should describe the content of the variable. Only non-letter prefix should identify the scope of a variable:

        Variable Type

        Scope

        $$VARIABLE

        global (module)

        $variable

        local (script)

        _variable

        minimal *

        For me, the symbolic of the characters point into the right direction: $ = scope within the script ($ looks like S), $$ = scope over multiple scripts, _ = very small scope.

        *) Example for minimal scope:

        Let( [
          x = 1;
          $x = Let( [
              x = 2
            ];
              x
            )
        ];
          x
        )
        

        This calculation will return 1 and $x will have the value 2. This show the narrow scope of simple variables. It is only valid within the same Let function.

        1. Anonymous

          No option? No shift?? I'm with Matt that if it needs ten fingers it's out; heck, I'm even willing to go down to two! But, at least for, me, it's not about the number of characters (and var. is four), but about visually seeing the variables when I open up the Let a week later. Alzheimer's is setting in and the "code readability" as Matt often says is paramount. What other keys has anyone tried extensively and a) thought were easy-to-read (not ugly) and b) easy to type?

          Nick Chapin

  6. Anonymous

    I've been using the underscore for a prefix for a while, find them easy to pick out and quick to type. Symbols with too much in the top half of the character, such as %, make reading much more difficult

    Tim Anderson 

  7. The tilde "~" prefix is already used with "private" custom functions by this standard, so it didn't occur to me before as a realistic option. On second thought, it doesn't seem like such a bad idea. There won't be any name collisions, since function names start with capital letters and variable names start lowercase; and the variables of a Let() function are more meaningfully private than any custom function. The tilde also has much less visual weight than the alphanumeric characters, so it shouldn't make variable names any harder to read. So how about ~letVariable ?

    1. I think you're right on with this one Jeremy. Aside from the slight discomfort of the finger position on the keyboard (wink) - this one makes sense.

      Did you want to edit the page or have me do it. You've got perms right?

  8. Anonymous

    I've done a lot of scripting in various applications going back to dbase. Since we now have $$Vars in Filemaker, would it not be simpler to set up the global variable as a temp parameter.

    Set inside your script...

    $$_param[1] = "Something"

    $$_param[2] = "Something Else"

    ...

    RR_param[3] = "The End"

    In your other script you define what each param in a comment is and then use it.

    Would this not eliminate all of the filtering and other things I see above?

    Jack Rodgers

    1. It looks like you're talking about passing values between scripts? We have a Best Practices page on a custom function interface for script parameters. That might be the better place for talking about this.