Lexical effects, part 1: introduction to effects

I’ve been working on my PhD for a year now, and it’s about time I show you what it’s about. I’m working on a language with constructs such as yield_to_list:

yield_to_list {yieldint.
    # Inside the yield_to_list block, variable yieldint is
    # a function that takes an int and returns nothing.
    yieldint(1)
    print("Hi!")
    yieldint(2)
    # After the yield_to_list block, yieldint goes away.
} to list_ints
# The result of yield_to_list is stored in list_ints.
 
return list_ints   # [1, 2]

When you write yield_to_list {f. ...}, you create a temporary function f that you can call with an int. The body of yield_to_list is executed, and all the values are collected in order in a list. That’s why list_ints contains [1, 2] at the end.

(Python aficionados are reminded of how they can return subresults from a generator using yield.)

You can use yieldint with higher-order functions like map. Let me remind you how map works in many languages:

# We define a new function.
def plusone(x) {
    return x + 1
}
 
# The following line will compute
#   [plusone(2), plusone(3), plusone(4)]
# = [3, 4, 5].
map(plusone, [2, 3, 4])

You can combine map and yieldint without problems:

yield_to_list {yieldint.
    # Variable yieldint is defined from here on.
 
    # Define a temporary function yielding_plusone that
    # yields its argument and returns it plus one:
    def yielding_plusone(x) {
        yieldint(x)
        return x + 1
    }
 
    # Let's try it and print the result.
    print(yielding_plusone(2))
    # (prints 3, and has a yieldint side effect)
 
    # Let's try it a lot more!
    print(map(yielding_plusone, [3, 4, 5, 6]))
    # (prints [4, 5, 6, 7])
 
    # End of block; yieldint and yielding_plusone go away.
} to list_ints
 
# Now, list_ints holds all the inputs
# to yielding_plusone: list_ints = [2, 3, 4, 5, 6]
#
# You can use this to debug a function's inputs, for instance.

Here’s another construct you can use in the language: with_counter {f. ...}, which creates a temporary function that will takes no arguments and return nothing. After executing the code between curly braces, it returns the number of times count was called.

with_counter {count.
    # Variable count is defined from here
    print("Inside with_counter")
    count()
    count()
    count()
    # Variable count goes away here
} to n
 
# Now n = 3.
 
# Note that for both yield_to_list and with_counter, the user
# determines the variable name. This works equally well:
 
with_counter {tick.
    tick()
    tick()
} to num_ticks
 
# num_ticks = 2

Neither yield_to_list nor with_counter is a core feature of the language: they are defined in terms of a more basic handle..with..to construct that I’ll describe later.


I hope this has wetted your appetite a bit. These are all things that you can write in some way in a modern imperative language, but we’re making them play nice with purely functional programming. That way, we will get most of the advantages of functional and imperative programming.

In the next instalment, I will write about the restrict(..) construct, which selectively allows side effects (or not, up to you). You can use it to explain to people what your code does (or does not) do. The compiler will correct you if you’re wrong. Also, it may help the compiler optimize away some things.

If you would like some more examples now of how effects can be useful, I recommend Programming with Effects and Handlers [pdf] by Andrej Bauer and Matija Pretnar. It uses global effects, not lexical effects, but they’re quite similar.