An idea for a programming language
Published on Friday, 15. October 2021From time to time I get bitten by an idea bug. It always follows a similar pattern. It starts with an idea. Then, for a few days, I can't think about anything else. Often I start to take a lot of notes about it, do some research or even start working on it. Soon after that, I loose the interest as fast as it came. This post is about one such idea.
While writing about functional languages yesterday, I had the idea for my own functional programming language. The language I have in mind isn't a complete language on its own but rather the logical glue between components that are defined elsewhere (more on this later). I don't know if this idea has potential, and I'm sure that the way I've written it down here will have some problems I'm not anticipating. It's just a collection of features I find interesting, and would like to see in a prototype to find out if they could be used in a meaningful context.
The basics
The language I have in mind will be compiled and have automated memory management. It will have a static type system. Much of it is conventional. It has the primitive types int
, float
, and string
and the collections set
, vector
(or list), tuple
, and map
(or dictionary, as it's called in python). The enums are nicked from Rust as is. There's also a templated type opt<>
for making variables optional. It gets a bit more interesting with structs. Syntaxwise they will be very similar to Rust's structs, but allow (multiple) inheritance. Here's an example how they are used:
struct Location { latitude : float, longitude : float } // Instantiate a location let loc = Location { latitude: 27.0, longitude: 172.0 } struct Restaurant : Location { name : string, phone : opt<PhoneNumber> // and whatever information a restaurant has. // The menu, information about the cuisine, ... } // Since Restaurant derives from Location, it // implicitly has a latitude and longitude. Also, // since the PhoneNumber is optional, it can // be omitted during creation. let rest = Restaurant { latitude: loc.latitude, longitude: 25.0, name: "These values don't make any sense" }
The language will support templates. Also, functions are implemented through pattern matching. An implementation of map
might look something like this:
template<T, U> map([T], T->U) -> [U] { [], _ -> [] [x,xs...], f -> (f x) + (map xs f) }
This language won't support any methods, or functions associated to types. This also means that it won't have virtual functions. But to make calling them more succinct, the syntax used for methods is still supported. For function calls that don't take any additional arguments the brackets can be omitted. This means a function f(x)
can be called like x.f
. In general, if you have a tuple of length n and a function whose first n arguments have the same types as the tuple, you can call the function as if it were a method. One consequense of this is that if the return values of a function a
are the same as the parameters of a function b
, they can be called like a.b
.
// using the map definition from above, // these two expressions would yield the same result [1,2,3].map(i -> i*i) ([1,2,3], i -> i*i).map
Even though there are no virtual functions, the multiple inheritance still has an effect on the way functions can be called. Let's take a function doSomething(location: Location)
. Since Restaurant derives from Location, the first argument of this function can be a Restaurant as well.
Side effects, the streaming operator, and api interfaces
I mentioned earlier that this programming language won't be used to write a complete program, but only the logical glue between different components.
These components are defined by streams. They are the only way to create side effects (In other words, all functions that can be written in this new programming language will be pure). A stream can have input and output arguments. A declaration might look like stream (url: string)>>GetRestaurans>>[Restaurant];
, and would be called like let restaurants = "some name" >> GetRestaurants
. Internally, the stream calls a component define outside this programming language. For the sake of argument, let's say it's used in a C++ project. In this case, the program would be compiled to a library that can be linked to said C++ project. This compilation creates a header file for all streams that contains interfaces for all streams. In this case, this header would contain a function std::vector<Restaurant> GetRestaurans(const std::string& url)
. The remaining C++ code has to implement this function for the code to work.
One use case for this type of interface is writing a game engine. Consider the following three streams:
/// Receive a specific game event template<T> stream Reveive>>T; /// Trigger a specific game event template<T> stream T>>Trigger; /// Log some value. Mainly used for debugging template<T..., U...> (T..., fmt: string)>>Log>>U...; // Here is an example how they could be used to // implement most of the game logic. Receive<InputEvent> >> do.some.operations >> Log("Does this have the correct value? {}") >> on.this.event >> Trigger<SomeGameEvent>;
Two things about the control flow in streams. First, if a stream is called in between multiple function calls (like the Log
stream in the example above), the execution of the surrounding code will wait on it. Second, if an incoming stream (i.e. the Receive
stream from the example) is used in multiple places, the code following them will be executed in parallel.
Finally, there's one special streaming operator. The main
stream is generic, but can only be used with one template type throughout the whole project. Then, based on the type given, and the docstrings that are used to document said type, an automatic commandline interface will be generated. This works a bit like argparse in python, only it is build-in directly into the language.
Anyway, these are some ideas that I had yesterday. I don't think I will spend any time working on this language. But it was fun to think about them.