Coding 2023-01-27

Tags:
By Max Woerner Chase

In the previous entry, I got diverted into thinking about whether I want to try using ContextVars in some places that I'm currently basically saying "pass through these parameters everywhere". The reason I'm pondering this is because I've read Koka's documentation, and I saw some connections between concepts in Python that don't obviously map to "the same underlying thing". This post could be pretty rough.

Here's the background: Koka uses effects for a lot of things, and provides various possible behaviors that an effect handler can apply to the effectful code. Basically, part of an effect in Koka is a set of functions that code that has the effect can call. The result of calling these functions is determined by the dynamic scope of how the function is called: most effects are not handled by main(), so they have to be handled by other code. A handler is an expression (I think...) that evaluates to a value of some type. This is done either by having its associated block execute to completion, or as part of an implementation of a function required for the effect.

The simplest thing to put in a handler implementation is an expression that does not resume the effectful computation. Let's compare the example from the Koka documentation with the equivalent code in Python:

fun raise-const() : int
  with handler
    ctl raise(msg) 42
  8 + safe-divide(1,0)

If we ignore the fact that the Koka documentation is defining some effects and functions explicitly for pedagogical purposes, the equivalent in Python is

def raise_const() -> int:
    try:
        return 8 + 1 / 0
    except ZeroDivisionError:
        return 42

There are some syntactic things that stick out, like that Python doesn't have an equivalent to Koka's with, so the indentation is, like, more severe? (Even ignoring that I 2-spaced Koka. Count the levels of indentation between the def and the +.)

The next thing the Koka documentation talks about is resuming, and I'm actually not sure what the idiomatic translation of their basic examples is. The reason I'm not sure actually gets into why I'm interested in working with effect systems.

Here's the Koka code I'm looking at...

effect ask<a>
  ctl ask() : a

fun add-twice() : ask<int> int
  ask() + ask()

If I wanted to surface the resumptive behavior to Python's type system, I think I'd end up with something like...

T = TypeVar("T")
R = TypeVar("R")

Ask = Generator[None, T, R]

def add_twice() -> Ask[int, int]:
    return (yield) + (yield)

But I could also hide this distinction from the runtime (and, as a consequence in this case, the type system) using ContextVars:

ask: ContextVar[Callable[[], int]] = ContextVar("ask")  # need to hardcode result type

def add_twice() -> int:
    return ask.get()() + ask.get()()

The latter option definitely looks a bit strange to me, but the reason I'm interested in it is because of function color. I can either give an example that works with these functions, or make them look a little more realistic...

def add_to_input_gen(var: int) -> Ask[int, int]:
    return var + yield

def add_to_input_ctx(var: int) -> int:
    return var + ask.get()()

(I did say a little.)

The point is, these new functions, at least conceptually, can be passed to higher-order functions or nice syntactic constructs. Let's pretend we have a reason to want to add numbers to a stream of data, but we want to be able to dynamically change the logic that generates the added numbers. For the latter function we do...

def add_to_stream_ctx(vars: Iterable[int]) -> Iterable[int]:
    return map(add_to_input_ctx, vars)

For the former, we, um... Look, I know I sometimes (very rarely) pretend to not notice something for attempted pedagogical purposes, but I'm really not sure what you'd do. I want to be missing something, but when I try to sketch something out, it just turns into a mess.

There's a lot more to Koka, but it's a combination of stuff that relatively-straightforwardly follows from all of this, or stuff that I can't imagine working well in Python, like the ability to resume from the same point multiple times.

For completeness, I'll try to translate my "more realistic" code back to Koka:

fun add-to-input(input : int) : ask<int> int
  input + ask()

This signature is in some sense even more specialized than the Ask[int, int] stuff, which was, in the end, nothing but standard library types mushed together. The difference is the higher-order functions in Koka's standard libraries are generic over effects. This gives you guarantees like "mapping over a total function is total" and its converse, "mapping over a non-total function is non-total". Or use custom effects like ask<a> explicitly. To do that in Python, you either need a new higher-order library, or you need to make Python "not see them", except to the extent that you need to provide implementations.

(Like, I didn't actually set the ContextVar anywhere, and if you don't set it, then, um, it's going to fall over in a way that I believe is imperceptible to static analysis.)

I'm interested in all of this not because I really want to choose different numbers to add to other numbers under various circumstances (at least, that's not the main thing I want), but because it seems really neat to be able to say "Here is a shiny new concurrency library. It is fully compatible with the higher-order functions in the standard library."

For this post specifically, I just wanted to think about some of the ways that Python surfaces the concept of effects, and how they don't really gel in Python, and I'm not really sure they could be made to gel in Python without a compatibility break that would make the transition from 2 to 3 look like a tea party.

And that's why I want to learn OCaml. (I haven't tried very hard yet, but I will.)

Good night.