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 tonull
. - 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 ofstdout:println
.
- We trigger the
- We listen to a change of the
name
variable: whenname
changes and it’s notnull
, we triggerprintName
. - We trigger
stdout:print
, whose result triggersstdin:line
, whose result triggers an assignment toname
.
- We define a variable named
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.