In my last post, I proposed a Smalltalk style syntax for computation using effects, functions, and combinators. An important element that I left out for simplicity was effect sequencers. All of the examples in the previous post used only one effect, but in real programming we likely want to combine multiple effects, both multiply instances of one effect as well as multiple different effects. One of the benefits of effect systems is that they allow this sort of combination in a more flexible way than, say, monads, the traditional method for combining effects in functional languages.
Effects differ from functions in that the order in which effects are resolved may be very important to your program, it may even by the whole point of your program. Imagine a program that prints the digits of pi, but out of order, that would not be very useful. Functions, on the other hand, can be evaluated in a number of ways that we can treat identically, so we leave the details of this up to the compiler and don't include explicit language features for controlling the evaluation of functions (although some other languages do just that). For effects, though, we really need a way to determine how and when they are executed and there are a wide variety of flavors of effect execution.
For this, we have effect sequencers, which are very much like effect constructors, except they take effects as parameters instead of values. They sort of fit the use of some of the function combinators in languages which lack explicit effects, so you can kind of think of them like combinators, but only for effect types, as the sorts of behavior they enable only make sense in the context of effects.
First, let's look at the simple case of combining multiple instances of the same effect:
return 1. also return 2. - {1, 2}
I'm not going to get into all of the specific sequencers, as that is very much a part of the flavor of the language that comes later, once we have specific effects, value types, and namespaces full of functions. The ones presented here are just concepts for possible sequencers that come to mind. The "also" sequencer just combines two effects. It does not determine order. Therefore, chaining together multiple returns with "also" is the same as returning a set of the values from each return. If you want to preserve order, you can use "then" to get a tuple return value.
return 1. then return 2. - (1, 2)
Sequencers work for multiple types of effects, here is a new effect, print:
print 1. -- prints "1"
print 1. also print 2. -- prints either "12" or "21"
print 1. then print 2. -- prints "12"
You can also mix and match different types of effects:
return 1. also print 1. -- either returns 1 and prints "1"
-- or prints "1" and returns 1
print 1. then return 1. -- prints "1" and then returns 1
return 1. then print 1. -- returns 1 and then prints "1"
Note how effect sequencers coalesce multiple instances of the same effect into one effect. You can intermix effects and this will still happen.
return 1. then return 2. -- (1, 2)
print 1. then print 2. -- prints "12"
print 12 -- prints "12"
print 1. also return 1. then: print 2. also return 2.
-- either returns (1, 2) then prints "12"
-- or prints "12" then returns (1, 2)
This last example gets into the issue of sequencer precedence. Sequencers are applied left-to-right. As with expressions, you can use ":" to create a subsequence.
print 1. also return 1. then print 2. also return 2.
-- print "12", then return {1, 2}
-- or return {1, 2}, then print "12"
You can also write them in one of these other formats:
print 1. also
return 1.
print 1.
also return 1.
So that's a little taste of effect sequencers. I feel like this description is a bit abstract and could use more concrete examples, but those will take a while to develop. Sequencers are a somewhat abstract concept, because they have unlimited expressive power. Functions and function combinators are limited in their expressiveness to only all of computation. However, effects and effect sequencers are not bounded by such limitations, because they exist outside of computation. Effect constructors don't DO anything, they describe something to be done. The runtime takes care of actually doing it. Similarly, effect sequencers are descriptions of how effects should be executed, but it's the runtime that actually handles this. Abstractedly, we are not even necessarily limited to sequencers that we know how to implement, as they are just descriptions. Although, practically speaking, it only makes sense to contain in the language effects and sequencers that we can actually implement, when using this notation to describe hypothetical programs there is no need for such restraint.
In my next post, I'm going to get more into practical matters of how to write actual programs that do useful things by extending the language a bit with object-oriented yet functional data structures.