logo
The Falcon Programming Language
A fast, easy and powerful programming language
Location: Home page >> Falcon wiki
User ID:
Password:
 

Falcon Wiki - Survival:Functional programming


Functional programming

According to Wikipedia's definition:

Functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions and avoids state and mutable data. It emphasizes the application of functions, in contrast with the imperative programming style that emphasizes changes in state.

As Falcon provides a hybrid model, where the user can chose "how much functional" its code will be, we present some novel nomenclature to identify functional programming entities. Experts will recognize well known concepts from lambda calculus and former languages as LISP or Scheme being given new names, but as some concepts fit while other are slightly overlapping or not fully compliant with pre-existing theory, we decided to present a completely new nomenclature to avoid confusion. Otherwise, we should have borrowed names and indicated where they fit and where not. Moreover, the relatively novel concept of "optional" functional programming, where in the past it has been more or less the only paradigm of programming languages implementing it, requires a slightly different approach that justifies new names even for old things.

Before going into the new nomenclature, a bit of context information may be useful to those ones knowing functional programming. Falcon functional programming is an "impure" model (functional with "side effects") with strict evaluation. Actually, evaluation is "strict", that is, parameters are evaluated before functions, only if not explicitly required differently by the user through "constructs". Despite being impure, Falcon supports monadic programming. It is inherently possible to create pure functions with monads, and have various form of lazy evaluation, but this is currently not used by the VM to optimize calculus and is not "forced". Means to achieve this are just provided to the programmers for their usage.

The theory

In this paragraph we explain formally how functional paradigm works in Falcon. In the next paragraph, we'll proceed with samples and introduce terminology little by little, so it is not necessary to read and understand this paragraph to proceed further. However, it is strongly advisable to gain confidence with the formalization of the paradigm, as understanding its working model may spare from some misunderstanding and headaches in using this features. In case this paragraph seems too abstract, it may be a good idea to start from the example and return to read it when the practice is mastered.

In short, the model used by Falcon functional programming is called Sigma to Alpha. The functional evaluation is the process to apply the Sigma-to-Alpha function to a list of zero or more items, each of which is called "topic". A topic may be either an Alpha, in which case Sigma-to-Alpha applied to that topic gives the same Alpha, or a Sigma, in which case it is Sigma-evaluated to obtain a result topic. The result may be an Alpha or again a Sigma, in which case Sigma-to-Alpha may be applied again until an Alpha result is reached. A list of topics is an Alpha if and only if each elements is an Alpha; this implies that topics containing a list of topics have Sigma-to-alpha applied to each one of their elements.

Sigmas are sequences composed of an application Kappa followed by zero or more topics. Sigma-evaluation of a Sigma is applying K to its argument (the list of topics), each of which being Sigma-to-alpha processed before K -application.

Applying the Sigma-to-alpha function to a sequence of topics is called Sigma-evaluation, and applying a K to its topics is called K-reduction.

Let's discuss this topic in a bit less cryptic language. In functional context, we want to determine the final value of a sequence of data, which may contain functions or terminal data. "Determining" this value may be interesting both for the value itself or for the process that is performed to achieve the value, as this may involve calling functions. However, let's focus on determining the value. We have an item that must be evaluated in functional context (a topic), or sigma-evaluated . It may be itself an atom, or a list of atoms not needing simplification. Those two entities are indicated with the term "alpha", initial letter of "atom" in the Greek Alphabet.

In a sequence, the sigma-evaluation is applied to each element to determine if it can be reduced, while if the topic was not a list in the first instance, the value is already known and corresponds to the value itself.

If the first element of a sequence to be sigma-evaluated is a Falcon callable item (Kappa for Greek initial of "callable"), then the sequence must be simplified (hence the name "Sigma" from the Greek letter "S"). In that case, the other topics in the sequence are themselves Sigma-evaluated, and when they are all reduced, the list of topics is passed to the K callable symbol. The return value is substituted with the previous Sigma in the sequence where it was found.

The return value of a K-reduction needs not to be an alpha. It may be still a Sigma, in which case the topic may be evaluated again, if needed and wished.

A special class of K applications is called Eta (extra-Kappa). The K reduction of Etas modifies the way Sigma-evaluation is performed. Actually, when a Sigma-evaluation finds a Sigma whose K is an Eta, it doesn't sigma-evaluate the other topics in the list, and passes them unchanged to the Eta for K-reduction. Then, it's the Eta that, if needed, starts a subsequent Sigma-evaluation on its parameters, or on just some of its parameters. This is called Eta-reduction, and form the basis of the implementation of functional "constructs" in Falcon.

Actually the concept of Sigma currently overlaps 1:1 with the callable arrays, as the arrays are the only sequence known as Sigma at this stage of development, but we'll keep the term "sequence" through, as the model shall be extended to various kind of sequences.

Evaluating in functional context

Falcon, by default, is not in functional mode. Functional evaluation, or Sigma-evaluation, is invoked by using special functions that instruct the VM to begin to consider items in Sigma-evaluation mode. Those special functions are called "ETA" (from the Greek letter "eta"), and have a special significance for Falcon functional model. When the Sigma-evaluation meets an Eta function, the VM drops sigma mode and lets the Eta function handle the situation. Then, the Eta usually instructs the VM on further sigma-evaluation needed.

The most simple eta function is the eval, which sigma-evaluates its contents and returns the evaluation result.

> eval ( 1 )

Not surprisingly, this results in 1; in fact, 1 is an atomic value, whose sigma-evaluation value is exactly itself. Sigma-evaluation recognizes special values only to certain sequences, and specifically to callable arrays.

Sigma-evaluation on normal arrays results in sigma-evaluation of each element:

inspect( eval( [1,2,3] ) ) 

will result in [1,2,3]; however, consider the following case (we'll be using dot-square notation for arrays from now on):

function returnTwo()
   return 2
end 
 
inspect( eval( .[ 1 .[returnTwo] 3 ] ) ) 

As the sigma-value of an array is the sigma-evaluation of each element, the [returnTwo] array gets evaluated. It's not a simple array; the evaluator finds out that its first (and only) element is a callable item, and to reduce it to a plain value the function gets called. The return value is substituted in place of the function that was called, so the final evaluation results in [1,2,3].

A callable item as first element of an array is called Kappa, and calling the Kappa with its parameters is called K-reduction.

When the K is not an Eta function, that is, if it's a plain function that has no special meaning in functional evaluations, all its parameters get sigma-evaluated before it is called. For example :

function sum(a, b)
   return a+b
end 
 
inspect( eval( .[ 
      .[sum .[sum 2 2] 4]
      .[sum 3 .[sum 3 3]]
      ]))

In this case, the inner sums are K-reduced and then they are passed to the outer sum for K-reduction. The result of this evaluation is an array [8, 9].

Another common Eta function is iff (functional if). This function Sigma-evaluates the first parameter. If the result is true, according to Falcon truth test, then the second parameter is Sigma-evaluated and the result of the evaluation is returned. If it's false, an optional third parameter will be Sigma-evaluated and returned.

For example :

function greater( a, b ): return a > b 
 
printl( iff (
  .[ greater .[random] 0.5],
  "you were lucky",
  "you were unlucky" ))

Falcon evaluates as true non-zero numeric values, non empty strings, arrays and dictionaries and any object. Zero, nil empty strings, arrays and dictionaries are considered false.

Notice two important aspects of the Falcon functional model. First, it is an impure model, meaning that K-reduction may have "side effects". In other words, they may alter the state of the program (as in creating items), and they can have input or outputs. Second, normal expression evaluation is also available in functional context; it is not strictly necessary that every parameter of an Eta-function gets evaluated through Sigma-evaluation; it's just an option at the programmer's disposal.

Given these two features, the above code can be rewritten as:

iff ( random() > 0.5,
        .[printl "you were lucky"],
        .[printl "you were unlucky"] )

Expression evaluation is performed before Sigma-evaluation, and is performed on every parameter of any function, regardless of their being Eta-functions or not; this means that it is possible to prepare simple values using imperative programming, and use them in a later functional context, as the above example has shown. However, be careful not to confuse imperative evaluation with K-reduction. Rewriting the above code as follows:

iff ( random() > 0.5,
        printl( "you were lucky" ),
        printl( "you were unlucky" ) )

both the printl calls would be performed, and then the Sigma-evaluation of one of their return values would end up being returned by iff. It is unlikely that this is the desired effect.

At times, the caller is interested in having a Sigma expression returned for later evaluation rather than evaluated on the spot. The lit Eta-function just passes down its parameters, interrupting the Sigma-evaluation. For example, take the following code:

picked =  iff ( random() > 0.5,
        .[ lit .[printl "you were lucky"]],
        .[  lit  .[printl "you were unlucky"]] ) 
... 
picked() 

This behavior is actually replicated by the Eta-function choice, which works like iff, but doesn't Sigma-evaluate the second and third parameter, returning one of them unevaluated. For example:

choice( random() > 0.5, 
        .[printl "you were lucky"],
        .[printl "you were unlucky"] )()  // notice the () call operator.

Evaluation operator

Since version 0.8.12, Falcon also provides an eval operator, formed by a cap-star sequence, which performs functional evaluations on sequences. It's an unary operator returning the item as-is if it's not callable, calling it if it's a function or evaluating it if it's a sequence:

>  ^*  1   // 1 
 
function  test() 
  > "Test"
    return  2 
end 
 
^*  test   // calls test() 
inspect( ^* .[ 1 .[test] 3 ] )   // evaluates 1,...,3 sequence 

This operator can be used only to initiate functional sequences, but it is more efficient than calling the eval function. It is also meant to retrieve values that may be either values or functions that return a value in an efficient way:

// Instead of this... 
 
// Should I get a value or call a property giving me the value? 
if  obj.prop.type() == FunctionType 
   value = obj.prop()
else 
   value = obj.prop
end 
 
// ...you can just do 
value = ^* obj.prop 

Multiple evaluation

The Eta-functions all and any take a sequence of items as parameters. The all Eta-function returns true if the results of all the items in the sequence are true, while any returns true if anyone of the items is true. Each item is Sigma-evaluated separately; so the net effect of all and any in functional context is that to perform Sigma-evaluations on a sequence of items respectively while and until an evaluation result is true.

Consider this example:

function falsifier(): return false
function verifier(): return true
 
any( .[ 
   .[falsifier .[printl "First call..."]]
   .[verifier .[printl "Second call..."]]
   .[falsifier .[printl "Third arg..."]]
]) 

As falsifier is not an Eta-function, its arguments are sigma-evaluated, with the effect to print the desired string; then the falsifier is called (with a value that is ignored), and being false it forces the evaluation of the next statement. The verifier returns true; once Sigma-evaluated its parameters, the function interrupts evaluation, and the third element is not K-reduced.

There are also two Eta-functions named allp and anyp , which work respectively as all and any , but they doesn't take a sequence as unique parameter; instead, they repeat Sigma-evaluation directly on each of their parameter respectively while and until an item evaluates to false. They are provided to avoid redundant array declarations, especially when all and any are themselves part of Sigmas:

iff ( reverseCalc,
   .[ any .[third second first]],
   .[ anyp first second third] ) // notice anyp vs. any

These four little functions can be also used to shorten long and if and/or sequences. For example , an if statement like the following:

if cond1 and cond2 and cond3 and cond4
   ...

may be even more readable if written as

if allp( cond1, cond2, cond3, cond4)
   ...

and even long sequences of logical operators may be changed into all/any sequences:

if (condition(1) and condition(2) and (c1 or condition(3))) or \
   (c2 and condition(4))
... 
end 
 
// same, but using anyp/allp 
if  anyp ( .[ allp .[condition 1] .[condition 2] .[ anyp c1 .[condition 3]] ],
         .[ allp c2 .[condition 4]])
... 
end 

Cascading

The Eta-function cascade evaluates a sequence of K-applications one after another, where the result of the first evaluation is feed as parameter of the second one and so on. In other words, cascade executes a predefined list of functions one after another, passing the result of the former as the parameter of the latter. The first K-application in the list receives all the parameters given to cascade, and the return result of the last K-application is finally returned by cascade. As an example, it's possible to use the classical mathematical definition of absolute value as the square root of the square of a number.

function square( x ): return x * x
function sqrt( x ): return  x**0.5
> cascade( .[square sqrt], 5 )      // prints 5
> cascade( .[square sqrt], -5 )     // prints 5 (again)

Cascade becomes useful when used create new functions by simply concatenating existing ones. For example , to create a "classical abs" function that works as the above example, the following code can be used:

cascade_abs = [cascade, [{ x => x*x}, {x => x ** 0.5} ]] 
> cascade_abs( 5 )      // prints 5
> cascade_abs( -5 )     // prints 5 (again)

The items in the sequence of K-applications need not to be just K, they can be complete Sigma (that is, i.e. callable arrays), so it's possible to pre-cache parameters that will be passed to functions in the sequence before the value coming from previous evaluation. For example , the following sequence calculates the percent ratio between two numbers.

function factorize( factor, x ): return x * factor
function proportion( whole, part ): return part / whole
percent = .[cascade .[proportion .[factorize 100]]] 
 
// if the whole is 2, and the part 1, what % is it? 
> percent( 2, 1 ), "%"                                      // 50%

The factorize K has been given 100 as the first parameter; the second will be the one received as result of the proportion function.

Being faithful to the impure character of Falcon functional programming model, cascade accepts functions having a pure side-effect, that is, accepting the value or values given as parameters, but refusing to produce a result. If a function returns an out-of-band item, then it is considered as if not called, and the same parameter (or parameters) that were given to it by cascade are given to the next one. In the following example, we inspect results as they are being formed:

function factorize( factor, x ): return x * factor
function proportion( whole, part ): return part / whole
 
function inspector( phase )
   >> phase, ": "
    for id in [1:paramCount()]
      >> parameter( id )
      formiddle: >>", " 
      forlast : > "."
    end 
      // returns an out-of-band item 
    return  oob( nil ) 
end 
 
percent = .[cascade .[  
   .[inspector "Begin"]
   proportion
   .[inspector "After proportion"]
   .[factorize 100]
   .[inspector "After factorize"]  ]
] 
 
> percent( 2, 1 ), "%" 

The oob() function turns any item into an out-of-band item; the out-of-band characteristic of an item is a marker, a signal that the upstream function wants special (namely out-of-band ) processing for the item being returned. Scripts can use the out of band feature too. We'll talk more specifically of out-of-band items in a later paragraph.

When a function in the sequence returns an out-of-band item, it instructs cascade not to use its return value, and to pass the same parameters it has received to the next function.

List evaluation

Some functional programming oriented functions (some of them being Eta-functions, other being normal functions) operate repeatedly on a list of elements. They are: the normal functions map , filter and reduce and the Eta-Functions dolist and xmap .

The map function transforms a list into another through a so called "mapping function"; it returns a sequence where each item is the return value of the mapping function applied to the original item. For example , to obtain the square of three numbers using map, it is possible to do:

s1, s2, s3 = map( { x => x * x }, [1, 2, 3] ) 

The variables s1 , s2 and s3 will respectively contain 1, 4 and 9.

The filter function stores the original items passed in an array in the result sequence if a "filtering" function applied to them returns true. For example , to filter a list so that only even numbers are left in, the following code can be used:

function passPair( x )
    return x % 2 == 0
end 
 
vals = filter( passPair, .[1 2 3 4 5 6] ) 
inspect( vals ) 

The vals array holds now the values 2, 4 and 6.

The functions filter and map can be combined to filter an array and change its value, but map may be also used to filter the mapped sequence: if the mapping function returns an out-of-band item, the value is skipped. The example can be rewritten as:

function mapPair( x )
    if x % 2 != 0: return oob(nil)
    return x
end 
 
vals = map( mapPair, .[1 2 3 4 5 6] ) 
inspect( vals ) 

The reduce function applies a sequence of values to a reducing function one at a time; it also passes the previous return value of the reducing function to the next call as the first parameter. An extra initialization value can be optionally given; if present, that initialization value will be used as the first parameter in the first loop, else in the first loop the reducing function will be called with the first two elements in the array.

The final return value of the reduce function is the last return value of the reducing function.

For example , the reduce function may be used to sum all the numbers in an array:

function sum( x, y ): return x + y
> reduce( sum,           // the function
  [1, 8, 4, 9 , 3, 2],   // the array to be reduced
  0                      // the initial sum value
) 

Using reduce in a bit of a smart way, it is possible to do interesting things such as calculating the mean value of a sequence:

function partialMean( x, y )
   static: count = 0
 
   // use an oob value to control start...
   if isoob( x )
      count = 0
      return y
    end 
 
   // ... and to control the end
    if isoob(y): return x / count
   
   // normally, count and sum
   count++
   return x + y
end 
 
function mean( sequence )
   return reduce( partialMean, sequence + oob(nil), oob(nil) )
end 
 
> mean( [ 1,2,3,4,5,6,7,8,9,10] ) 

The xmap Eta-Function works as map , feeding a list of items as parameters of a given function one at time and building an array of results, but it has a relevant difference: it is an Eta-Function, so it takes care to Sigma-reduce its parameters by itself. Each item of the list to be mapped is Sigma-reduced before being fed to the mapper, and if the mapper is not a simple function, but a sigma, it is Sigma-reduced too before being used.

In other words, it is possible to map function results, and provide a variable mapper (the result of the sigma function). If one of the items in the list, or the evaluator itself, is not to be evaluated, it can be passed to xmap through the lit Eta-Function.

Finally, the dolist Eta-Function works similarly to xmap , feeding a Sigma-reduced item from a sequence in as parameter of a given function, but it doesn't create a result map. The dolist function is useful when it's necessary to process the list or to generate some non-functional effect in the processing function, avoiding paying extra unneeded memory. For example , the following example creates a even number counter:

function countPairValsIn( sequence )
   // local function cp
   cp = function ( x )
   static : count = 0
 
   if isoob(x)
      val = count
      count = 0
      return val
   end 
 
   if x%2 == 0: count++
      return  true 
   end 
 
   dolist ( cp, sequence )
   return cp( oob(nil ) )
end 
 
> countPairValsIn( [1, 2, 3, 4] )  // will print 2

Late bindings

So far we have been using external item references to provide functional evaluations with symbols that may be changed. Although effective, this technique precludes the possibility of creating a single execution unit consisting only of functional code. Also, sharing code across coroutines (logically parallel program execution units) could be tricky.

Falcon proves an item type called "late binding" which is specifically designed to be bound to a value only during array-calls and functional evaluation. Late bindings can be generated at any time through the & operator and the lbind function.

For example :

counter = lbind( 'counter' ) 
> counter 

Late bindings become interesting when merged with arrays. Arrays can be assigned local variables through the '.' operator; then, a late binding can be applied to retrieve that value and pass it to the function during the call or the evaluation.

For example :

calling = .[printl &value] 
calling.value = "Hello world" 
calling()                      // execute directly
eval( calling )                // evaluate in functional context

So, the value local symbol will be bound only at call, and it may be changed between calls, or changed directly in between. For example :

function inc( x ): x+=1 
 
calling = []                   // our execution canvas
calling.value = 10             // initializing
calling += .[ .[inc &value]]   // adding an increment call
calling += .[ .[inc &value]]   // and another one
eval( calling ) 
> "After increment: ", calling.value 

The binding context is always the outermost sequence in a computation. Notice that the calling array in the above example includes two sub-arrays, both calling inc , but the &value late binding refers to calling and not to them.

As late bindings are items, they can be stored and applied separately.

For example :

calling = .[ printl ] 
calling.even = "Even number: "
calling.odd = "Odd number: "
 
for i in [0:4]
   valsym = i % 2 == 0 ? &even : &odd
   (calling + valsym)( i )
end 

In the above example, a callable array is first created, and two symbol values are stored into as even and odd . Then, one late binding, either even or odd , is set in valsym and a new array composed from the original and the applied late binding is created and called at each time. The result is:

Even number: 0
Odd number: 1
Even number: 2
Odd number: 3

Self-referencing local values.

Late bindings can have any value, including functions or callable sequences. For example :

sequence = .[ &multiplier 3 ] 
sequence.multiplier = .[ {x => x * 3} ] 
> eval(sequence)  

Notice that when calling directly sequence in the above example is not possible as &multipler isn't bound to a callable item (the code block) until the evaluation context is started or the execution is performed.

Considering the fact that local values can store arbitrary functions, and that they can be used separately from the evaluation of the sequence, sequences can also be used to produce flexible object-oriented like functional operations. The special binding value &self resolves into the sequence leading the evaluation context, and can be used to reference the "calling sequence".

For example, it is possible to use part of the current functional sequence as a value:

load funcext           // for the "at" function

vector = .[.[printl .[at &self 1] ] 100]  // print [1] of this sequence
eval( vector )                            // 100     
vector[1] = "New value"                   
eval(vector)                              // "New Value"

Of course, &self1 wouldn't work as &self gets bound only during an evaluation, while using [] on it would try to access the current value of self.

Finally, the self keyword (notice the lack of '&') can be used to access the host sequence when calling methods created as functions bound in the sequence. The following example prints each item in vector.

vector = .[ 'a' 'b' 'c' 'd' ] 
vector.display = {=>dolist( .[printl "Item... "], self ) }
vector.display()  

In short: &self resolves in the currently being evaluated sequence (during evaluations), while self accesses the owning sequence in bound methods during calls.

Parametric evaluation

At times it becomes useful to modify the evaluated sequences through different executions. It is generically possible to insert variable references in the sequences and modify their values, or to modify directly the sequences (by changing their contents, as they are arrays and can be modified), but the simplest way is to use late bindings, which allow to modify the atoms stored in a sequence as the evaluator reaches them.

One special kind of late bindings are the parametric bindings. They refer to parameters given to the evaluated sequence by the most internal eval call (or generically, a functional parametric evaluator), and are formed by the & symbol and the number of the parameter (1 based). So

eval( .[ printl ">" &1 "<" ], "The thing to be printed" )

Parametric evaluation comes very handy when building flexible functions on the fly:

looper = { limit, seq =>
   for n = 1 to limit
       eval( seq, n, limit )
   end }

looper( 5, .[ printl "loop " &1 " out of " &2 ] )

Functional loop

It is even possible to create loops using functional constructs. The floop (functional loop) Eta-Function takes a sequence given as parameter, and iterates forever calling each callable item in the list one after another. When the last callable item is processed, the first one is executed again. For example , the following loop prints forever "one, two, three".

floop( .[ 
   .[ print "first, "]
   .[ print "second, "]
   .[ printl "third."]
]) 

You can interrupt this endless loop by pressing CTRL+C. There are times when endless loops are needed, but usually, it is useful to provide a condition for the loop to terminate. If a function in the sequence returns an out-of-band 0, the loop is interrupted, and if it returns an out of band 1 the loop is restarted. In other words, out-of-band zero works as functional break, while out-of-band 1 is interpreted as functional continue.

The following example prints the numbers from 1 to 10.

i=0 
.[floop .[ 
  .[inc $i]
  .[printl $i]
  .[iff .[ge $i 10] oob(0) ]
]]() 
 
function inc(x): x = x + 1 
function ge(x, y): return x >= y 

First, an "i" variable is created with an initial value; then, the floop function is called iterating over a list of three Sigmas: the first increments its parameter, and gets called with a reference to "i", the second prints the variable and the third checks its value; if it's greater or equal than 10, an out-of-band 0 is returned, breaking the loop.

Notice that the "i" loop variable is always used by reference in the list. The list gets first created and then evaluated; when the variables inside the list may change during evaluation, it is necessary to use a reference to them, as the list gets created only once and then evaluated. Variables not passed by reference are evaluated at list creation, or in other words, the evaluation access them by value using the value they had at list creation.

The same loop may be rewritten using code blocks instead of fully declared functions as follows:

i=0 
.[floop .[ 
  .[{ x=> x=x+1 }  $i]
  .[printl $i]
  .[iff .[ {x => x>=10} $i] oob(0) ]
]]()

More functional loop

Another function performing functional loops is times . The times function is extremely flexible: it can perform ranged loops (as for/in) and it can either execute repeatedly functions or evaluate repeatedly sequences. It passes the current value of the loop variable as the first parameter of functions, or as the &1 parametric evaluation parameter if receivs a sequence. As in floop, oob(0) and oob(1) have special meanings, causing the loop to be respectively broken or restarted.

For example:

// Times used with functions
times( 5, { x => printl( "As a function: ", x)} )

// Times used with sequences
.[times ,[2:11] .[                        // Notice the ',' to tell the range
  .[iff .[{ x=> x % 2 == 1}  &1] oob(1) ] // skipping impairs
  .[printl 'Counting even... ' &1]
]]() 

The times function can be applied also to integers (will loop from 0 to the number excluded). It also provided as a method of both ranges and integers; so the above can be rewritten as:

// using the step variable
[2:11:2].times( .[printl 'Counting even... ' &1] ) 

Integers have also downto and upto methods, working similarly:

5.upto(10, { n => printl( "Loop up: ", n )} )
10.downto(5, .[ printl "Loop down: " &1 ] )

Out of banding in detail

It is often useful to give items a "special meaning", so that when they travel in functional sequence evaluations, or as we'll see, when they travel along in messages, or just when they are to be returned by functions in your scripts.

Every Falcon item can be associated with a out of band status flag which can be given, removed or queried through a set of three functions and four operators.

Operators are faster and possibly more compact and readable than functions, but functions are useful in functional sequences where the value of the item on which to perform the out of banding isn't ready or known from the beginning.

The operations on out-of-band items (oob) are the following:

  • Assigning the out-of-band status: ^+ operator or oob function.
  • Removing the oob status: ^- operator or deoob function.
  • Checking the oob status (evaluates to true if oob is given to the item): ^? operator or isoob function.
  • Reversing oob status (removing it if given, giving it if ungiven): ^! operator.

The following example show a script-level usage of out of band items through operators.

function getNum()
   num = random(1,10)
   if num > 4: return ^+ num
   return num
end 
 
for i = 1 to 10
   > "At round ", i, " the number is ", \
   ^? getNum() ? "less." : "greater."
end 

In this example, the function getNum() also performs a check on the random number, and if it's greater than 4, it marks it as out of band via the ^+ operator. The calling program can check the outcome of this test via the ^? operator, and print a different string depending on what happened. This example is trivial, but in case the check is complex or if the remote code is the only part of the program that could easily perform the check (i.e. because the information needed to decide the status of an item are transient and destroyed soon after), then this technique comes extremely useful.

There are cases in which a function return, or a generic processing result, may legally be any Falcon item, including, for example, nil. In those cases, having an extra flag on the returned item if it is to be considered valid, of if the process evaluation was faulty or couldn't be completed correctly can be extremely handy and can obviate the need of setting validity flags elsewhere (in a by-ref parameter, in properties or in global variables).

While it is sometimes more appropriate to raise errors in those cases, in other cases the raise semantic may not be the correct solution to handle the caller the notion of an impossible evaluation. This is the case with functional sequences and message programming, where the caller may not be prepared to deal with exceptions coming from the processor code, but at times it also becomes useful in procedural and object oriented programming. In fact, a raised exception has the semantic value of signaling a problem, while an "empty result" or "result to be specially considered" may not necessarily be due to a fault in the processing algorithm or in the input data. It may just be one of the possible and legal outcomes of the processing.

Out of banding and procedural programming

Out of band construct comes also extremely handy in procedural programming. For example, functions with static data may reset or change their state through out-of-band paramters.

Suppose we want to create a generator, returning an item from an array. We can replenish the generator by sending it a new array, as in the following example:

function generator( data )
   static
      array = nil; pos = 0
   end

   if data
      array = data
      pos = 0
      return
   end
   
   if pos >= array.len(): return nil
   return array[pos++]
end

generator( [1,2,3] )
> generator()  // 1
> generator()  // 2
> generator()  // 3
> generator()  // nil...

Now, the generator may be a bit smarter, and allow to reset the internal counter if it receives an out of band integer number; let's change if data with...

   if isoob(data)
      pos = data
   elif data
     array = data
     pos = 0
   end

This new function can be invoked as follows:

generator( [1,2,3] )
> generator()  // 1
> generator()  // 2

generator( oob(0) )    // reset
> generator()  // 1
> generator()  // 2

The for/in loop understands generators; it respects formiddle and forlast blocks by caching in advance the results of generator calls, and terminates the loop when an oob(0) value is returned. For example, changing the return nil into return oob(0) in the above generator, it's possible to feed the function in a complete for/in loop:

function generator( data )
   static
      array = nil; pos = 0
   end

   if data
      array = data
      pos = 0
      return
   end
   
   if pos >= array.len(): return oob(0)
   return array[pos++]
end

generator( [1,2,3] )
for elem in generator
   forfirst: >> "Begin: "
   >> elem
   formiddle: >> ", "
   forlast: > "."
end

This will print "Begin: 1, 2, 3."

Generators could be created also with closures, methods, callable arrays, or in general with any callable Falcon item returning an oob(0) element to terminate the loop.


Navigation
Go To Page...

Loading

Elapsed time: 0.029 secs. (VM time 0.021 secs.)