Meet K-oz
A language based on cause and effect

Most programming languages are based on the imperative paradigm, where one writes a series of instructions that execute in sequence. Languages like Haskell use a very different paradigm: the functional paradigm, where everything you do happens through functions and recursion. In this post I wanna introduce you to K-oz (pronounced “chaos”), which is neither functional or imperative, but completely based on events and listeners: an event-driven programming language.

main -> {
    def name: str? = null

    on printName -> stdout:println ("Hello, " + name)!
                 -> end!

    when name changed to name != null -> printName!

    stdout:print "Enter your name: "!
        -> stdin:line!
        -> |ln| name = ln
}

Ok that’s impressive, but kinda confusing: what is this even doing? Let’s analyze this little program step by step:

  • We listen to the main event: this is the event triggered at the start of your program.
  • Our listener is an event space where we can make multiple things happen. In here:
    • We define a variable named name which we initialize to null.
    • We define a local endpoint named printName, which we listen to:
      • We trigger the stdout:println target. This target causes a result event when the IO operation finishes.
      • We trigger the end target as the result of stdout:println.
    • We listen to a change of the name variable: when name changes and it’s not null, we trigger printName.
    • We trigger stdout:print, whose result triggers stdin:line, whose result triggers an assignment to name.

We can translate this to a similar Python program:

name = input("Enter your name ")
print("Hello, " + name)

Much simpler, right? That’s only because I wanted to show you various features of K-oz in a single example. Above K-oz program can be simplified into a oneliner:

main -> stdout:print "Enter your name: "! -> stdin:line! -> |ln| stdout:println "Hello, " + ln!

Still not as simple as Python, but that’s because this program can’t really “cause K-oz”, as it’s just a series of things that happen right after eachother.

The basics of K-oz

In K-oz we identify several fundamental things:

  • Values and types
  • Events and listeners
  • Sources and targets
  • Spaces

Let me go over them quickly:

Values play an important role in the ecosystem of K-oz. Values represent the data we are working with. They are of various types. Types represent the structure of values: int for example, is a type defining an integer (a 32-bit signed integer, to be precise). The value 42 is of type int, for example.

Events are what K-oz is all about! An event is the occasion of something happening. While a lot of things can happen, we usually want to do something only when something very specific happens. That’s where a listener comes into play. Listeners listen to certain events, which can cause other events to happen. For example, the start of the program is an event. We can listen to this by listening to main.

Sources are a categorization of events. Listeners can be attached to sources and listen for events on that source only. Targets are the opposite of sources. A target can be triggered by listeners. Sources are essentially targets, but one end of them is internal and one end is external. For example: stdout:print is a target, but internally it’s a source since the implementation should listen for triggers on this target to start the IO operation. Upon triggering, it gives back a source which is internally a target: the implementation will trigger it when the IO operation finishes so that we can listen for this event.

Spaces are a bit more advanced but still fundamental part of K-oz: they are where things happen. A space is initialized each time its listener triggers, and it makes several things happen. It can be used to trigger multiple targets in parallel, but in a space we can also store variables, create local targets and register listeners that will only work as long as the space is alive. Once a space is destroyed, everything happening in this space will immediately be interrupted and stopped, all listeners regsitered within it will be unregistered, and all variables will be deallocated.

Example: fibonacci

One example I love to write to demonstrate a programming language is a program that prints a bunch of numbers of the fibonacci sequence. In K-oz, this is rather involved.

# This is the amount of numbers we'd like to print. It's a constant so it can't be
# listened to for changes.
const AMOUNT: int = 20

main -> {
    # This keeps track of how far we are in the sequence.
    def i: int = 0

    # These keep the fibonacci numbers.
    def x, y: int = 0, 1

    # This does one iteration: it prints y and then calculates the next iteration.
    # Note how we assign all variables in one line, this is an atomic operation so
    # the changes of x, y and i will only trigger events after they are all assigned.
    # If we were to assign i separately, we risk the occasion of 'loop' triggering
    # again before x and y are updated.
    on loop -> stdout:println y! -> x, y, i = y, x+y, i+1

    # The change of i, which is caused by loop, should retrigger loop, but only
    # if i is less than AMOUNT. Otherwise, we're done and trigger end.
    when i changed -> if i < AMOUNT then loop! else end!

    # To start off, we'd also like to trigger loop.
    loop!
}

There are many ways to make loops in K-oz, above is just one example, but simple while and for loops do not exist. Rather, you’ll have to make a listener somehow trigger itself.

Concurrency in K-oz

Programming languages have introduced many useful ways to be able to run on multiple threads. Java lets you use Thread objects however you like and has many thread pool and concurrency tools, in Python and JavaScript you have async functions that you can await, and Rust solves many concurrency problems at compile-time. But all of these languages require you to manually use it. K-oz doesn’t!

In K-oz, concurrency is already in the nature of the language. Look at this program:

main -> {
    stdout:print "Hello"!
    stdout:print "World"!
}

This program can output either HelloWorld or WorldHello, and which of the two it outputs is not determined. The stdout:print target is triggered twice at the same time, which causes two events in the IO implementation, and it may handle them in either order. It must be noted that each print event is handled atomically: while a print event is being handled, other print events have to wait, so an output like HeWlorllod cannot happen.

What we see here is the natural concurrency of K-oz: both print events are handled in parallel. K-oz may as well decide to dedicate two threads for this occasion, each of which handling one of the events. On the other hand, it could also decide to use just one thread for this. While this means the events are handled one at a time, it does not yet mean the order is determined. K-oz may still handle them in any order it prefers to.

So what can be done to make above program print HelloWorld properly? One option is simple:

main -> {
    stdout:print "HelloWorld"!
}

Now there is only one event and no parallelism is needed. But what if there were two events that can’t be joined into one? In case of IO we can just listen to the result of one and then trigger the other:

main -> {
    stdout:print "Hello"! -> stdout:print "World"!
}

But this is neither practical, since not all events give an “I am done”-signal. The general way to solve this is to use await:

main -> {
    await stdout:print "Hello"! -> stdout:print "World"!
}

Awaiting means you’re listening for the event of an entire chain of cause and effect coming to rest. Awaiting can be used on multiple parallel events as well:

main -> {
    await {
        stdout:print "Hello"!
        stdout:print "Fancy"!
    } -> stdout:print "World"!
}

Above code will print either HelloFancyWorld or FancyHelloWorld. It awaits for two parallel events to finish, and once they’re both finished, the chain of events in that space has come to rest and the awaiting ends, causing the last event to happen.

Note that in above code examples, we have not manually triggered end. This is because that’s not actually really needed. When the program starts, main is triggered and K-oz will await this before triggering end automatically. Otherwise, above programs would never stop. It is always ok to trigger end earlier, but if you don’t, it will trigger automatically when nothing else is happening anymore.

Uses of K-oz

K-oz has various applications. It could be used in user interfaces, to listen for events from widgets. For example, the press of a “Save file” button is an event, which needs to be handled. But, you do not immediately wanna block the UI loop with this occasion: saving a file can take a lot of time. K-oz takes all of that into account, all you have to focus on is saving that file when the button is pressed.

Another application of K-oz is in games: think of something like Minecraft. Minecraft is (originally) written in Java, but suppose it were written in K-oz. In this case, a player doing something is an event, causing other things to happen in the world. K-oz’s natural concurrency allows a giant server with thousands of players to easily distribute all tasks nicely over the available hardware, and this barely needs to be taken into account.

K-oz is generally great for highly parallel software systems. A database management system needs high concurrency as there are lots of applications reading from and writing to the database. K-oz makes this once again easy.

Conclusion

K-oz isn’t like other programming languages. It’s all based on cause and effect: when something happens, other things happen. It’s something you really need to get used to.

Currently, this is only an idea, and there is no compiler or interpreter for K-oz. There is much more for me to research about building a programming language fully oriented around event-driven programming since it hasn’t really been done before.

Some examples

Listening for user input indefinitely

# Note that this does not need to be in a space, you can define it here globally
# as well
on lineInput |str|

main -> {
    # However, we cannot trigger events globally
    stdin:lines! -> |ln| lineInput ln!

    lineInput -> |ln| stdout:println "You entered: " + ln!
}

# The interrupt event is a predefined event triggered when Ctrl+C is pressed
interrupt -> end!

Importing modules and parallel repeating

The following program uses the rng module to calculate the sum of 10 random numbers

use rng

main -> {
    def nr, amt: int = 0, 0

    # While sequential, conditional loops don't exist syntactically,
    # it is possible to repeat things in parallel
    # You don't need to do things in order here anyway
    repeat 10: (rng:randint 0..10! -> |n| nr, amt += n, 1)

    # No 'amt changed to', this just listens for the condition to go from false to true
    when amt == 10 -> stdout:println nr!
}

Parallel vs sequential iteration

The following program first iterates over a list in parallel, and then in sequence.

main -> {
    def list: [int] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]

    # Parallel iteration is built into the language, it
    # simply triggers an event for each element
    # See how this will print the elements in the list
    # in arbitrary order
    each el in list: stdout:println el!

    # Sequential iteration is usually provided through API,
    # this will await your listener before iterating to the
    # next element
    # This will print the elements in the correct order
    list:elements! -> |el| stdout:println el!
}

The end event

The end target does not only interrupt the main event chain immediately, it also triggers another event: end.

def num: int = 0

main -> {
    on infloop -> sleep 1 -> stdout:println num! -> await num += 1 -> infloop!

    infloop!
}

interrupt -> end!

end -> stdout:println "Ended! num = " + num!

Expressions as events

You can let an expression evaluate as an event, simply listen to the expression

main -> ("Hello " + "world") -> |s| stdout:println s!

Parentheses

Listening to a source can cause another source to return.

main -> {
    # This will print Foo Bar Baz (on separate lines)
    stdout:println "Foo"! -> stdout:println "Bar"! -> stdout:println "Baz"!

    # This will also print Foo Bar Baz
    stdout:println "Foo"! -> (stdout:println "Bar"! -> stdout:println "Baz"!)

    # This will fail to compile: void cannot be listened to
    # Listening to an IO event will not give another source that can be listened to
    (stdout:println "Foo"! -> stdout:println "Bar"!) -> stdout:println "Baz"!

    # However, this will compile and print Foo Bar Baz End
    # That's because listening to the source returned by triggering elements
    # returns another source that can be listened to, which will trigger when
    # all elements of the array were visited
    def list: [str] = ["Foo", "Bar", "Baz"]
    (list:elements! -> |el| stdout:println el!) -> stdout:println "End"!

    # This line will also compile but it instead prints Foo End Bar End Baz End
    list:elements! -> |el| stdout:println el! -> stdout:println "End"!
}

License

Copyright © 2023 Samū
All rights reserved

The syntax, and the name K-oz (and Koz) are reserved for this language and may not be used without my permission.

Hi! I'm Samū, a furry, artist and game developer from the Netherlands. This is my blog, where I write about my projects and ideas.

Copyright © 2023 Shadew

All rights reserved

Powered by Jekyll

License - Privacy statement