Delegates (function references)

Overview

There are times it would be useful to be able to store, not the result of calling a function, but rather a reference to the function itself without calling it yet. Then you can use this value to call the function later on. Or it would be useful to choose one of several functions you might want to call, store that choice in a variable, so that you can call it multiple times later.

(If you are an experienced programmer, you have probably heard of this feature or a similar feature under one of a number of names, depending on which language you learned it in: “Function pointers”, “Function references”, “Callbacks”, “Delegates”, “Deferred execution”, etc.)

Kerboscript provides this feature with a built-in type called a KOSDelegate, which remembers the details needed to be able to call a function later on.

The topic can be a bit confusing to people new to it, but this video by a prolific kOS YouTuber may help:

CheersKevin’s explanation of delegates

Note

It’s important to know before going into this explanation, that the feature described here does not work on structure suffixes as of this release of kOS. See the bottom of this page for more details.

Syntax: @ symbol

To obtain a delegate of a function in kOS, you place a single at-sign (@) to the right of the function name, where the parentheses and arguments would normally have gone, as shown below:

// example function:
function myfunc { parameter a,b. return a+b. }

// example delegate of that function.
// Note the at-sign ('@'):
set aaa to myfunc@.

When you do this, you are creating a variable of type KOSDelegate, which can be passed around and copied to other variables, sent as an argument to other functions, and so on.

Then you may call the function later on by using the :call suffix, and giving it the parameters that myfunc would normally have expected, which might look something like this:

print aaa:call(1, 2).

Here’s the full example:

function myfunc {
  parameter a,b.

  return a + b.
}

print myfunc(1, 2). // Prints the number 3, by calling myfunc now.
set aaa to myfunc@. // You don't see any effect just yet from this.
print aaa:call(1, 2).  // Now you see the number 3 printed,
                       // just like calling myfunc directly.

Omitting :CALL

You can call a KOSDelegate without the use of the :call suffix, instead just using parentheses directly abutted against the variable name like a normal function call:

function testfunc {print "test".}
set del to testfunc@.

// The following two are equivalent:

del:call().
del().

Why the ‘@’ sign?

In Kerboscript, often when you mention a function’s name and don’t provide any empty parentheses, if it’s a function that takes zero arguments, it ends up being called anyway. Thus set x to myfunc. ends up doing the same thing as set x to myfunc().. It ends up calling the function right now. This is why you must append the @ (at-sign) symbol to the end of the function name to obtain a delegate of it. It tells the compiler to suppress the normal automatic calling of the function that would have occurred if you had left it bare.

Why?

There are several reasons this feature can be useful. Some experienced programmers will already know them, but here is an example of a useful case as an illustration for people new to programming. Let’s say you wanted to start from a list of numbers, and you wanted to create a subset list of just those numbers which are negative. You might write code to do so like this:

// Just a hodgepodge list of numbers to use as an example:
local numlist is LIST(5, 6, 1, 49.1, 10, -2, 0, -12, 50, 0.3, 1.2, -1, 0).

local result is list().
for num in numlist {
  if num < 0 {
    result:add(num).
  }
}
// Now result is the subset list.

Okay, but then later let’s say you want to do the same thing, but now you want to get the subset which are integers (no fractional component after the decimal point). Then you might do this:

local result is list().
for num in numlist {
  if num = round(num,0) {
    result:add(num).
  }
}
// Now result is the subset list.

Okay, but then later let’s say you want to do the same thing, but now you want to get the subset which are even numbers:

local result is list().
for num in numlist {
  if mod(num,2) = 0 {
    result:add(num).
  }
}
// Now result is the subset list.

So you look at these three cases and think “well, gee, they’re all pretty much the same thing except for what I put in the ‘if’ check. I should probably combine them into one function.” You want to make one function that does essentially this:

function make_sublist {
  parameter
    input_list, // Full list to take a subset of.
    check.      // Condition to look for.

  local result is list().
  for num in input_list {
    if check...TO-DO, how do I do this?? {
      result:add(num).
    }
  }
  return result.
}

But how do you call it telling it what condition to look for? You’re essentially not trying to pass it a value, but you’re trying to pass it some code for it to run.

And that’s what you would use a delegate for. Here’s the full example that passes in a delegate where you tell it what kind of check you want it to do by giving it a function you want it to call for the boolean check:

function make_sublist {
  parameter
    input_list, // Full list to take a subset of.
    check_func. // pass in a delegate that expects 1 number parameter and returns 1 number.

  local result is list().
  for num in input_list {
    if check_func:call(num) {
      result:add(num).
    }
  }
  return result.
}

// Just a hodgepodge list of numbers to use as an example:
local numlist is LIST(5, 6, 1, 49.1, 10, -2, 0, -12, 50, 0.3, 1.2, -1, 0).

function is_neg { parameter n. return (n < 0). }
function is_round { parameter n. return (n = round(n,0)). }
function is_even { parameter n. return (mod(n,2) = 0). }

print "A list of all the negatives:".
print make_sublist(numlist, is_neg@). // note the '@' for a delegate of the function.

print "A list of all the round numbers:".
print make_sublist(numlist, is_round@). // note the '@' for a delegate of the function.

print "A list of all the even numbers:".
print make_sublist(numlist, is_even@). // note the '@' for a delegate of the function.

This technique can be chained together to form very powerful operations on collections and enumerations of data. You can start nesting several of these types of function calls inside each other to perform a result, such as “get the average mass of the subset of the subset of the parts on my vessel that are fuel tanks that have oxidizer in them”. There is a style of programming called Functional programming in which you are meant to try to think this way about all possible problems you are trying to solve. While Kerboscript is mostly an imperative programming language, some limited concepts of functional programming style are possible through the use of these delegates.

Anonymous functions

Kerboscript also allows you to make a Delegate from an Anonymous function (see the link for the full description of the syntax and its use), as in this example below:

set add_func to { parameter a,b. return a+b. }.
// add_func is now a KOSDelegate of the anonymous function.

When using anonymous functions like this, you don’t use the ‘@’ character because an anonymous function already is a Delegate to begin with.

It is technically possible, using anonymous functions, to actually make an entire program library out of delegates rather than normal functions.

lib_enum in KSLib

There is a library in the kslib that can be used to perform many data set enumeration operations like the one described in the above section. It was written to be released coinciding with the addition of this feature to Kerboscript. In addition to being useful as a library, it also can serve as a good list of example cases for how you can use this “delegate” feature in your own code. Please have a look at the lib_enum library in KSLib to see what it has to offer. It allows you to do things such as sorting a LIST() based on whatever comparison criteria you like, finding the minimum or maximum from a list, transforming all items in the list according to a mapping rule, finding the index of the first hit in a list that matches given criteria, and so on.

Advanced topics

Can’t call dead delegates

You might store a KOSDelegate in a global variable. Global varibles continue existing even after all the programs are done and you are back at the terminal interpreter.

This makes it possible to have a (global) variable that contains a KOSDelegate that refers to user program code that no longer exists.

But you can’t actually call that delegate. If you have such a situation, that delegate is referred to as “DEAD” and trying to call it will cause an error. You can test for this with the KOSDelegate:ISDEAD suffix.

Pre-binding arguments with :bind

A KOSDelegate allows you to create another KOSDelegate that has some of its parameters bound to some pre-set values, so you then only need to supply the remaining, unbound values when you call it. This allows you to implement certain types of functional programming styles. This is done using the :bind suffix of KOSDelegate.

Let’s say you have a function you made that draws a vector arrow from one ship to another, in a color of your choice, that looks like so:

function draw_ship_to_ship {
  parameter
    ship1,
    ship2,
    drawColor.

  local vdraw is vecdraw().
  set vdraw:start to ship1:position.
  set vdraw:vec to ship2:position - ship1:position.
  set vdraw:color to drawColor.
  set vdraw:show to true.
  return vdraw.
}

You realize that you’ll be using this a lot with the same two ships over and over. You decide to create a variation of this function that already has the two ships hardcoded to begin with, only asking you for the final color parameter.

You can do that with KOSDelegates, using the :bind suffix of KOSDelegate, as follows:

local draw_delegate is draw_ship_to_ship@.
local draw_a_to_b is draw_delegate:bind(shipA, shipB).

// Then later on you can call it with the first two arguments omitted
// because you pre-loaded them with BIND:

set greenvec to draw_a_to_b(green). // note, only passing 1 arg, the color.
set tanvec to draw_a_to_b( rgb(0.7,0.6,0) ). // note, only passing 1 arg, the color.
set whitevec to draw_a_to_b(white). // note, only passing 1 arg, the color.

Note that you can combine the two lines above that looked like this:

local draw_delegate is draw_ship_to_ship@.
local draw_a_to_b is draw_delegate:bind(shipA, shipB).

into just this:

local draw_a_to_b is draw_ship_to_ship@:bind(shipA, shipB).

When you use the at-sign(@), you are returning an object of type KOSDelegate that can be used in-line right in the expression, as demonstrated above.

Currying

It is possible to shave off exactly one parameter at a time in a chain of these :bind calls. You could do this, for example:

// V() is the built-in function that makes a vector of x, y, and z
// components.  You could bind the values one at a time as follows:
local vecx is V@:bind(10). // vecx is now a KOSDelegate hardcoding x to 10 and taking just y and z args
local vecxy is vecx:bind(5).  // vecxy is a KOSDelegate hardcoding x to 10 and y to 5, taking just the z arg
local vecxyz is vecxy:bind(1).  // vecxyz is a KOSDelegate hardcoding x to 10, y to 5, and z to 1, taking no args.
local vec is vecxyz:call(). // makes a V(10, 5, 1).

// The above chain of bindings could have been chained together on one line like so:
local vec is V@:bind(10):bind(5):bind(1):call().

The technique of transforming a function that takes many arguments into a nested succession of functions that each only take one argument has a name. It’s called Currying. (It’s named after mathematician Haskell Curry and has nothing to do with delicious spicy food).

(If anyone reading this is an experienced functional programmer and is thinking, “But :bind as described here isn’t currying”, yes, we are aware that this is correct. The KOSDelegate suffix :bind is technically not a proper “curry” because it is actually a partial function application. and thus doesn’t require that you limit it to only one parameter at a time.)

Closures

Kerboscript KOSDelegates of user functions do hold their “closure” information inside themselves. What on earth does that mean? If you haven’t heard this term before, it essentially means that the KOSDelegate “remembers” what the local variables were at the location where it was created. It is possible for the KOSDelegate you make of a function to access the local variables that only that function is allowed to see, even if you call that delegate from a “foreign” location where those variables wouldn’t normally be in scope.

Kinds of Delegate (no suffixes)

Under the hood, kOS handles several different kinds of “functions” and methods that aren’t actually implemented the same way. A KOSDelegate attempts to hide the details of these differences from the user, but one difference in particular still stands out. As of kOS version 1.1.5.2, you cannot reliably make a delegate of a suffix just yet. (This is intended as a future feature though. It’s been put off because it involves decisions that impact the future of the language and which, once made, can’t be changed easily.)

  • You can make a delegate of a user function implemented in Kerboscript code.

    function mysquarefunc { parameter a. return a*a. }
    set x to mysquarefunc@.
    set y to x:call(5). // y is now 25.
    
  • You can make a delegate of a built-in function provided by kOS itself, provided it isn’t a structure suffix.

    set r to round@.
    set s to sqrt@.
    print "square root of 7, to the nearest 2 places is: " + r:call(s:call(7), 2).
    
  • You cannot make a delegate of a suffix of a structure (yet?) in Kerboscript.

    //
    // WON'T WORK, WILL GIVE ERROR:
    //
    set altpos to latlng(10,20):altitudeposition@. // altitudeposition is a suffix of geoposition.
    print "altpos at altitude 1000 is " + altpos:call(1000).
    

    However, if you like you can make your own user function that is a wrapper around a structure suffix call, and make a delegate of that.