iogii
I only golf in iogii because it is simple yet has the features I love: tacit, lazy, vectorized, one letter/symbol per op, no syntactic cruft.
Like many golf languages it uses postfix notation because this removes the need for any parenthesis. When using functions (e.g. folding), other postfix languages will require stack manipulation for all but the simplest of functions, but iogii never does thanks to its two key ideas:
- A functional pearl (circular programming) can be used to fold using only a function of a single argument.
- A single function argument can be placed anywhere by rotating the function definition.
The result is that programs are almost purely just a bunch of operations, no characters wasted on syntax!
Example
This program generates the Fibonacci numbers and then keeps the first 10 (click to run it):
1 iterate dup 0 cons + 10 keep → [1,1,2,3,5,8,13,21,34,55]
Notice the lack of apparent syntax. dup
creates a copy, cons
prepends to a list. It could also have been written:
1i:0c+10k → [1,1,2,3,5,8,13,21,34,55]
This page will explain the ideas behind iogii and how this example works.
Alpha
Iogii is in alpha, which means that it is slow, there are bugs, and things will change. I am looking for feedback: What is painful in it? What feels like it is missing? What seems wrong? Please let me know by emailing me at <name of this language>
at golfscript.com if you try it!
Tutorial
Syntax
Expression syntax is postfix. Every operation pops the necessary number of arguments, computes the result and pushes it. Even things like numbers (they are just functions of 0 arguments). Be sure to read up on this if it is new for you.
5 2 1+* → 15
Here is the stack after each token in this program:
5 : 5
2 : 5 2
1 : 5 2 1
+ : 5 3
* : 15
An exception to this is the data format, which is numbers, chars, or strings, separated by commas:
1,2,3 → [1,2,3]
"asdf","1234" → ["asdf","1234"]
You may use two or more ,
s to separate between higher ranks:
1,2,,3,4 → [[1,2],[3,4]]
By the way strings are just a list of characters
'a,'b,'c → "abc"
All ops have both verbose names and one character names. In this tutorial I am using the long form for clarity.
Laziness
The language is functionally pure and lazy. This is useful for working with infinite lists:
wholes 5 keep → [0,1,2,3,4]
wholes
is the infinite list of whole numbers. keep
is an op that keeps the second arg number of elements from the first. In short form this would have been:
{5k → [0,1,2,3,4]
Laziness is also useful for avoiding special syntax for conditionals. For example you can write an if statement as so:
condition trueCase falseCase ifElse
E.g.
0 3 3+ 1 1+ ifElse → 2
Thanks to laziness we don't need special syntax and the trueCase
is only evaluated if the condition
is truthy and the falseCase
is only evaluated if falsey. Here computing 3+3
would not be expensive, but this could be invaluable for infinite lists or expressions that would error.
Vectorization
All operations vectorize (similar to APL or numpy).
1,2,3 4,5,6+ → [5,7,9]
With broadcasting as needed:
1,2,3 2+ → [3,4,5]
Even ops that take arbitrary types:
"abc","123" head → "a1"
head
gets the first element from a list, so it would have returned "abc"
except that it vectorized and got the first element of "abc"
and "123"
which were 'a
and '1
respectively.
If we wish for these generic ops to not vectorize, capitalize them or proceed them with a ,
(or both to unvectorize twice for a rank 3 list). This is why generic ops use alphanumeric names instead of symbols.
"abc","123" Head → "abc"
If using the one char name of an op, it works the same:
"abc","123" H → "abc"
Similarly to Haskell, lists of different lengths are truncated when doing a vectorized operation:
"abcde" 1,2,3 + → "bdf"
Tacitness
An iogii program is a function of its input. Functions take their argument implicitly as the first thing on the "stack".
So if we wanted to add 1 to every character of an input our program would just be:
1+
This is equivalent to writing:
$ 1+
Because $
is the current function's argument, and the main function treats its implicit argument as optional, it doesn't put an extra copy of the input first since it would go unused.
All this is standard, but what if we needed to use that input as the second arg to an op? For example, a greeter, a program which says "hi" to user input:
"Hi, " $ append
Normal stack oriented languages would need to do stack manipulation in this case, but this is where iogii's second key idea comes in: just rotate everything before the $
to the end of your program and remove the $
:
$ append "Hi, " # (rotated)
append "Hi, " # (final tacit program)
If that string was a complex expression instead, it is the same. The way it works is: if it attempts to pop a value from the stack and the stack is empty, it pops from what the stack will be at the end of the program instead. This may seem impossible since we don't know what that is yet, but the stack can be analyzed statically so it is actually not a problem.
Functions
There are a handful of operations that take in a function. mdup
is one of them. It is like dup
, which pushes a copy of the top of the stack onto the stack, except that it is followed by a user defined function that is applied to the original copy first. Functions go after the op they are being passed to, this may seem like an exception to postfix notation, but functions are not first class values, they are always known statically.
Suppose we wanted to compute (x+10)/x
. Let's assume x
is currently on the stack. dup
wouldn't work because we need to add ten to the first arg to /
, so we just need to use mdup
using +10
as the function instead. It would just be:
mdup 10+ /
Functions have their own stack. Here are both stacks as we step through the program:
main fn
x
mdup x
10 x 10
+ (x+10)
(x+10) x
/ (x+10)/x
We could also have written:
mdup +10 /
But that would technically be (10+x)/x
using the rotation trick we mentioned earlier, since +
would need two arguments but the stack size is only one at that point.
This may look too good to be true, and it is. Iogii knew that the function ended right after the 10+
because that is the first time the stack size returned to one and there are no end function delimiters present to extend it further. If we wanted to write (x+4+6)/x
instead. That would be:
mdup 4+6+ / # won't work
But this wouldn't work because it would think the function ends after 4+
. So we would need to use the end function delimiter (>
) to extend that function:
mdup 4+6+ > /
You can always use >
to terminate your functions. When your function is the shortest possible function (the stack size is never equal to one before it ends) then you may usually omit it.
Iteration and folding
Iogii has an iterate
op that is similar to its Haskell counterpart.
0 iterate 1+ 5 keep → [0,1,2,3,4]
In Haskell, the equivalent program starts with the value 0
, the next value is the function applied to that value, and the next is the function applied again to that and so on. But in iogii the function is applied to all previous values in one call to the function using vectorization. This may seem paradoxical, how can we pass in all inputs to the function when all but the first of those inputs are the result of that same function? The answer is laziness. We don't need the entire result to know that the first value is 0, and we know that the next value is 1+ that, etc. It works so long as we don't write code that creates an actual circular dependency. This code would if we asked for the last element, but so long as we ask for a finite number of elements it will only require finite steps. This is an application of circular programming. You can learn more about this by reading about a similar project of mine called Atlas.
Circular programming is not something specially coded into iogii, it is a natural consequence of laziness and these techniques can be used in languages like Haskell too.
This may still seem strange. Why do it like this?
Because it allows for all sorts of concepts to be unified.
Let's look at what would happen if instead of adding 1
, we add a list:
0 iterate 4,5,6+ → [0,4,9,15]
It performed a "scanl!" (which is just a left fold but capturing the value at each step). Since the yielded value was a list, this +
operation just did a zipped +
with that list and 4,5,6
. At each step it is adding the next number to the sum so far resulting in the next sum so far.
We don't need the 5 keep
anymore because these lists are different length and truncated to the shorter 4,5,6
.
This is similar to how our fibonacci example worked:
1 iterate dup 0 cons + 10 keep
It is adding the previous numbers to a copy of the previous numbers but with 0 prepended, lining it up so that each number is added to one before that (or 0
for the first).
Suppose we wanted to iterate with an index? No new op is needed. Just do what ever operation was going to use that index as a zipped operation with the whole numbers!
All sorts of concepts can be handled just by manipulating lists rather than needing special ops to handle them.
By the way there is nothing special about iterate. We could achieve the same thing without it:
@ 1+ 0 cons @ 5 keep → [0,1,2,3,4]
(The first @
gets the value at the last @
, see Nitty Gritty for more info).
Foldl is the same as doing a scan and then taking the last element. Iogii provides a builtin called foldr
that works similarly to iterate
but from right to left and then taking the first element. The advantage of this as opposed to foldl is you could actually compute the answer without considering all elements, using laziness. Suppose you wanted to inefficiently find the square root of a number. You could look for the first element such that (x+1) squared is greater than the target. Normally I would just filter and then take the first, but for this example let's do it without filter.
0 foldr wholes ifElse 26 wholes 1+ dup * < not > → 5
It computes the answer even though we are essentially folding on an infinite list.
This way of performing folds allows for us to be tacit, even for complicated expressions. It is necessary because there isn't a good way to be tacit for functions of 2+ args. Even for the simplest case you still need to specify the order of the arguments (if it isn't a commutative op). And for more complicated cases the rotation trick won't work since it can only allow us to specify the location of one of the args, not both.
Next steps
Iogii was built to be as simple as possible while still providing a way to write any program tacitly. This page covered the main ideas but there are a few more details that could be useful if you intend to really use it, see Nitty Gritty for those. There is also a printable list of all ops in the Quick Reference.
If you want to get started golfing in iogii, download the source and try some problems from: code.golf, golf.shinh.org, or codegolf.stackexchange.com. Currently you'll only be able to submit your solutions at the latter two.