📜 ⬆️ ⬇️

Index-based programming or why do we need all these if, switch, ternary operator?

Recently I read a topeip about the beauty of the code . In the comments, the topic of moving parentheses when writing a conditional statement gained popularity. In one of the options, the example from the article looked like this:
if (typeof a ! == "undefined" && typeof b ! == "undefined" && typeof c === "string") { call_function(a, b, c); // ... } 
Thinking about the conditions themselves: they are a bit strange, although they often occur. Inside “call_function”, type “a” and type “b” will be checked, but not type “c”. On the other hand, the number of supported combinations of the “a” and “b” types supported by the function is of course, and, most likely, is fixed, which means it would be useful to see these combinations. And this post prompted the idea that you can do without conditional operators. So the idea was born to abandon conditional operators in favor of indices. Although the approach is considered within the framework of Javascript, it can be successfully applied in many other languages ​​after taking into account their syntactic features.

Do not expect to see here pictures Rembrandt of the programming world. The code in the article is a work by Dali. However, as the article itself.

So, this is how a expanded condition can look like (I will write in my own style and immediately in Perfect Disjunctive Normal Form (PDNF) ):
 if(false ||(true &&typeof a === "string" &&typeof b === "string" &&typeof  === "string" )||(true &&typeof a === "object" &&typeof b === "string" &&typeof  === "string" )||(true &&typeof a === "number" &&typeof b === "string" &&typeof  === "string" )||(true &&typeof a === "string" &&typeof b === "object" &&typeof  === "string" )||(true &&typeof a === "object" &&typeof b === "object" &&typeof  === "string" )||(true &&typeof a === "number" &&typeof b === "object" &&typeof  === "string" )||(true &&typeof a === "string" &&typeof b === "number" &&typeof  === "string" )||(true &&typeof a === "object" &&typeof b === "number" &&typeof  === "string" )||(true &&typeof a === "number" &&typeof b === "number" &&typeof  === "string" ) ){ call_function(a, b, c); // ... } 

It turned out somehow long. We see that the perfect conjunctive normal form (SKNF) is more suitable here:
 if(true &&(false ||typeof a === "string" ||typeof a === "number" ||typeof a === "object" )&&(false ||typeof b === "string" ||typeof b === "number" ||typeof b === "object" )&&(false ||typeof c === "string" ) ){ call_function(a, b, c); // ... } 
But then the question arises, what to do when the combination of argument types is different? Why not cover all possible options at once. Errors of not accounting for some combination of a multitude of conditions are common, and they are difficult to detect.
Let "a" can have the types 'undefined', 'object', 'boolean', 'number', 'string', 'function', but the behavior is important for us only when 'object', 'string', 'number', the behavior with other types, the behavior is similar to 'undefined'.
Let for “b” everything will be similar.
Type "c" - consider only 'string' and 'undefined'.
Total, the number of options = 4 * 4 * 2, each of which corresponds to one elementary conjunction in the PDNF, but now we can distinguish them and change the behavior for each specific case.

Analogue of the conditional operator:

  //     "a", "b", "c" //    //     var a_type_index = {'undefined':0, 'object':1, 'boolean':0, 'number':2, 'string':3, 'function':0}[typeof a] var b_type_index = {'undefined':0, 'object':1, 'boolean':0, 'number':2, 'string':3, 'function':0}[typeof b] var c_type_index = {'undefined':0, 'object':0, 'boolean':0, 'number':0, 'string':1, 'function':0}[typeof c] var index = a_type_index + b_type_index*4 + c_type_index*4*4 
For demonstration purposes, I decided not to complicate the code ...
but then he changed his mind and decided that the universal solution was more beautiful. I think it is convenient for many to think in numbers:
  //     "a", "b", "c" //    //     var types = ['undefined', 'object', 'boolean', 'number', 'string', 'function'] var a_type_index = types.indexOf(typeof a) //    a_type_index = +'010230'[a_type_index] var b_type_index = types.indexOf(typeof b) b_type_index = +'010230'[b_type_index] var c_type_index = types.indexOf(typeof c) c_type_index = +'000010'[c_type_index] var index = a_type_index + b_type_index*4 + c_type_index*4*4 
All that is left now is to select by the index what to do:
  ;[ //c has incorrect type //b has incorrect type //types a: incorrect, object, number, string show_err_all, show_err_bc, show_err_bc, show_err_bc, //b has object type //types a: incorrect, object, number, string show_err_ac, show_err_c, show_err_c, show_err_c, //b has number type //types a: incorrect, object, number, string show_err_ac, show_err_c, show_err_c, show_err_c, //b has string type //types a: incorrect, object, number, string show_err_ac, show_err_c, show_err_c, show_err_c, //c has string type //b has incorrect type //types a: incorrect, object, number, string show_err_ab, show_err_b, show_err_b, show_err_b, //b has object type //types a: incorrect, object, number, string show_err_a, process, process, process, process, //b has number type //types a: incorrect, object, number, string show_err_a, process, process, process, process, //b has string type //types a: incorrect, object, number, string show_err_a, process, process, process, process ][index](a, b, c) 

So, the first principle of index-oriented programming : the coverage of all options .
')
Naturally conditions can be any. Why confine one conditional operator when there are others like him.

Analogue of the ternary operator:

  var b_status = false //    var str_status = b_status? 'ok': 'this is false' //   var str_status = ['this is false','ok'][+b_status] 
The "+" sign converts the b_status variable into a numeric type, since the index must be a number. Notice that the first expression goes to fail the condition.

Analogue of the multiple choice operator:

  var i_state = 1 //  switch switch (i_state) { case 1: console.log('you select 1') break case 2: console.log('you select 2') break default: console.log('you select another') } //   i_state = 0|[0,1,2][i_state] //       //i_state = 0|{'one':1, 'two':2}[str_state] //i_state = ['one', 'two'].indexOf(str_state)+1 [ function(){ //default console.log('you select another') },function(){ //i_state == 1 console.log('you select 1') },function(){ //i_state == 2 console.log('you select 2') } ][i_state] 
Also, as for the ternary operator, the first (zero) case is for false, for the switch, the first is the default case, which is generally convenient, since it is one, its presence is mandatory - this is a certain analogue of zero.

The second principle of index-oriented programming : the presence of a special zero variant .

However, the idea of ​​index-oriented programming is somewhat broader than the replacement of conditional statements. A couple more examples.

Generating permutations from the index:

 function factorial(n){ if(n<=1) return 1; var ret = 1; for(var i=2; i<=n; i++) ret *= i; return ret; } function i_to_insert_order(n, i){ var arr = [] var i_tmp = i for(var j=0; j<n; j++){ var a = i_tmp%(j+1) arr[j] = a i_tmp = ~~(i_tmp/(j+1)) } return arr } function correct(arr){ var n = arr.length; for(var i=1; i<n; i++){ // for(var j=0; j<i; j++){ for(var j=i-1; j>=0; j--){ //if(arr[j]>=arr[i]) arr[j]++ arr[j] += arr[j]>=arr[i] } } } //correct([0,0,0,0,0]) == [4,3,2,1,0] var n = 5 var arr_perm = [] for(var i=0; i<factorial(n); i++){ var arr = i_to_insert_order(n, i) //arr = 0, 0..1, 0..2, 0..3, 0..4 correct(arr) arr_perm.push(arr) } arr_perm.reverse() //arr_perm.length == factorial(n) //arr_perm[0] == [0,1,2,3,4] //arr_perm[-1] == [4,3,2,1,0] 
A little bit of clarification: "~~ (x)" - a short recording of "Math.floor (x)", which I learned about from livecoding video . When using a short recording, you should observe the restriction: 0 <= x <= 2147483647.9999998 (selected experimentally ). For comparison, the limit for " Math.floor ": -9007199254740992 <= x <9007199254740993. Array inversion is necessary to obtain permutations in lexicographical order . By index we get (i_to_insert_order) one of the possible sequences for inserting elements. In principle, it is enough to get a permutation of real elements:
 function insert(arr, i, elem){arr.splice(i, 0, elem)} //  var elements = ['word', 'symbol', 'face', 'colors', 'song'] // 35-   var order = i_to_insert_order(5, 35) var arr = [] for(var i=0; i<order.length; i++) insert(arr, order[i], elements[i]) //arr == ['word', 'song', 'colors', 'symbol', 'face'] 
But, in order to see a clear permutation, we emulate this insert. This is what the correct function is for. Perhaps this is not a very good example, because it blurs the main point.

The third principle of index-oriented programming : numbering options .

Fibonacci number generation by number:

We use the abbreviated Binet formula . Then, compare the result with the table .
  const φ = (1+Math.sqrt(5))/2 function fib_by_index(i){ return Math.round(Math.pow(φ, i)/Math.pow(5,0.5)) } fib_by_index(6) == 8 fib_by_index(50) == 12586269025 

And, finally, the fourth principle of index-oriented programming : O (1) complexity algorithms .

Benefits:Disadvantages:
Number object
permutation
Fibonacci numbers
computational complexity
Steve McConnell. Code Complete, Second Edition. Chapter 18: Tabular Methods

Source: https://habr.com/ru/post/270241/


All Articles