Falcon Wiki - Survival:Tabular programming
Tabular programming
Long before fourth generation languages appeared, long before compilers appeared, even long before computers ever appeared, there was a time in which people needed to categorize things efficiently and effectively into simple, easily understandable and useful groups.
Tables have been the most effective categorization tool for hundreds years, and they still make the fortunes of marketing and strategy gurus. Compared to them, the formal definition of “class” as we know it as the base of object oriented programming is relatively young. Actually, outside IT labs, people use tables to analyze situations and drive decisions. Unsurprisingly, every managerial decision making system involves some sort of grid, or table, that can easily be built with advanced instruments such as pencil & paper, and these tools can drive decisions worth billion of dollars.
Despite this, IT has relegated tables to the ancillary role of storing data. L ots of interesting data, for sure, but still just data.
Falcon can employ tables to drive program logic as they can drive decision making systems.
Tables are classes
The base of tabular programming is the Table class, which is absolutely a normal class except for some very special interactions with arrays that are handled transparently. It would be more accurate to say that arrays know what a Table instance is, and are kind with them, rather than seeing the Table class as special .
More precisely, a Table is a class that stores a set of rows, each of which is an array, and a heading which describes the meaning of each column. Tables can have one or more pages, that is, sets of rows that can be accessed at a time, and each heading can optionally be provided with “column data” whose semantic value can vary depending on the operations being performed. Both data in rows and column data can be any Falcon object, including other tables, while column names are limited to strings (not necessarily, but preferably, not containing whitespace).
>>> x = Table( [ "name", "income" ], ... [ 'Smith', 2000 ], ... [ 'Jones', 2800 ], ... [ 'Sears', 1900 ], ... [ 'Sams', 3200 ] ) : Object
We have just created a table, which is a standard object, with a mandatory first parameter (the column heading) and a set of rows (each one in a different parameters).
Rows can be then accessed through the get method, which returns an array (the row).
>>> inspect( x.get(0) ) Array[2]{ "Smith" int(2000) }
Falcon sees tables as sequences, so it is possible to iterate on each row:
>>> for l in x ... > @ "name: $(l[0]:r10) income: $(l[1]:r6)" ... end name: Benson income: 2300 name: Smith income: 2000 name: Jones income: 2800 name: Sears income: 1900 name: Sams income: 3200
Arrays stored in tables stay available to the outside. For example ;
>>> row = [ 'Benson', 2300 ] : Array >>> x.insert( 0, row ) >>> row[1] = 2450 : 2450 >>> inspect( x.get(0) ) Array[2]{ "Benson" int(2450) }
They also assume two important properties. First, they become immutable; their size stays fixed to the number of columns in their table (which can vary later on); yet, it is possible to change each element, as long as this doesn't shrink or grow the array. Second, they inherit the table columns, which references their entries. In other words, the row variable of the example above knows that it is part of a table, and that its first element can also be called “name”, while its second element can be called “income”.
>>> row.name : Benson >>> row.income = 2500 : 2500 >>> inspect( row ) Array[2]{ "Benson" int(2500) }
Like any other array, table rows can also have bindings; the main difference between the bindings and the table column names is that bindings lay beside the array, while table names lie inside it, and allow indirect access of their ordinal content. This allows virtualizing the array as an accessible object when needed, while accessing it with a direct index for when higher performance is necessary.
As for pure bindings, methods extracted from the array by their column names can refer the self item;
>>> row.income = function(); return self.name.len() *800; end : Function _lambda#_id_1 >>> row.income() : 4800
and they can also refer the table they come from:
>>> row.income = function(); return self.table().get( self.tabRow()+1 ).income + 100;end : Function _lambda#_id_2 >>> row.income() : 2100
The code above refers the table and the row in the table at which our self is placed. The table and tabRow methods are pre-defined methods of the Falcon Basic Object Model. The FBOM are a set of methods, some common to all the Falcon items, other specific of some item types, which can be generally applied to any Falcon item, including numbers, strings and even nil.
Default Values
As said, tables can be provided with column values. They can be added at initialization using future bindings in the heading entry:
>>> table = Table( ... [ name|"unknown", income| {=> self.name.len()*100} ], ... [nil, nil], ... [ "Smith", nil ], ... [ "Sams", 2500 ] ) : Object
This created a table with two columns, each having an associated column value.
>>> for row in table ... > row.name, ": ", valof( row.income ) ... end unknown: 700 Smith: 500 Sams: 2500
They can also be accessed directly through the columnData method;
>>> table.columnData( 0 ) // access : unknown >>> table.columnData( 0, "known" ) // change : unknown >>> table.get(0).name : known
Table operations
Other than providing names and defaults to access row elements, tables have a set of logical operations that can be used to perform code selection and branching.
The choice method feeds all the rows in the table to an external function for evaluation; the row that gets the highest value is then selected and returned. For example , suppose that we have to pick the discount function to be applied to a certain customer. The next sample program is a bit complex, so it's better to save it in an editor and execute it from the command line:
offers = Table( .[ 'base' 'upto' 'discount' ] , .[ 0 2999 { x => x } ] , .[ 3000 4999 { x => x * 0.95} ] , .[ 5000 6999 { x => x * 0.93} ] , .[ 7000 0 { x => x * 0.9} ] ) function pickDiscount( pz, row ) if pz >= row.base and ( row.upto == 0 or pz <= row.upto ) return 1 else return 0 end end
The pickDiscount function will receive a pz parameter needed for configuration and a row; the row is the element in the table that the choice method extracts and feeds into pickDiscount, Then, if the value of the pz variable is between the base and upto items in the row, 1 is returned; in all the other cases, 0 is returned. This means that the function will select that row where our value lies.
The following statement performs an evaluation, passing 3500 as the pz value, and then applying the discount code block of the row that applies to an arbitrary price.
> offers.choice( [pickDiscount, 3500 ] ).discount( 500 )
We can build a more interesting “all in one” function; see the following examples.
offer = .[ offers.choice .[ pickDiscount &qty ] 'discount' ]
We created a functional sequence made of the choice method on the offer table and the callable it should receive to pick the desired row. The callable itself uses a qty binding that can be configured later on; then, instead of accessing the discount column via the dot accessor (or using the at function), we can use the extra parameter of the choice method, which designs a single column value to be returned. Calling this offer function, we automatically select the discount that is to be applied to customers having a ranking as designed by the qty binding.
This may be used like the following:
offer.qty = 500 > "Price 500 discounted for a small customer: " , offer()( 500 ) offer.qty = 3200 > "Price 500 discounted for a medium customer: " , offer()( 500 ) offer.qty = 6300 > "Price 500 discounted for a big customer: " , offer()( 500 )
The program runs with this result:
Price 500 discounted for a small customer: 500 Price 500 discounted for a medium customer: 475 Price 500 discounted for a big customer: 465
The advantage of using tables for data definitions instead of switches, cascades of nested ifs, method overriding in class hierarchies and so on is that the parameter definitions may be changed live, as the program runs. A new row may be inserted, a new column may be added, the discount conditions or the level limits may be altered, and still our offer functional sequence would reflect the up-to-date status of the condition table.
An interesting feature of tables is that of being able to swap all its data at once, through pagination.
Table pages
Tables can have an arbitrary number of pages which can be independently grown, shrunk, changed and generally updated. All the pages share the same column structure and column data; only the rows are changed. So, inserting, removing and changing columns is immediately reflected on every row of every page. Each page can have a different size, and rows extracted from a page are also available when switching the active page.
Activating a page will have the effect of causing get, find, insert, remove and other table-wide operations to be performed exclusively on the current page.
For example , suppose we have a set of bank account meta-data, as the interest rate for a certain account category. Then, a table may store all the account types and their interest rates in different pages, which can be marked at different times.
The following class extends the base Table so selecting a page depends on the year for which calculations are required.
object AccTable from Table( [ "name" , "rate_act" , "rate_pasv" ] ) init // in year 2007 self.insertPage( nil , .[ .[ 'basic' 2.3 8.4 ] .[ 'business' 0.8 4.2 ] .[ 'premium' 3.2 5.3 ] ] ) // in year 2008 self.insertPage( nil , .[ .[ 'basic' 3.2 7.6 ] .[ 'business' 1.1 4.5 ] .[ 'premium' 3.4 5.2 ] ] ) end // set the current year function setYear( year ) if year < 2008 self.setPage( 1 ) else self.setPage( 2 ) end end // get rates function activeRate( acct, amount ) return amount + ( self.find( 'name' , acct ).rate_act / 100.0 * amount) end end // A bit of calcs. // what should a premium account get for 1000$ on 2007? AccTable . setYear( 2007 ) > "Premium account with 1000$ on 2007 was worth: " , \ AccTable.activeRate( 'premium' , 1000 ) AccTable . setYear( 2008 ) > "Premium account with 1000$ on 2008 was worth: " , \ AccTable.activeRate( 'premium' , 1000 )
Via the setYear method, we have been able to change the underlying data definition and transparently use new calculation parameters. This could also have been easily done with simpler methods, and would definitely also fit OOP; but suppose that it's not just a parameter change in years, but the whole logic that regulates refunds of credit accounts.
Suppose, for example , that starting from year 2007 the bank starts to add a fixed commission of $50 for accessing credit lines.
object AccTable from Table( [ "name" , "rate_act_func" , "rate_pasv_func" ] ) init // in year 2007 self.insertPage( nil , .[ .[ 'basic' { x => x * 2.3 / 100} {x => x * 8.4 / 100 } ] .[ 'business' { x => x * 0.8 / 100} {x => x * 4.2 / 100 } ] .[ 'premium' { x => x * 3.2 / 100} {x => x * 5.3 / 100 } ] ] ) // in year 2008 self.insertPage( nil , .[ .[ 'basic' {x => x * 3.2 / 100} {x => x * 7.6 / 100 + 50} ] .[ 'business' {x => x * 1.1 / 100} {x => x * 4.5 / 100 + 50} ] .[ 'premium' {x => x * 3.4 / 100} {x => x * 5.2 / 100 + 50} ] ] ) end // set the current year function setYear( year ) if year < 2008 self.setPage( 1 ) else self.setPage( 2 ) end end // get rates function creditCost( acct, amount ) return self.find( 'name' , acct ).rate_pasv_func(amount) end end // A bit of calculation. // what should a premium account get for 1000$ on 2007? AccTable.setYear( 2007 ) > "Asking for 1000$ on 2007 costed: " , \ AccTable.creditCost( 'premium' , 1000 ) AccTable.setYear( 2008 ) > "Asking for 1000$ on 2007 costed: " , \ AccTable.creditCost( 'premium' , 1000 )
The output of this program is:
Asking for 1000$ on 2007 costed: 53 Asking for 1000$ on 2008 costed: 102
Doing the same thing with traditional OOP constructs would require to program this variability since the very beginning of the project, or face the eventuality that the classes initially designed to represent bank accounts will be missing just the last sparkle of flexibility needed to implement the last twist that the marketing guys have thought of to make their bank sexier.
OOP is very flexible, and Falcon adds tons of flexibility to the base model already, providing rich functional constructs, prototype oriented OOP and so on. But at times, this just isn't enough, and thinking of some problem category as two dimensional tables (or eventually as three-dimensional tables-in-time as in this example) fits much better the more destructured and pragmatic approach to problem definition and analysis that business professionals are accustomed to use (and send down to the development departments for implementation).
Additionally, tables have some further utility worthy of consideration.
Table-wide operations
For brevity, we'll consider only one significant operation taking the whole contents of the (current page of a) table.
Other operations are described in the Table class reference, and exemplified in other manuals.
The bid method selects a row given a column. The rows must provide an evaluable item (i.e. a function) at the specified column; the item is called in turn, and must return a value greater than zero. At the end of the bidding, the item offering the highest value will be selected.
Consider the following simulation: several algorithms are struggling to win the highest possible number of auctions out of N (a number known in advance). One algorithm bids a fixed amount, another bids a random amount and a third bids a base price doubling it each if it doesn't win. After each bidding, the bid amount is removed from a pool of resources initially given to each bidder.
The first two algorithms haven't any state, so they are quite easy to code:
// a function always betting the same value function betFixed() return self.table().amount / self.table().turns end // a function always betting a random value function betRandom() return self.table().amount * random() end
The last algorithm must remember its previous bet, and if it was a winning bet or not. We'll have then a state property, called storage , where the bid is placed, and a property filled by the calling table indicating if the algorithm was winning in the previous turn or not. As this information may be interesting for every bidder, it will be placed in a table column called hasWon . Each turn, this column will be reset and the winner row will have this value set.
// a function betting each time the double of the previous time function betDouble() if self.hasWon bid = self.table().amount / self.table().turns / 2 else if self provides storage bid = self.storage * 2 else bid = self.table().amount / self.table().turns / 2 end end self.storage = bid return bid end
We'll see first how the algorithm works without the help of tabular programming, and then see how table operations can be employed to simplify it.
The table heading is like the following:
class BidTable(amount, turns) \ from Table( [ "name", "bet", "amount", "hasWon", "timesWon" ] ) amount = amount turns = turns
A simple utility method allows correct creation of our rows:
function addAlgorithm( name, func ) self.insert( 0, [name, func, self.amount, false, 0] ) end
The betting process involves calling all the algorithms, recording what they bet, provided they can pay using what's left of their initial account, and removing the bet quantity from the accounts. Then, the algorithm having bet the highest value is declared to be the winner:
function bet( time ) > "Opening bet ", time+1, ": " winning = nil winning_bid = -1
Each row is taken in turn for betting:
for row in self bid = row.bet() if bid > row.amount: bid = row.amount > @" $(row.name) is betting $(bid:.2) out of $(row.amount:.2)" row.amount -= bid
Then, if this bet is better than the others, it is recorded as the temporary winner:
if bid > winning_bid winning = row winning_bid = bid end end
Finally, it is necessary to declare the winner; to do this, we must scan all the table and set the winning flags correctly for each participant:
// declare the winner for row in self if row == winning winning.hasWon = true winning.timesWon ++ > " ", row.name, " wins this turn!" else winning.hasWon = false end end end
The game consists of playing the bet stage for the required amount of times, and picking up the final ranking.
function game() for i in [0:self.turns] self.bet(i) end > "====================================" > " Final Ranking " > "===================================="
We can use the arraySort function to sort the algorithms taking into account the times they have won. The getPage table method will take a copy of the page as it is now as an array containing all the rows, and that can be sorted leaving the actual data in the page untouched. The sorting just requires a function returning 1, 0 or -1 depending on the order that needs to be applied; the compare FBOM method applied on the timesWon table property will play the trick.
bidders = self.getPage() arraySort( bidders, { x,y => -x.timesWon.compare( y.timesWon ) } ) for id in [0:bidders.len() ] >> @ "[$(id)] $(bidders[id].name) with $(bidders[id].timesWon) victor" > bidders[id].timesWon == 1 ? "y" : "ies" end end end
There is nothing else to do but fill the table and start the game:
randomSeed( seconds() ) bt = BidTable( 100, 6 ) bt.addAlgorithm( "Mr. fix", betFixed ) bt.addAlgorithm( "Random-san", betRandom ) bt.addAlgorithm( "Herr Double", betDouble ) bt.game()
The method bidding of the Table class does more or less what the bidding function we have created does in a table: it calls a method stored in a column, recording the one that returned the highest value and returning it. However, the loop in the bid method of this sample is not just selecting the row with the algorithm returning the highest value; it also changes the status of each row. We'll need then an extra column where to store a method doing some house cleaning before and after the call of the betting algorithms:
function genericBetting() bid = self.bet() if bid > self.amount: bid = self.amount > @" $(self.name) is betting $(bid:.2) out of $(self.amount:.2)" self.amount -= bid return bid end
We may store this generic cleanup routine in another column, as column wide data, so that it will be normally used if the corresponding cell is nil when the bid is performed:
class BidTable(amount, turns) \ from Table( [ "name", "bet", "amount", "hasWon", "timesWon", genbid|genericBetting ] ) amount = amount turns = turns function addAlgorithm( name, func ) self.insert( 0, [name, func, self.amount, false, 0, nil] ) end
Notice the genbid column, which is given nil in addAlgorithm .
Now the bet function can be much simpler:
function bet( time ) > "Opening bet ", time+1, ": " winner = self.bidding( 'genbid' ) > " ", winner.name, " wins this turn!" winner.timesWon ++
Instead of clearing all the hasWon properties during the genbid calls, and setting here the value for the winner, we use this method which does the same in one step:
self .resetColumn( 'hasWon', false, winner.tabRow(), true ) end
The rest of the program is unchanged.
The method choice is similar to bidding, but it calls a generic function provided during the call. As choice calls the given function passing it one row at a time, we have to change each reference the generic betting function into:
function genericBetting( row ) bid = row.bet() if bid > row.amount: bid = row.amount > @" $(row.name) is betting $(bid:.2) out of $(row.amount:.2)" row.amount -= bid return bid end
Then, just change the self.bidding call into
winner = self.choice( genericBetting )
And now it's possible to remove the extra column “genbid”.