Τρίτη 23 Ιανουαρίου 2018

Refactoring code in M2000 using an example from c Sharp


Example 1 code transcript to M2000 from C#, and  I add an exception if type is unknown. Source for C#: https://www.codeproject.com/Articles/1083348/Csharp-BAD-PRACTICES-Learn-how-to-make-a-good-code
At that article supposed to demonstrate good practice on the first example. My opinion is that this code is useful (adding the exception) if we want what we get, and never we have to change it. 
In original code there is a Result variable to hold result, and give it as last statement, but in M2000 we can give result (without leaving function) as many time we want, because always a function exit at last statement or if an exit statement exist out of block, or a break exist in a block  (but not in a Case block).
Type is colored blue because is known to editor and used in Group definitions, but here we use it as a variable. Check that variable list is as a tuple, but it is a syntactic sugar, interpreter make a Read statement as first line of code inside calculate as Read amount, type, years which means read from stack of values and place values to variables, and if not identifiers exist then make them. This is a way to define variables by assigning values from stack. We say stack and this is a stack of values, not the return stack, and all functions called with a new stack of values. Also notice that M2000 interpreter call user functions without knowledge about what a function need, so it up to us, as programmers,  to decide it, when we read values from stack, or do some task on stack before, and if we leave values then these values dropped, erased as stack erased at return from function.
The code below has something wrong...we don't describe what is the "magic numbers", and the most we didn't know what a number for type variable supposed to mean. Maybe we have a reference sheet, so we read that. We can make some remarks describing these values also. This is no problem. Writer in the article for bad practices never wrote about these optionals ways to "memorize" the "magic numbers".
In this article I want to show how we can expand this logic, not for a kind of "good practice", but a kind of useful refactoring to achieve more from the code, if we want that.
Lets see this code and we discuss the first expansion

Example 1


Function Calculate (amount, type, years) {
      result = 0
      disc = If(years > 5 ->5/100 , years/100)
      if (type == 1) Then {
            result = amount
      } else.if (type == 2) Then {
            result = (amount - (0.1 * amount)) - disc * (amount - (0.1 * amount))
      } else.if (type == 3) Then {
      result = (0.7 * amount) - disc * (0.7 * amount)
      } else.if (type == 4) Then {
        result = (amount - (0.5 * amount)) - disc * (amount - (0.5 * amount))
      } else {
            Error "Unknown type"
      }
      =result
}
\\ we can use this without result
\\ so we can change the function definition here
Function Calculate (amount, type, years) {
      \\ If(condition -> true, false)  \\ only one expression evaluated
      \\ condition has to be an expression which return a number -1 or 0 (true or false)
      \\ there is another option if we use numbers>0, so 1 is for first expression and N for Nth expression
      \\ here we use years>5 which return always true or false (-1 or 0)
      disc = If(years > 5 ->5/100 , years/100)
      if (type == 1) Then {
            = amount
      } else.if (type == 2) Then {
            = (amount - (0.1 * amount)) - disc * (amount - (0.1 * amount))
      } else.if (type == 3) Then {
            = (0.7 * amount) - disc * (0.7 * amount)
      } else.if (type == 4) Then {
            = (amount - (0.5 * amount)) - disc * (amount - (0.5 * amount))
      } else {
            Error "Unknown type"
      }
}
Print Calculate(100, 2, 4)
Print Calculate(100, 4, 4)

We can use less curly brackets, and types plus Enum type:
Global Enum Types {type1=1,type2,type3,type4}
Function Calculate(amount as double, type as Types, years as long) {
      disc=If(years > 5 ->5/100 , years/100)
      if type=1 then
            =amount
      else.if type=2 then
            =amount-0.1*amount-disc*(amount-0.1*amount)
      else.if type=3 then
            =0.7*amount- disc*0.7*amount
      else.if type=4 then
            =amount-0.5*amount-disc*(amount-0.5*amount)
      end if
}
Print Calculate(100, 1, 4)
Print Calculate(100, type2, 4)
Print Calculate(100, 3, 4)
Print Calculate(100, 4, 4)
Try Ok {
      Print Calculate(100, 5, 4)
}
If Error or not Ok then print Error$

Using Global Enum we can pass numbers, so function test if number fit to expected Enum. If we use llocal enum we have to place enum type value (constant or variable).

Or we can use a lambda function with a closure to Enum types

Calculate=lambda -> {
      Enum Types {
            type1=1,
            type2,
            type3,
            type4
      }
      =lambda
            Types,
            (amount as double, type as Types, years as long)
            -> {
                  disc=If(years > 5 ->5/100 , years/100)
                  Select Case type
                  case type1
                        =amount
                  case type2
                        =amount-0.1*amount-disc*(amount-0.1*amount)
                  case type3
                        =0.7*amount- disc*0.7*amount
                  case type4
                        =amount-0.5*amount-disc*(amount-0.5*amount)
                  end select
            }
}() ' see () execute top lambda immediately
Print Calculate(100, 1, 4)
Print Calculate(100, 2, 4)
Print Calculate(100, 3, 4)
Print Calculate(100, 4, 4)
Try Ok {
      Print Calculate(100, 5, 4)
}
If Error or not Ok then print Error$

Or using an object, which can act as function, and as lambda function, using private and public members instead of closures. Here Enum Types is a member of Calc class type. Because Calc is a global function, there is no Calc.type1, we have to find it as using object, like here we use Calculate.type1. See type as .Types has a dot before Types.

Class Calc {
Private:
      counter=0
Public:
      Enum Types {type1=1, type2, type3, type4}
      Value (amount as double, type as .Types, years as long) {
            .counter++
            disc=If(years > 5 ->5/100 , years/100)
            if type=1 then
                  =amount
            else.if type=2 then
                  =amount-0.1*amount-disc*(amount-0.1*amount)
            else.if type=3 then
                  =0.7*amount- disc*0.7*amount
            else.if type=4 then
                  =amount-0.5*amount-disc*(amount-0.5*amount)
            end if
      }
      Property TimesCalc {
            value {
                  link parent counter to counter
                  value=counter
            }
      }
}
Calculate=Calc()
Print Calculate(100, 1, 4)
Print Calculate(100, Calculate.type2, 4)
Print Calculate(100, 3, 4)
Print Calculate(100, 4, 4)
Try Ok {
      Print Calculate(100, 5, 4)
}
If Error or not Ok then print Error$
Print "Calculate return value ";Calculate.TimesCalc;" times"



Try to use Calculate(100,6, 4) you get an error. So now we see that this code works. But we don't provide any reference about value for type variable.
So let we use some lambda and inventories. We use "original"  calculate too to get results and compare them.
Lambda functions are first citizens in M2000. We can put some closures with it. a closure is a copy if exist or a new variable which exist only for lambda function. If a closure is a reference type, such as an inventory list, then we get a copy of reference. A lambda function is a value type.
Here in lambda GenerateFunctions we make a local lambda as disc and place it in each lambda for each key in inventory TypeOfCostumer. GenerateFunctions return a lambda also, which have a reference to TypeOfCustomer. This is the last reference because after the exit of GenerateFunctions all local variables destroyed (but for reference type, only the identifier destroyed, which points to inventory). Inventory will be destroyed when no pointer point to it.
We can call a lambda function directly from a inventory, passing what we want (as mentioned above, no check for parameters happen in the call, but in the function where we call). So the return from GenerateFunctions is a lambda and when we call it we pass typename$, amount and years. So now we don't use number for type of costumer. If we want to add some other types we have to do this in source, at GenerateFunctions definition.

For testing purposes we use a TestIt() subroutine, passing by reference a lambda, just name of it, and an inventory with keys as the names of types for customers. We use the M2000 console, and the Menu, a drop down list which use the character position (text cursor) to calculate the real position of showing. We can use Esc to make no choice, so maybe Menu (as read only variable), return 0. So Menu with a number from 1 means we have a choice. Because Inventories keys are numbered from 0, we have to use Menu-1.
Also in this example we use a Menu with direct strings to show, and a Menu with strings which are keys from an inventory which pass (we pass it by value, but because is reference type, we pass a copy of pointer to it).
For this code we can omit the second parameter (erased from definition, and at the calling of sub) because Sub can read CostumerType, is at module level, so each variable at that level is visible in subs (that not hold for functions and modules - modules are not subs, but may have functions/modules/subs/threads also). When we call a sub a READ NEW statement executed. Read new make new variables always, so CostumerType exist two times, but the last one is visible until we return from sub, where all new definitions erased.
Also notice in TestIt() subroutine we use a For This block, which is useful for temporary definitions. So Iterator exist only in this block. A subrutine is like a block with temporary definitions, but is not a block (a block use different resources in M2000 from a subroutine)

Example 2

GenerateFunctions =Lambda -> {
      Inventory TypeOfCustomer
      disc =lambda (years) -> If(years > 5 ->5/100 , years/100)
      Append TypeOfCustomer, "NotRegistered":=Lambda disc (amount, years)-> {
            = amount
      }
      Append TypeOfCustomer, "SimpleCustomer":=Lambda disc (amount, years)-> {
            = (amount - (0.1 * amount)) - disc(years) * (amount - (0.1 * amount))
      }
      Append TypeOfCustomer, "ValuableCustomer":=Lambda disc (amount, years)-> {
            = (0.7 * amount) - disc(years) * (0.7 * amount)
      }
      Append TypeOfCustomer, "MostValuableCustomer":=Lambda disc (amount, years)-> {
            = (amount - (0.5 * amount)) - disc(years) * (amount - (0.5 * amount))
      }
      // we place TypeofCustomer as a closure for returned lambda
      =Lambda TypeofCustomer (typename$, amount, years) -> {
            If Not Exist(TypeofCustomer,typename$) then Error "Not implemented yet"
            =TypeofCustomer(typename$)(amount, years)
      }
}
// We place lambda from GenerateFunctions() to a closure in FunctionByType
DiscountManager =lambda FunctionByType=GenerateFunctions() (typename$, amount, years)-> {
      =FunctionByType(typename$, amount, years)
}
Function Calculate (amount, type, years) {
      result = 0
      disc = If(years > 5 ->5/100 , years/100)
      if (type == 1) then
            result = amount
      else.if (type == 2) then
            result = (amount - (0.1 * amount)) - disc * (amount - (0.1 * amount))
      else.if (type == 3) then
      result = (0.7 * amount) - disc * (0.7 * amount)
      else.if (type == 4) then
        result = (amount - (0.5 * amount)) - disc * (amount - (0.5 * amount))
      else
            Error "Unkown type"
      end if
      =result
}
Print Calculate(100, 2, 4), DiscountManager("SimpleCustomer",100,4)
Print Calculate(100, 4, 4), DiscountManager("MostValuableCustomer",100,4)
Inventory CustomerType = "NotRegistered":=1, "SimpleCustomer":=2, "ValuableCustomer":=3, "MostValuableCustomer":=4
TestIt(&DiscountManager, CustomerType)
End


Sub TestIt(&DiscountManager, CustomerType)
      // simple menu to test it
      Menu "NotRegistered", "SimpleCustomer", "ValuableCustomer", "MostValuableCustomer"
      If Menu>0 then Print Menu$(Menu), DiscountManager(Menu$(Menu),100,4)


      // Fill menu with keys, We use For This to open a block for temporary definitions
      CustomerTypeKey$=Lambda$ CustomerType (base0)-> Eval$(CustomerType, base0)
      For This {
            // clear Menu list
            Menu
            // Make an iterator
            Iterator=Each(CustomerType)
            // Use it  (iterator^ is the cursor and based 0)
            // Menu + means append a string to menu
            //             While Iterator : Menu + CustomerTypeKey$(Iterator^): End While
            While Iterator {Menu + CustomerTypeKey$(Iterator^)}
            // Menu ! means show menu
            Menu !
            // We can open menu to specific string using Menu Show "SimpleCustomer"
      }
      // we can use Menu (base 1) to get index in base 0, subtract 1, and get the name using CustomerTypeKey$()
      // This is usefull if we have different names in menu, so Menu$(Menu)) can't return the proper key for inventory
      If Menu>0 then Print CustomerTypeKey$(Menu-1), DiscountManager(Menu$(Menu),100,4)
End Sub



So from now on we don't repeat the Sub TestIt(). Just copy to run examples. The third program change the game, using a Class to make a Group with some members. We want now to add types later in code. To Check that we make MostValuableCustomer as an addition after the first use.
We can make a group and handle it without constructor, but Class definitions gave us two significant things, the constructor, and the ability to not include some members in the returned group, after the Class: label. A class definition is a group factory, not a class with the meaning of prototype. Each group in M2000 is a prototype by itself. Inheritance in M2000 exist as two kinds, but here we don't use either, and it is out of scope for this text.

To use the class we have to call it like a function and the result we have to put somewhere, in a name, so we define a named group, or in an item in a container. Here we make a named group the DiscoutManager. We don't have private members here.
Notice that we use a Module as member of group to append customer types, accessing .TypeOfCustomer (a member of group, see dot before, this is like This.TypeOfCustomer). Also notice that we make disc as copy of member .disc (another lambda) to pass it as a closure. Also notice the mysterious syntax of ![] which are a symbol ! and special function [] which get stack of values, replacing with a new empty one. So after the call to that module we get an empty stack of values (this is something which a M2000 programmer must know, if using the stack for storing previous calculations). So Symbol ! in a function before a stack object move all items from that object to function's own stack object. We use this to pass fast from a module to a function values without defining variables, and then pass the variables to function call.

Example 3

Class DiscountManagerClass {
      Inventory TypeOfCustomer
      disc =lambda (years) -> If(years > 5 ->5/100 , years/100)
      Calculate=lambda->0
      Module AppendTypeOfCustomer (CustomerType$, lambdafun) {
            disc=.disc
            Append .TypeOfCustomer, CustomerType$:= Lambda disc, lambdafun -> {
                  = lambdafun(disc, ![]) ' pass stack to lambdafun
            }
      }
      Module RefreshCalculate {
            FunctionByType=Lambda PrivateTypeofCustomer=.TypeofCustomer (typename$, amount, years) -> {
                  If Not Exist(PrivateTypeofCustomer,typename$) then Error "Not implemented yet"
                  =PrivateTypeofCustomer(typename$)(amount, years)
            }
            .Calculate<=lambda FunctionByType (typename$, amount, years)-> {
                  =FunctionByType(typename$, amount, years)
            }
      }
Class:
      Module DiscountManagerClass {
            \\ get a local disc from This.disk
            .AppendTypeOfCustomer "NotRegistered", lambda (disc, amount, years)->amount
            .AppendTypeOfCustomer "SimpleCustomer", Lambda (disc, amount, years)-> (amount - (0.1 * amount)) - disc(years) * (amount - (0.1 * amount))
            .AppendTypeOfCustomer "ValuableCustomer", Lambda (disc, amount, years)->(0.7 * amount) - disc(years) * (0.7 * amount)
            .RefreshCalculate
      }
}
Function Calculate (amount, type, years) {
      result = 0
      disc = If(years > 5 ->5/100 , years/100)
      if (type == 1) then
            result = amount
      else.if (type == 2) then
            result = (amount - (0.1 * amount)) - disc * (amount - (0.1 * amount))
      else.if (type == 3) then
            result = (0.7 * amount) - disc * (0.7 * amount)
      else.if (type == 4) then
            result = (amount - (0.5 * amount)) - disc * (amount - (0.5 * amount))
      else
            Error "Unkown type"
      end if
      =result
}
DiscountManager=DiscountManagerClass()
Print Calculate(100, 2, 4), DiscountManager.Calculate("SimpleCustomer",100,4)
\\ Now we add MostValuableCustomer, in execution time
For DiscountManager {
      .AppendTypeOfCustomer "MostValuableCustomer", Lambda (disc, amount, years)->(amount - (0.5 * amount)) - disc(years) * (amount - (0.5 * amount))
      .RefreshCalculate
}
Print Calculate(100, 4, 4), DiscountManager.Calculate("MostValuableCustomer",100,4)
\\ now we get a reference from DiscountManager.TypeOfCustomer
CustomerType=DiscountManager.TypeOfCustomer
CustomerTypeKey$=Lambda$ CustomerType (base0)-> Eval$(CustomerType, base0)
TestIt(&DiscountManager.Calculate, CustomerType)
End

(copy Sub here)
The problem with previous example is that we can't change Disc closure in lambda functions when these functions written as values to inventory. So it is good example if we never change Disc lambda in DiscountManager. So the solution is to have Disc as a reference type, but lambdas are not reference types. So we have to use an inventory and write there our lambda. So we pass as closure the inventory, and if we change the item at key 0 (we can use numbers as keys also) we change the "service" lambda.
Also we want to change the function associate with a customer type after the first use. So we make an new member to our group (in class definition) as ChangeTypeOfCostumer. 
Block For DiscountManager {} is a same kind as for For This {}, so any new definition erased after, but not for those that we make as closures and those inserting in containers.

Example 4

Class DiscountManagerClass {
      Inventory TypeOfCustomer, disc= 0:=lambda (years) -> If(years > 5 ->5/100 , years/100)
      Calculate=lambda->0
      Module AppendTypeOfCustomer (CustomerType$, lambdafun) {
            disc1=.disc
            Append .TypeOfCustomer, CustomerType$:= Lambda disc1, lambdafun -> {
                  = lambdafun(disc1(0), ![]) ' pass stack to lambdafun
            }
      }
      Module ChangeTypeOfCustomer (CustomerType$, lambdafun) {
            disc1=.disc
            If Not Exist(.TypeOfCustomer, CustomerType$) then Error "Not implemented yet"
            Return .TypeOfCustomer, CustomerType$:= Lambda disc1, lambdafun -> {
                  = lambdafun(disc1(0), ![]) ' pass stack to lambdafun
            }
      }
      Module RefreshCalculate {
            FunctionByType=Lambda PrivateTypeofCustomer=.TypeofCustomer (typename$) -> {
                  If Not Exist(PrivateTypeofCustomer,typename$) then Error "Not implemented yet"
                  \\ pass amount and year too, of not we get error
                  =PrivateTypeofCustomer(typename$)(![])
            }
            .Calculate<=lambda FunctionByType -> {
                  ' need typename$, amount, years
                  =FunctionByType(![])
            }
      }
Class:
      Module DiscountManagerClass {
            \\ get a local disc from This.disk
            .AppendTypeOfCustomer "NotRegistered", lambda (disc, amount, years)->amount
            .AppendTypeOfCustomer "SimpleCustomer", Lambda (disc, amount, years)-> (amount - (0.1 * amount)) - disc(years) * (amount - (0.1 * amount))
            .AppendTypeOfCustomer "ValuableCustomer", Lambda (disc, amount, years)->(0.7 * amount) - disc(years) * (0.7 * amount)
            .RefreshCalculate
      }
}
Function Calculate (amount, type, years) {
      result = 0
      disc = If(years > 5 ->5/100 , years/100)
      if (type == 1) then
            result = amount
      else.if (type == 2) then
            result = (amount - (0.1 * amount)) - disc * (amount - (0.1 * amount))
      else.if (type == 3) then
            result = (0.7 * amount) - disc * (0.7 * amount)
      else.if (type == 4) then
            result = (amount - (0.5 * amount)) - disc * (amount - (0.5 * amount))
      else
            Error "Unkown type"
      end if
      =result
}
DiscountManager=DiscountManagerClass()
Print Calculate(100, 2, 4), DiscountManager.Calculate("SimpleCustomer",100,4)
\\ Now we add MostValuableCustomer, in execution time
For DiscountManager {
      .ChangeTypeOfCustomer "SimpleCustomer", Lambda (disc, amount, years)-> (amount - (0.2 * amount)) - disc(years) * (amount - (0.2 * amount))
      .AppendTypeOfCustomer "MostValuableCustomer", Lambda (disc, amount, years)->(amount - (0.5 * amount)) - disc(years) * (amount - (0.5 * amount))
      .RefreshCalculate
}
Print Calculate(100, 4, 4), DiscountManager.Calculate("MostValuableCustomer",100,4)


Return DiscountManager.disc, 0:=lambda -> .5
Print DiscountManager.Calculate("SimpleCustomer",100,4)
Print DiscountManager.Calculate("MostValuableCustomer",100,4)
\\ restore old function
Return DiscountManager.disc, 0:=lambda (years) -> If(years > 5 ->5/100 , years/100)
Print DiscountManager.Calculate("SimpleCustomer",100,4)
Print DiscountManager.Calculate("MostValuableCustomer",100,4)


\\ now we get a reference from DiscountManager.TypeOfCustomer


CustomerType=DiscountManager.TypeOfCustomer
CustomerTypeKey$=Lambda$ CustomerType (base0)-> Eval$(CustomerType, base0)
TestIt(&DiscountManager.Calculate, CustomerType)
End


(copy Sub here)
An idea from example 4 is to put it in a module (without Sub) and return to stack only two values: DiscountManager.TypeOfCustomer, DiscountManager.Calculate

These all we need to use the final Calculate and to get names from TypeOfCustomer.


Conclusion

For my opinion, some "bad practice" is not always bad if we have no problem, and works nice. Forget the spaceship if a bicycle is all you need.




















Δεν υπάρχουν σχόλια:

Δημοσίευση σχολίου

You can feel free to write any suggestion, or idea on the subject.