How can executable code be passed to a method?

Motivating Example

Take for example, passing a comparator to an insertSorted function.

sorted := List clone
list(3, 1, 4, 2, 6, 5) foreach(x,
    sorted insertSorted(x, /* comparator */)
)

Where insertSorted is implemented as follows.

List insertSorted := method(x, /* comparator argument */,
    self foreach(i, y,
        if(/* use comparator to do: x < y */,
            insertAt(x, i)
            return self
        )
    )


    append(x)
    self
)

There are at least four straight forward ways:

Each approach has its advantages and disadvantages.

Comparison of Approaches

Blocks

# caller
sorted insertSorted(x, block(a, b, a < b))


# method argument list
List insertSorted := method(x, cmp, ...)

# using comparator
cmp call(x, y)

Blocks can be handy to use, but they have a reference to the scope they were created in. The created scope has a reference to it's caller, and it's caller to it's. Each of these callers have references to all of their local variables.

If the life span of the block will be longer than the method it was created in, then using blocks can prevent the call stack and everything referenced by it from being garbage collected.

The main case when the life span of a block is longer than the method that creates it is when the block is used as a call back. For the reason above, it is not recommended to use blocks for callbacks.

Methods

# caller
sorted insertSorted(x, method(a, b, a < b))


# method argument list
List insertSorted := method(x, cmp, ...)

# using comparator
perform("cmp", x, y)
#or
getSlot("cmp") call(x, y)

Passing around methods requires particular care, as using the variable that holds the method will trigger the method to be called. Instead, getSlot("variableName") must be used when handling the variable.

When writing code that might have a value as a method, it is good practice to use getSlot("variableName"). Build-in methods often uses getSlot to prevent methods from being called by mistake.

Call Object

# caller
sorted insertSorted(x, a, b, a < b)


# method argument list
List insertSorted := method(x, ...)

# using comparator
call sender setSlot(call argAt(1) name, x)
call sender setSlot(call argAt(2) name, y)
call evalArgAt(3)

Using the call object often requires a little more work on the method implementor's part. They can also be difficult if the method takes other arguments.

call objects can be passed down to other methods if necessary.

Comparator Objects

# caller
sorted insertSorted(x, Object clone do(cmp := method(a, b, a < b)))


# method argument list
List insertSorted := method(x, comparator, ...)

# using comparator
comparator cmp(x, y)

Creating an entire object can be handy when the object holds other data or methods, but it can be overly verbose in simple cases.

Notable Examples

List select, foreach, map and detect

Io> list(3, 1, 2, 5, 12, 3, 2, 3) select(x, x > 3)
==> list(5, 12)


Io> list(3, 1, 2, 5, 12, 3, 2, 3) map(+ 10)
==> list(13, 11, 12, 15, 22, 13, 12, 13)

Io> list(3, 1, 2, 5, 12, 3, 2, 3) detect(x, x > 10)
==> 12

Foreach, and friends use the call object approach.

List sortBy

Io> list(3, 1, 2, 5, 12, 3, 2, 3) sortBy(block(a, b, a < b))
==>  list(1, 2, 2, 3, 3, 3, 5, 12)

List sortBy takes a block, although there is little advantage to taking a block over using the call object.

List sortKey sort

Io> list(3, 1, 2, 5, 12, 3, 2, 3) sortKey(x, x) sort(<)
==> list(1, 2, 2, 3, 3, 3, 5, 12)


Io> list(3,1,2,5,12,3,2,3) sortKey(x, x) sort(a, b, a < b)
==> list(1, 2, 2, 3, 3, 3, 5, 12)

List sortKey sort uses the call object approach.

Conclusion

In Io there are many ways that executable code could be passed in to methods. The most common is by using the call object to decide when to execute arguments. Using the call object is the recommended approach.

Blocks are sometimes used, and are particularly helpful when the method takes other arguments. However, their use is discouraged due to the unexpected problems that occur when they are stored.

Methods are rarely passed to other methods.

Object providing a particular interface are rarely used for quick small things, but the are used widely when working on a larger scale.