Effects

When a function runs it can, very generally, do two things:

  1. Return a value
  2. Have some “effect” on environment

In most programming languages, you can make some checks on the first option, using the function’s return type. If, e.g. in C you specify a function like

int f() { return 5; }

You know for sure that it can only return an integer (or another value that is casted into an integer), because of its return type. However, a function with the same signature (return and argument types) can also be

int f() {
    launch_missiles();
    return 5;
}

or

int f() {
    write_to_disk();
    return 5;
}

or

int f() {
    access_network();
    return 5;
}

And there is no way to check which of these “side-effects” the function can or does perform.

Effect Types

Kima allows you to specify this. Using effect types. Every function is annotated with an effect type that specifies what kind of side effects it is allowed to perform. There are many effect types, and you can also define new ones within Kima, but the most basic ones are pure and IO:

  • pure is the default effect type for functions which do not declare one and it simply does not allow any kind of side effect.

  • IO is the “all-powerful” effect. It is meant to allow for any kind of input or output, as well as unchecked exceptions.

See Builtins for the other built-in effects

Using effectful functions

Within a function annotated with an effect type you are allowed to use other functions annotated with the same effect. Trying to call functions with incompatible effects within one another will be a compile-time error.

Defining new effects

Defining a new effect is done with the effect keyword. An effect is defined as a function signature or as a set of other effects. For example, to define a stateful integer effect:

effect get() -> int
effect set(newVal: int) -> unit

effect intState { get, set }

This is all that is required. You can then define functions which use this effect:

fun incrementState(increment: int) : state -> unit {
    let newVal = get() + increment;
    set(newVal);
}

fun squareState() : state -> unit {
    set(get() * get());
}

The problem now is that whatever other function you might call squareState in will also have to have the state effect. In other words, you can never discharge the state effect. This is where effect handlers come in.

Effect handlers

Effect handlers allow you to run effectful functions inside functions that don’t support their effect. They do this by allowing you to describe some effect in terms of another (or no effect!). Here is an example:

fun purifiedState(init: int) : pure -> int {
    let procedure = fun() {
        incrementState(4);
        squareState();
        get();
    }

    var procState = init;

    handle procedure() {
        get():       { procState }
        set(newVal): { procState = newVal }
    }
}

# purifiedState(1) == 25

All that was needed here was to describe the basic effect operations in order to be able to run a state function in a pure context!

Since state is defined as the combination of get and set you can even define functions using only one of the two operations:

fun getF(f: (int) -> int) : get -> int {
    f(get());
}

This function can the be used inside other state functions or handled to run in another context:

fun getonly() -> int {
    let procedure = fun() : state {
        getF(fun(x: int) { x + 1; })
    }
    
    handle procedure() {
        get() -> { 1 }
    }
}

Here we only need to handle the get() effect.