Nameless Graph Language
This programming language was born from my affinity for visual thinking, my frustrations with existing language environments, and my ideas for enhancing the experience of giving instructions to a computer.
The goals of this project are to:
Here is a complex example of code in this language. Click “evaluate” to run it, and use the arrow keys to move the blue box around the screen. You can inspect the program by double-clicking on the small boxes inside it to see their contents.
A program in Nameless is represented by a single function. A function can have one or more outputs and any number of inputs. To run a program, you must select one (or more, some day) of its outputs to evaluate, and provide values for all of its inputs.
No matter what type of thing you’re editing, you can edit its friendly name in place in the top left of the canvas. There is also a delete button, as well as a feature that allows you to jump to any graph this thing is used in. Every definition shows its uinque ID in the URL, which is why links work, at least to standard library functions. You can even use the Back and Forward buttons on your browser to backtrack to other things you have been editing.
Your work is saved every half-second to localStorage, which is local to whatever computer you are using, so unfortunately you currently cannot share links to programs you have created on the internet. I hope to fix this some day.
Example: The Factorial Function is a simple recursive function. See wikipedia for explanation. To run it, click “evaluate”, and it will ask you for a number. Enter a small integer, and it will tell you that number’s factorial. You can now click “debug” and step through the program to see how it works.
A graph in Nameless is a large box that contains a bunch of smaller boxes, all connected together with lines. The whole graph represents a function, and the smaller boxes inside of it represent function calls. The lines describe how data travels throughout the system.
This makes it sound like a dataflow language, like PureData, but it isn’t. It is simply a traditional programming language that is mapped to a visual interface.
Both the parent function/graph and the child function calls contain little labeled dots, which I refer to as nibs. Nibs appearing on the right side of either construct are inputs, and those on the left side are outputs, thus information requests travel from left to right and responses travel from right to left.
Nibs that represent sources of information are filled in, and nibs that represent requests for information, aka information sinks, are hollow. If you click and drag a nib, you can draw a line from it to a nib of the other type (sources go to sinks, and visa versa) to build a connection.
You can add additional inputs/outputs to the graph by clicking the +input/+output buttons on the bottom of the screen. You can remove them by clicking the minus button next to each one. You can edit their names in place, and this information will propagate to anywhere the nibs are used. There is also a hidden text field under each input nib that allows you to give it a default value, which will prevent the system from prompting you for that input. Each output nib has an “evaluate” button below it which allows you to run the program and retrieve the value of that output.
To evaluate a program, click on the “evaluate” button next to one of its outputs. When you do this, it will follow the line from that information sink to the information source it is connected to, and attempt to evaluate that information source. When it is ready, the source will send its answer to the sink, and the program will present you with the answer.
All of those smaller boxes represent function calls. They are colored differently based on how they are implemented. You can double-click on one to see what is inside of it. It could be another graph, some code, etc.
You can add function calls to a graph by finding functions in the library on your right and clicking the Call or Value buttons by their name. Type in the search box up top to filter the list and find what you want. Clicking Call creates a regular function call, while clicking Value creates a function call that has only one output: the function itself as a value (this feature may be removed in favor of lambda prototypes later). Clicking on a function’s name in the library, or double-clicking on a function call in your graph, will take you to an editor for that function.
You can even place a call to the same graph inside of itself. In fact, this is how the factorial example accomplishes looping. Recursion is the only method of looping in Nameless, though it can easily be bundled up into a higher order function to create other looping constructs such as Map and Reduce.
You can click on function calls to select them. If you click and drag on empty space, it will draw a box. This allows you to select several function calls at once. You can also hold down Shift and click on function calls to toggle them in and out of the current selection.
When you have several calls selected, you can click the Join button to create a new function that contains those calls, in-place in the current function. I encourage you to use this feature if you notice a pattern, or feel that you are repeating yourself, or simply want to re-use some nodes somewhere else.
When you have only one function selected, which happens to be a graph, you can click Bust to perform the opposite action as Join. All of the function calls inside of this graph will pop out into the current graph.
Using these tools should not alter the behavior of your program. They just help you organize things.
These functions are implemented in traditional textual languages. Their purpose is to wrap native browser features and make them available to graphs. Ideally these functions should be very short. The contents of the text field should evaluate to a function when run through “eval”.
The first argument that is always passed to one of these functions contains general information you may need in order to implement it. The object has these fields:
Checking the “stateful” checkbox adds an extra input and output to your function with a semicolon for a label. Connections on these nibs do not pass any meaningful data (it will come out as null) but they can be used to ensure functions execute in a particular order. Specifically, if you connect a function’s sequencer input (a sink nib) to a source nib, it will consider evaluating that source to be a prerequisite to running your function. In this way, a string of sequenced functions queue up from left to right and execute from right to left. When sequenced functions execute, they may store information in their state fields which can be retrieved later by evaluating their other outputs.
Text, JSON, and Symbols are very simple functions that I refer to as values because they have no inputs and only one output. A Text will output a string of text, while a JSON will parse its text as JSON first and then output the result.
A Symbol returns its own ID. They were created as a way of getting unique identifiers into data structures. They are especially useful for identifying global variables in a way that cannot collide with other authors. You can get at the global state using the global function, or using the getter and setter functions I made for it.
Another type of function is a Type Constructor/Destructor. It allows you to bundle a set of inputs into a single object, and unbundle them later. This feature is pretty minimal at the moment. To use it for bundling, add it as a Value. To unbundle, use it as a Call.
Currently Lambas can only reach out one level, but eventually I would like it to be possible to nest as many lambdas as you like.
When you create or edit a lambda’s definition, all you can do is edit a set of inputs and outputs. These serve as a function prototype. It’s like a template for building functions that are compatible with that prototype.
When you use a lambda as a value, you get a box with one output on the outside, but with all of the nibs from its prototype on the inside. The outer output/source value is the function your lambda defines. You can connect the inner outputs/sinks to any sources in the current graph, whether they are part of the lambda or not.
If you drag a function call into the subgraph, it will gain a thick outline indicating that it lives in the subgraph’s scope, as opposed to the parent graph. Every time the function this lambda defines is called, its subgraph will get a brand new scope, but the parent graph will retain its same old scope forever. Thus, when you access information sources inside the subgraph, they will be calculated fresh, while information sources in the parent graph will retain cached values from the last time they were called.
When you use a lambda as a call, it will look like a regular function call but with one extra input labeled “implementation”. This is where you feed in the value you got from a Lambda Value of the same prototype.
It is sometimes useful to write functions that make use of lambda calls which expect the caller to pass in their implementation. This is commonly called a callback. Map and Reduce make use of callbacks of this sort.
I’m calling it Nameless because the lack of significant “names” or identifiers in the language was one of its first and most compelling features. This is subtle, but important. You can still give things names, but they only serve as documentation, not as identifiers. The true identifiers are long GUIDs which are generated automatically and are generally hidden from the programmer.
Since names are irrelevant to the interpreter, two authors can give two different concepts the same name without causing problems. This has several benefits.
There is no such thing as variable shadowing in this language. This prevents a class of possible mistakes.
In other languages, a programmer may name a variable in an inner scope the same as a variable in an outer scope, which prevents them from accessing the outer variable. This can often lead to mistakes, where the programmer attempts to access the variable that is shadowed, and ends up accessing the wrong variable. This problem is rampant in Python, where there are so many “builtins” and modules with simple names that programmers often shadow them accidentally. “file” and “id” are prime candidates.
Often, languages will add a concept of Namespaces as a way to prevent reused names from colliding. This adds complexity to the language. The programmer must think about which names are available in which context, and they must write and maintain “import” statements to select which names are available. The complexity added here is huge. Obsolete import statements are seldom removed, leading to bulkier programs with irrelevant dependencies.
In Nameless, all references are globally unique. There is no need to write import statements, as they would not add any information that cannot already be discovered automatically by the interpreter.
A function should be the unit of a program, not a file. Grouping functions into files only causes difficulties in packaging: the functions must be shipped together for no good reason. Many languages also cause declaration order dependencies between functions, which I find annoying. Circular dependencies in import statements cause Python to break. In my language, since functions are the unit, and they can easily calculate their real dependencies (the functions they call) you can easily distribute only the subset of functions that are actually needed, and you can also easily reference the subset of another person's program that your program needs. This should be a massive community enhancer!
Order often does not matter. I mentioned before that in my language declaration order does not matter. Also, unless otherwise specified, execution order does not matter either. You simply link up information sources to information sinks, and the implementation of the lowest level functions ends up determining the order things actually happen in. To make things happen in a different order, mark your function "stateful" and you'll get a new information source and sink: it sends and receives no actual information (just a null) but it must be satisfied before the rest of the function will execute.
Expressions are good. By representing my language as a DAG (directed acyclic graph) I can actually build richer expressions than would be possible in traditional languages, since any information source can be referenced multiple times; an implicit intermediate variable. An interesting effect of this is that it is now never beneficial to repeat yourself -- in textual languages the repetitious form is sometimes shorter than the form where you assign many intermediate variables -- because the intermediate variables are invisible! This encourages people to see the real patterns and factor down their code further than they would in other languages. Refactoring is easy -- just drag a box and "join" some functions into a new function. This refactor does not change the behavior of the program at all.
If there's no text, you can't have typos. What's the point of allowing this class of mistakes anyway? I suppose that any good IDE will fix this for you already, but then why don't they use a non-textual form as well?
It's sort of something I'd been brewing for years because I love visual thinking. Geometry was my favorite math class, and my interest was rejuvenated when I studied the geometrical parts of linear algebra. I tutored computer science in college, and I would always draw out the box and arrow diagrams to explain how to manipulate data structures.
After that spark, I started adding all sorts of enhancements to it, fueled by my frustrations with the languages I had to deal with every day. Most of these frustrations were with Python. This lead me to a few principles, which all work together marvelously to make something great.
It's lazily evaluated. Ask for an answer and it will compute it. I must admit I waffled on this one, and it could have gone either way, but I was fascinated by haskell. Also, see the project goals above.
Also I chose to put it in a web browser because I wanted it to be immediately accessible to anyone. Most programming languages require some setup, and you have to go through warnings about whether you trust this software running on your computer. It's nice if you can just jump in and play with no barrier to entry though, in the friendly sandbox of a web browser.
I discovered a negative thing when talking to a guy who does language design for a living. My language allows "global" variables -- that is, variables that are attached directly to the runtime, to keep track of the top level state. This makes it difficult for you to write a program that runs several instances of another program. It would need some way to keep their runtimes separate. This problem is solved trivially in Object Oriented programming, but my language does not have any such concept. Hmm. Now that I think about it, maybe this would be easy to solve just by making a function that takes a function as an argument, and calls it with a new runtime, as if it were a new process?
The language design, or this implementation? I currently do want to tear it apart and change a lot of things, because it does not meet these requirements:
It doesn't. I haven't implemented touch controls yet, and I will have to tear apart the mouse controls to do so, since they make certain single-finger assumptions. Heck, it might even be cool to rewrite it in unity or something else iOS-friendly.
The best feedback I have gotten was reluctant feedback from a guy who has experience with language design. It mostly centered around the problem with globals that I mentioned earlier.
Most people do not understand that this is not a dataflow language, so it’s hard to have a proper discussion.