If you are using Clojure, you might be interested in nREPL which lets you connect a REPL terminal to a running Clojure program.
In the following I am setting up a small nREPL demo using the nREPL server and the REPL-y nREPL client.
First I set up aliases for the server and client in $HOME/.clojure/deps.edn as follows:
Now I need a small demo program to test things out.
First I create $HOME/Documents/repltest/deps.edn which just specifies the Clojure version.
The following program then displays a counter which gets increased once per second.
Furthermore it starts an nREPL server on port 7888.
The program goes into the file $HOME/Documents/repltest/src/repltest/core.clj.
Now one can run the program using clj -M:nrepl -m repltest.core.
The program will print out consecutive numbers as follows:
Now you need to open a second terminal for the nREPL client.
You run the network client using clojure -M:reply.
The important thing which took me some time to find out is that you need to then switch to your applications namespace as follows:
Now you can easily access the variables of the main program:
You can also modify the value while the main program is still running:
You should see the counter decrease in the application’s output.
You can even redefine methods using the nREPL client.
I.e. you can do interactive development.
The first program I wrote as a kid was an Omikron Basic program on an Atari 1040ST.
I wrote a program to print something and later I tried out a for-loop.
It took me a long time to figure out that there was an EDIT mode with automatic line numbering.
Using Hatari I recreated a short video of this:
As a kid I got the joy of playing Flight Simulator 2 on my dad’s Atari 1040ST.
The Atari had a 640x400 monochrome monitor.
I noticed that Debian 12 comes with the Hatari Atari emulator.
Furthermore one can download a disk image of Flight Simulator 2 at atarimania.com.
Then it was just a matter of unzipping the file and specifying the disk image on the Hatari command line.
I took a video of the initial part of the demo mode here:
Clojure is a dynamically typed language which means that functions can be called with arbitrary values.
If the function cannot operate on the values (or if it calls a function which cannot operate on a value or part of a value) a runtime exception is thrown.
Even worse, a function could exhibit undefined behaviour for the values provided.
Statically typed languages are often considered safer because the types of values are known at compile time so that the validity of values can be checked.
However statically typed implementations tend to become inflexible.
Type checking at compile time requires the types of the values held by variables to be defined at compile time.
When trying to keep the system both modular and extensible, the developer is forced to come up with complicated type hierarchies and possibly even generic types.
There is a promising solution to this and this is using pre- and post-conditions (contracts) for functions.
In particular there is Malli which allows you to specify schemas for each function using a concise syntax.
Malli schemas can be defined, reused, and composed like types.
Note that a value can fulfill multiple schemas (similar to Rust traits), however implementations and schemas exist independently from each other.
This allows schemas to be more specific than static types will ever be (e.g. you can have a schema for lists with three or more elements).
Malli is optimized for good runtime performance.
If performance is still a concern, one can leave schema validation disabled in production for some or all namespaces.
Here follows a short introduction to using Malli for function pre- and post-conditions:
You can use the Malli provider module to suggest possible schemas matching a list of values.
The resulting schemas can be used to validate values.
Schemas are values which can be reused and composed.
It is possible to add predicate functions to schemas.
Most importantly one can add pre- and post-conditions (function schemas) to functions.
By default function schemas are ignored until the schemas are collected and the functions are instrumented.
Note that here I am replacing the default exception thrower with a more human-readable one.
If you want to collect the schemas from a module (e.g. for testing purposes), you can specify the namespace as follows.
You can also parse code to infer a general schema for a function header.
If you have a multiple arity function, you can use :function to specify the different alternatives.
There is also support for sequence schemas which allows a more compact schema in this case.
Finally here is an example for a method accepting keyword arguments which took me some time to figure out.
The only problem I encountered so far is that instrumentation clashes with Clojure type hints.
I guess the solution is to instead provide the type hints at the call site instead where necessary for performance reasons.