next up previous
Next: Recursion Up: IBAL Tutorial Previous: Blocks and Libraries

Functions

Functions are the lifeblood of a programming language, and the same is true of IBAL. Without them, only static, rigid models can be defined. With functions, one can build reusable model components and combine them in all sorts of ways. IBAL, in fact, is modeled on the functional programming style, in which functions play a central structural role. As we will see, IBAL functions are a tremendously powerful tool both for representing models and for controlling the inference process.

A simple way to define a function is through a function definition, which is a form of declaration. A function definition has the form

f(x_1, ..., x_n) = e
where f is the name of the function, the x_i are the formal arguments, and the expression e is the body of the function. The formal arguments can be used as variables in the body of the function. In addition, the body may contain free variables that are defined before the definition of the function.

A function may be applied to arguments in a similar way to many other programming languages. An application is an expression of the form e_0 (e_1, ..., e_n). This can be understood as the experiment that first evaluates e_0 to produce a function f that takes n arguments, then evaluates each of the e_i to obtain the value of the corresponding argument to f, and then evaluates the body of f, using the obtained values for its arguments.

Functions are the programming language analogues of class models used in object-oriented Bayesian networks. In the previous section, we saw how to define a probabilistic object using a block. A class of object can be defined simply by creating a function that returns such a block. Once we have defined a class model as a function, we can apply the function to generate instances of the class, which are particular objects whose model is the class model. For instance, we can modify the example from the previous section so that each of the object definitions is now inside a function. We can then generate particular professors, students, courses and so on. This example can be found in oobn2.ibl.

field() = { hard = flip 0.5; high_standards = flip 0.3 };

o_chem = { hard = true; high_standards = true };

basket_weaving = { hard = false; high_standards = false };

prof(field) = {
  private mean = flip 0.1;
  tough = mean | (field.high_standards & flip 0.8);
  clear = ~ mean & (if field.hard then flip 0.5 else flip 0.8);
  field = field
};

course(prof, field) = {
  well_taught = (prof.clear | ~ field.hard) & flip 0.9;
  prof = prof;
};
    
student() = {
  smart = flip 0.4; 
  hard_working = flip 0.7;
  good_test_taker = if smart then flip 0.8 else flip 0.3 
};

performance(student, course) = {
  private understands = 
    if student.hard_working
    then case <student.smart, course.well_taught> of
    # <true, true> : flip 0.95
    # <false, false> : flip 0.35
    # _ : flip 0.7
    else case student.smart of
    # true : flip 0.7
    # false : flip 0.1;

  exam_grade = 
    case <understands, student.good_test_taker> of
    # <true, true> : dist [ 0.6 : 'A, 0.3 : 'B, 0.1 : 'C ]
    # <true, false> : dist [ 0.4 : 'A, 0.2 : 'B, 0.4 : 'C ]
    # <false, true> : dist [ 0.5 : 'A, 0.2 : 'B, 0.3 : 'C ]
    # <false, false> : dist [ 0.1 : 'A, 0.4 : 'B, 0.5 : 'C ];

  homework_grade =
    case <understands, student.hard_working> of
    # <true, true> : dist [ 0.6 : 'A, 0.3 : 'B, 0.1 : 'C ]
    # <true, false> : dist [ 0.4 : 'A, 0.2 : 'B, 0.4 : 'C ]
    # <false, true> : dist [ 0.5 : 'A, 0.2 : 'B, 0.3 : 'C ]
    # <false, false> : dist [ 0.1 : 'A, 0.4 : 'B, 0.5 : 'C ];
};
  
field1 = field();

prof1 = prof(basket_weaving);
prof2 = prof(field1);

course1 = course(prof1, o_chem);
course2 = course(prof2, field1);

student1 = student()

We have seen how to create a function using a declaration f(x_1, ..., x_n) = e. One can also define a function directly within an expression. There are two ways of doing this. One uses the fun notation (like lambda in some programming languages). The general syntax is fun x_1,...,x_n -> e where x_1,...,x_n are the formal arguments, and expression e is the body. The value of this expression is a closure value, that can later be applied to arguments.

The second way to define a function is to use the fix notation. In this notation, the function is given a name, which can be used recursively inside the function. Recursion will be discussed more in the next section. The syntax is fix f(x_1,...,x_n) -> e where f is the name of the function, x_1,...,x_n are the formal arguments, and expression e is the body. Again, the value of this expression is a closure.

The declaration f(x_1, ..., x_n) = e is in fact equivalent to the definition f = fix f(x_1,...,x_n) -> e. One can also define a function within a let expression, using the syntax

let f(x_1,...,x_n) = e_1 
in e_2
Again, this is equivalent to
let f = 
  fix f(x_1,...,x_n) = e_1 
in e_2

Note that in contrast to other functional languages, but similar to many other programming languages, functions take multiple arguments, and the arguments are placed inside parentheses. This is consistent with the application notation, in which the function arguments must also be listed in parentheses.

A function may take zero or more arguments. Functions of zero arguments are very useful in our setting. They can be viewed as stochastic generators that can be applied many times to generate values from some distribution. For a very simple example (in coins1.ibl), here are functions representing the toss of fair and biased coins, together with the generation of two fair coins:

fair () = dist [ 0.5 : 'heads, 0.5 : 'tails ];
biased () = dist [ 0.75 : 'heads, 0.25 : 'tails ];
x = fair ();
y = fair ()
Note the () after the names fair and biased in their definitions, indicating that they are functions of zero arguments. Similarly, the application of fair in the definitions of x and y consists of the name of the function followed by its zero arguments in parentheses. In this model, x and y are two independent instantiations of a fair coin, as can be seen by querying <coins1.x, coins1.y>.

Recall that the general form of an application is e_0 (e_1, ..., e_n). In particular, the function to be applied is itself indicated by an expression, e_0. Since e_0 is a general IBAL expression, its outcome may be stochastic. In that case, we have functional uncertainty, or uncertainty over which function to apply.

Here is a simple example. Imagine an experiment where you pick a coin out of bag containing both fair and biased coins, and then toss it. This can easily be expressed using functional uncertainty, as in the following example (coins2.ibl):

fair () = dist [ 0.5 : 'heads, 0.5 : 'tails ];
biased () = dist [ 0.75 : 'heads, 0.25 : 'tails ];
x = dist [ 0.5 : fair, 0.5 : biased ] ();
y = dist [ 0.5 : fair, 0.5 : biased ] ()

Here, x and y are generated by applying a function, but we are uncertain as to whether that function is the fair or biased function. It can easily be checked that x and y are still independent in this example.

Functional uncertainty combines well with another powerful feature of IBAL, higher-order functions. One of the main features of functional programming languages is that functions are values just like any other. They can be passed to other functions as arguments, returned from functions, and contained in data structures. Continuing the coins example, we can imagine the experiment where a coin is picked from a bag, and then tossed many times. We can capture this by having a variable represent the function to apply on each toss. Furthermore, we can imagine running the experiment many times, each time picking a coin out of the bag, and tossing it repeatedly. To capture this, we create a new function pick corresponding to the act of picking a coin from the bag, that itself returns a function. The example, in coins3.ibl, is as follows. The variables c1 and c2 represent two separate picks from the bag. Their values are functions that can be applied to generate the results of coin tosses. Note that x and y are no longer independent of each other, since they are correlated by the value of c1. However they are conditionally independent given c1, and they are both independent of z which is the result of the toss of a different coin.

fair () = dist [ 0.5 : 'heads, 0.5 : 'tails ];
biased () = dist [ 0.75 : 'heads, 0.25 : 'tails ];
pick () = dist [ 0.5 : fair, 0.5 : biased ];
c1 = pick ();
c2 = pick ();
x = c1 ();
y = c1 ();
z = c2 ()


next up previous
Next: Recursion Up: IBAL Tutorial Previous: Blocks and Libraries
Avi Pfeffer 2006-11-19