Tips for Creating/Maintaining a Golfing Language
Here are some suggestions. Sorry that this partially overlaps with other answers, which have been posted as I was writing this.
- One possibility (by all means not the only one) to decide which features (functions, data types, etc.) your language L should have is to base it on another language B that you have been using for long. That way you already have a good idea which functions of B are used most often, and therefore should be included in your language L, and you can assign shorter names in L to functions that are commonly used in B.
- L = MATL: B = MATLAB/Octave.
- L = Pyth: B = Python.
- L = Brachylog: B = Prolog.
- L = Husk: B = Haskell.
- L = ShortC: B = C.
- L = V: B = Vim.
- Decide which "paradigm" your language will use. Some examples are
- Stack-based (Cjam, MATL, 05AB1E);
- Tacit (Jelly);
- Prefix notation (Pyth);
- Infix notation (Pip, Japt);
- Fixed arity (Pyth);
- Variable arity (CJam, MATL, Japt).
These decisions are quite independent from item 1. For example, MATL's function
f has the same functionality as MATLAB's
find, but is used differently in that it pops its inputs from the stack and pushes its outputs onto it, whereas MATLAB uses normal function arguments which can be stored in variables.
Incorporate functions from other golfing languages that you find useful. Don't let item 1 (if applicable) limit language L's definition.
Be prepared to add functions in the future. As you (or others) use the language, you will find things it would be nice to add. So reserve some of the "namespace" for future expansion. For example, don't use up all your single-character names at once.
- Write the compiler (or interpreter) for your language L in a language C that you know well (often C = B).
- The compiler is most often actually a transpiler into another language T (which can be the same as B or C). Language T should have a free implementation. That way it is viable to have an online compiler for your language. The compiler will be a program in C that takes source code in L to produce transpiled code in T, and then calls T's compiler/interpreter to run the transpiled code.
A natural choice is C = T. Examples:
- L = Jelly: C = T = Python (and Jelly is inspired by J; but perhaps not so much as to claim that B = J).
- L = MATL: B = C = T = MATLAB/Octave.
- Host your compiler/interpreter in a public repository such as GitHub, so people can easily download it, create bug reports, suggest new features or even contribute with code.
- Write good documentation. That helps users understand your language better. Besides, I found that task more rewarding than I expected. I recommend writing the specification while you are designing the language, not at the end. That way consistency between language behaviour and its specification is ensured.
- Ideally the documentation should include some quick reference (table or summary), so experienced users don't have to go to the full documentation simply because they have forgotten the name or the syntax of a function they know.
- Create an esolangs page with basic information about your language.
- Create a chat room where people can ask and discuss about the language. Visit it often.
- Answer questions in your language, with explanations about how the code works. You'll want to do that anyway (it's your language, you will find it fun to use), but this also helps get people curious about your language, and shows some of the language's properties, which might get people interested.
- If you chose to base your language L on a language B (see item 1) that is general-purpose and well known, users of B will find it easy to switch to L, which will allow them to provide short answers (in L) with minimal effort (coming from B).
Here's a couple of random hints.
Choose your built-in commands and values carefully
Especially when your language is new, it's tempting to add lots of functionality that you think may be useful at some point. If that turns out to be false, you'll have a mostly useless command taking up a valuable spot in your language's namespace. Rebinding the symbol breaks backward compatibility and may invalidate existing answers on this site. That's not the end of the world since we're talking about weird recreational languages, but if it happens frequently, your users will have a hard time keeping up with the changes.
As a hypothetical example, testing whether a given number is prime is probably a good choice for a one-symbol function, since primes come up a lot in golf challenges. Testing whether it's a square or a power of 2 can be useful, but not as often. Testing whether it's a perfect number most likely doesn't warrant a one-symbol command. You'll get a sense of what generally useful things the language is missing by using it.
Minimize the number of incorrect programs
By an incorrect program I mean one that produces any kind of error: syntax error, type mismatch, division by zero etc. Every error is an opportunity for your language to do something useful instead. Syntax error due to a missing parenthesis? Allow it to be implicit in some way (like at the beginning or end of line). Division by zero throws an error? Consider having it return infinity or NaN instead, or allow errors to be caught and used for flow control.
A type mismatch is usually a sign of a function that's not generic enough. Ideally, every function should accept every type of input and do something useful with it, unless there's a good reason not to. There are several common tactics for this:
- Overloading. For example,
+can be addition on numbers and concatenation on lists. This seems to be the most popular option.
- Vectorization. If
+is given lists, it can perform addition element-wise. Jelly uses vectorization extensively.
- Coercion. If
+is given non-numbers, it can convert them into numbers implicitly. For example, 05ab1e allows numbers and strings to be used mostly interchangeably.
- Keep the error if you have a good use for it. In Brachylog, arithmetic operations only work on numeric types. Since it's a logic programming language based on Prolog, they can be used backwards or to generate input-output pairs, and in those cases it's essential that the input can't suddenly be a list or string.
It strikes me that the most important design decision is what the underlying paradigm of the golfing language is.
Here are some possible types of language:
- Stack based
- Array based
- Object based
Indeed you might even have a mixture of these, or something else entirely like a two dimensional language, automata, regex, machine language or a Turing machine and so on.
This is important because it will greatly affect the syntax of the language and in some ways how concisely code can be written.
Edit: Follow Up
I thought that I would show how the design and implementation phases of creating a language actually work in practice. I realise that's it's more than one tip in this answer and it's lengthy I hope that's ok.
I wanted the following things out of my new language:
- Quick to implement (or prototype)
- Flexible and concise syntax, perhaps with a view to Golfing
- Can do useful things, not just a toy language
I plumped for some version of Forth, because it would satisfy the first two critera. It would also have to be interpreted at least for prototyping, compiling is out because it would take too much development time.
In terms of it doing useful things, it absolutely had to have the following traits:
- Ability to call functions
- Arithmetic and logic operations
- Manipulation of numbers and strings
- Loops and conditional structures
- Stack manipulation
And because it's Forth I thought the following would be kind of cool:
- Multiple data stacks
- Ability to 'pass in' function literals to be executed inside functions (kind of functional style)
Firstly, I would leverage my knowledge and use a language I know well for the interpreter: PHP - at least for the prototyping stage.
Next I needed the interpreter to be able to recognise (tokenise) these four things:
- String Literals:
- Numeric Literals:
- Labels (for function names)
- symbols (representing atomic actions):
The simplest solution for tokensing the program is to use regex. Also because I wanted concise syntax labels would be strictly alphabetic. I would also need some sort of separator to remove ambuiguity in tokenising, I left a space and comma free for that.
So with all that in mind I could create a fat-free Forth syntax. For example:
1.5,2.7,3,4add add add;
would push four numbers on to the stack, and call a function
add three times and then return
Once tokenised the interpreter can then consume tokens one by one and act accordingly.
One consequence of using regex, is that it's unable to handle a nested syntax. So I would need to manage nested loops in some way. The way to do this is to look for the start of loops (token) and find the corresponding end and record somehow in the start token where the end is and vice-versa. That way the interpreter could jump around loops and conditionals very easily. A stack would be needed to know which token closes which opening token.
Functions I would just manage as simply labels, or named positions in the token stream. When a function is executed, the position is looked up and interpretation continues from there. I would return from the function using a
; token. This would also require a function call stack to handle nested function calls - and return back to the calling position in the token stream.
For the fancy stuff, such as passing in function literals, the idea would be to push a string literal containing the code fragment on to the data stack e.g:
So to break the above down,
'2+;' is a string literal containing the code fragment (push 2 and add to top item on data stack). A function called
apply is then called. The function definition begins
apply: and a backtick actually pops the string literal and executes it in its own brand new context. Once the fragment has been executed and returns, the function then continues.
The interpreter would handle this by separating out the parsing and tokenisation from the actual execution. That way the literal code fragment can be parsed when pulled off the stack, and that new context passed into the Executing function, using PHP's scoping to handle the new context's scope. The only fly in the ointment is that to be able to call a function from the code fragment, it would need to be able to access the parent's context. For example:
Next for multiple data stacks that would be easy. I would have just one main data stack where all the action happens and provide atomic actions that can push and pull to other named data stacks. That should greatly simplify operations with vectors or arrays of numbers.
I also wanted conditional loops with the condition either at the beginning or end or just infinite loops. So I chose
[...] for an if condition,
(...] for conditional loops and
(...) for infinite loop.
Lastly, some features that are missing: general mathematical functions, extensive string handling, goto and breaking out of loops and conditions. Although it is possible to break out by returning in a function by using
Anyway, here's the semi-golfed prototype, and hopefully semi readable, enjoy!
Try it online!