Previous post shows Functional Programming (FP) concepts and definition with JavaScript examples: https://community.dynamics.com/nav/b/conceptsindailynavisionwork/archive/2018/05/20/functional-programming-javascript-vs-nav . Now let´s try to apply FP in NAV C/Side.

Pure functions.

It´s common sense, you can´t put all your code in pure functions, we write code looking for side effects: write a file or database, show a page, etc. We want to share data and state between objects. So, our goal could be put all code in pure functions as we can. Separate calculations and getting values from the rest of the code with intended and desired side effects. As much code you have in pure functions, safer are your programs.
Example:
 
AddNewLineExistingOrder(OrderNo : Code[20];ItemNo : Code[20];RequiredQty : Decimal;AvailableQuantity : Decimal)
SalesLine.SETRANGE("Document Type",SalesLine."Document Type"::Order);
SalesLine.SETRANGE("Document No.",OrderNo);
IF SalesLine.FINDLAST THEN
  LastOrdenLineNo := SalesLine."Line No.";
 
CLEAR(SalesLine);
SalesLine."Document Type" := SalesLine."Document Type"::Order;
SalesLine."Document No." := OrderNo;
SalesLine."Line No." := LastOrdenLineNo + 10000;
 
SalesLine.VALIDATE(Type,SalesLine.Type::Item);
SalesLine.VALIDATE("No.",ItemNo);
IF RequiredQty < AvailableQuantity THEN
  SalesLine.VALIDATE(Quantity,RequiredQty)
ELSE
  SalesLine.VALIDATE(Quantity,AvailableQuantity);
SalesLine.INSERT;
 
The example above adds a new line in an existing order, passing item number, quantity and max available quantity for the item. We can put in separate pure functions calculations and all the code that we can:
 
AddNewLineExistingOrderRefractored(OrderNo : Code[20];ItemNo : Code[20];RequiredQty : Decimal;AvailableQuantity : Decimal)
WITH SalesLine DO BEGIN
  "Document Type" := SalesLine."Document Type"::Order;
  "Document No." := OrderNo;
  "Line No." := GetLastOrderLineNo(OrderNo) + 10000;
 
  VALIDATE(Type,SalesLine.Type::Item);
  VALIDATE("No.",ItemNo);
  VALIDATE(Quantity,MinimunBetween(RequiredQty,AvailableQuantity));
  INSERT;
END;
LOCAL GetLastOrderLineNo(OrderNo : Code[20]) : Integer
WITH SalesLine DO BEGIN
  SETRANGE("Document Type",SalesLine."Document Type"::Order);
  SETRANGE("Document No.",OrderNo);
  IF FINDLAST THEN
    EXIT("Line No.");
END;
LOCAL MinimunBetween(DecToCompare1 : Decimal;DecToCompare2 : Decimal) : Decimal
if DecToCompare1 < DecToCompare2 THEN
  EXIT(DecToCompare1)
ELSE
  EXIT(DecToCompare2);
This way we only have to worry about the few lines remaining in AddNewLineExistingOrder, because the other two functions are very safe.

The C/SIDE return value question.

Functional programming encourages us to avoid modify function parameters and only handle function return variable.
We have a little problem with this, because C/SIDE functions can´t return complex values as records or arrays. This force us to fail into output parameter smell, we have to pass a parameter to return complex values. We can´t avoid this but we can manage this limitation better, like NAV do it (example existing function in table 111):
[External] FilterPstdDocLnItemLedgEntries(VAR ItemLedgEntry : Record "Item Ledger Entry")
ItemLedgEntry.RESET;
ItemLedgEntry.SETCURRENTKEY("Document No.");
ItemLedgEntry.SETRANGE("Document No.","Document No.");
ItemLedgEntry.SETRANGE("Document Type",ItemLedgEntry."Document Type"::"Sales Shipment");
ItemLedgEntry.SETRANGE("Document Line No.","Line No.");
 
Avoiding side effects due input var record NAV do a RESET statement at the beginning of function.

No side effects.

Not about avoid all side effects, is about avoid unnecessary side effects.
The main way to get no side effects is to be local over global. I think local variables have been already advised a lot of times before. Gary Winter told us in his clean code sessions: be as local as you can. Only two reminders:
P1 Think about all the time and resources you can waste due to global variables failures: they are really hard to detect.
P2 It´s better to pass global variables as parameters between functions. If you are afraid to fall in too many parameters smell you can use this pattern: https://community.dynamics.com/nav/w/designpatterns/245.argument-table
Title Avoid loops.
We don´t have functor objects (you can see previous post explanation about functors and map, reduce and filter) to avoid loops but we have another similar resources to avoid loops:
Item.SETRANGE(“Item Category”,SomeCategory);
Item.MODIFYALL(Blocked,TRUE,TRUE);
 
This is very similar to JavaScript map function: Modify and apply OnModify trigger to all the set. We have filter functions like SETRANGE, or SETFILTER. CALCSUMS are like a reduce statement in JavaScript:

Item.SETRANGE(“Item Category”,SomeCategory);

Item.CALCSUMS(“Gross weight”);

 

Recursion.

Be careful, because sometimes recursion could be more dangerous than loops we want to avoid. But sometimes is very useful, you have beautiful examples in NAV code (Codeunit 6520 Item Tracing Mgt.):
LOCAL NextLevel(VAR TempTrackEntry : Record "Item Tracing Buffer";TempTrackEntry2 : Record "Item Tracing Buffer";Direction : 'Forward,Backward';ShowComponents : 'No,Item-tracked only,All';ParentID : Integer)
WITH TempTrackEntry2 DO BEGIN
  IF ExitLevel(TempTrackEntry) THEN
    EXIT;
  CurrentLevel += 1;
 
And bellow:
          IF InsertRecord(TempTrackEntry,ParentID) THEN BEGIN
            FindComponents(ItemLedgEntry,TempTrackEntry,Direction,ShowComponents,ItemLedgEntry."Entry No.");
            NextLevel(TempTrackEntry,TempTrackEntry,Direction,ShowComponents,ItemLedgEntry."Entry No.");
          END;
Notice the use of panic mode variable CurrentLevel to avoid infinite loop and subsequent memory overflow, or never-ending process. That´s a good practice in recursion. But even better, don´t create a global, declare it as parameter.

Immutability.

When customer balance change due an invoice or payment we don´t have a balance field and do that:
Customer.Balance := Customer.Balance + OperationAmount;
Instead we create a new Customer Ledger Entry and Customer Balance is calculated from these entries. This is a use of immutability in NAV. We don´t change the state of existing object (Customer balance) we create a new object (Customer Ledger Entry). Why NAV do this (despite another functional considerations, like the fact that we need a detailed customer history)?:
  • Is safer, think about all the concurrency errors could raise updating a balance field in Customer.
  • We have a track of changes: is more much clear, and can help us to detect errors.

Declarative vs Imperative.

Lot of people told us about be more declarative, but it´s important to show what this really mean.
Let´s see Codeunit 439 Approvals Management in 2015 release. The TestSalesPayment ends this way:
(Warning: I talk about NAV standard code, but I don´t intend to be mean, cute or disrespectful with the authors. The opposite, I think NAV standard code it´s the best pattern source in administration software.)
IF EntryFound THEN
  EXIT(TRUE);
EXIT(FALSE);
 
Not bad code, but why this IF statement? If you write instead only this line of code is better:
EXIT(EntryFound);
Then you have two great (little) advantages:
  • Avoid the conditional statements, another source of potential errors.
  • The code is more expressive. I know, here we are only few lines but we can feel the difference.
I don´t want to be critic with the Codeunit, because it has some good examples of use of declarative code like:

  IF NOT SalesLinesExist THEN

    ERROR(Text015,FORMAT("Document Type"),"No.");

 
Instead of
 

SalesLines.SETRANGE(…….

SalesLines.SETRANGE(…….

IF NOT SalesLines.FINDFIRST THEN

  ERROR(Text015,FORMAT("Document Type"),"No.");

 
The original author uses a well-named function instead put all the code like I do bellow and the result it´s more expressive and safer.

Some rules about the conditional and boolen.

Avoid unnecessary conditional statements.

IF Quantity > 0 THEN

  Positive := TRUE

ELSE

  Positive := FALSE;

 
NAV do the right choice in Codeunit 22:
Positive := (Quantity > 0);

Simplify conditions and try to only evaluate Booleans in conditional statements:

OnRun(VAR Rec : Record "Sales Header")

IF PostingDateExists AND (ReplacePostingDate OR ("Posting Date" = 0D)) THEN BEGIN

  "Posting Date" := PostingDate;

  VALIDATE("Currency Code");

END;

 
Would rather:

ReplaceDate := PostingDateExists AND (ReplacePostingDate OR ("Posting Date" = 0D));

IF ReplaceDate THEN

  "Posting Date" := PostingDate;

  VALIDATE("Currency Code");

END;

 
And better, create a new function:

LOCAL ReplaceDate(DateToEval : Date) : Boolean

EXIT(PostingDateExists AND (ReplacePostingDate OR (DateToEval = 0D)));

 
Called this way:

IF ReplaceDate("Posting Date") THEN

  "Posting Date" := PostingDate;

  VALIDATE("Currency Code");

END;

 
Another good example: evaluate with IF between two values:

IF AsignedQty < OrderQty THEN

  RemQty := AsignedQty

ELSE

  RemQty := OrderQty;

 
Better:

RemQty := MinValueBetweeen(AsignedQty,OrderQty);

 
We only have to make a Function MinValueBetweeen that returns min between two decimals.

Conclusions.

We can use Functional programing learnings this way in NAV:
  • Put as code as possible in pure functions. Notice that is impossible to put all code in pure functions, be practical.
  • Try to use NAV version of immutability, insert an entry or log record instead modify state (field). There are a lot of examples in NAV: Ledger Entries, Approvals or Budgets.
  • Avoid unnecessary loops with CALCSUMS or DELETEALL(TRUE), when possible (but don´t expect say goodbye to loops soon).
  • Be local instead global.
  • Be declarative over imperative.
To be declarative you can do these improvements:
  • Avoid unnecessary conditionals.
  • Substitute lines of code related with a final evaluation with a well named function.
  • Simplify condition evaluations with functions and boolean variables.