Nitty Gritty
Nested arg
In order to use a value from outside the function use !
to consume a value from the stack of the calling function. This is necessary to reference arguments to functions for which the current function is nested inside. Let's say we wanted to compute (x+10)/10
, but only writing 10 once. Let's say x is the input to our program as a string, so we'll first convert it to an integer with read
.
read 10 mdup + ! /
We want the value from the input to go inside the mdup, but there is no way to just put it there this time, so we pull it in with !
.
Other ways to use values multiple times
dup
and mdup
are ways to use a value more than once. Iogii could provide ops like dupm
, which would apply the function to the second copy, but this wouldn't be useful that often (only when you need to put values before the arg using the rotation trick), and besides this strategy can have the downside of sometimes requiring !
and >
.
So there are a couple of other methods which will often be more convenient:
[ ]
[
copies a value onto a parallel stack, and ]
pops from a parallel stack. Essentially ]
gets the value corresponding to its parenthesis style matching [
.
1 [ 2 ] → 1 2 1
1 [ 2 [ 3 ] 4 ] 5 → 1 2 3 2 4 1 5
@
@
called the register, is useful when you need to use a value more than twice. The last use of @
sets the value of all other @
s. The reason for it being the last instead of first is incase you wanted to use circular programming (see below).
1 @ 2 @ → 1 2 2
=
~
is assignment, the token following the ~
gets the value before the ~
. This overrides any other meaning of that token for the entire program (both before and after the =
).
1 2+ ~ foo foo → 3 3
Debugging
Debugging a lazy functional program can be tricky. pp
can be used to pretty print values before the rest of the output.
Getting folds/iterates correct can also be tricky due to their circular/infinite nature. I recommend first writing the code that maps desired input to desired output for the function of the fold, and then calling it manually with all examples at once (so that you are using it vectorized like the real function will be).
For example, for the fibonacci program. Let's we could say our inputs are 1 1 2 3
we can just test those rather than the infinite list:
1,1,2,3~inputs inputs 0 cons + → [1,2,3,5]
And that is our desired output since 1 iterate
will prepend the 1
for us.
Broadcasting 1d to 2d+
Broadcasting is obvious from 0d (scalar) to 1d, just repeat it. But what about from 1d?
It will do an unvectorized repeat.
1,2,,4,5 10,100+ → [[11,102],[14,105]]
1,2,,4,5 10,100 R + → [[11,102],[14,105]]
You can utilize this to easily do cartesian products by only doing the vectorized repeat on the desired list:
1,2r 10,100+ → [[11,101],[12,102]]
1,2 10,100r+ → [[11,12],[101,102]]
Coercion
Any time you use an op that expects a char
and you give it an int
it simply first converts the int
to a string.
For example:
1,2,3 " "* → "1 2 3"
"abc" 3 append → "abc3"
Promotion
You can give ops that require lists a value whose rank is too low and it will automatically enclose that value in a list of one element, equivalent to using just
.
For example:
"asdf" 'z append → "asdfz"
The char is first turned into a string of one character.
This would never be useful for some ops, like head
since it would just return the original value, so it will error in cases like these.
Special Promotions
Ops that accept two different types like sortBy
, filter
, etc. will allow their second arg to operate at a lower rank rather than promote. This should be intuitive and not something you need to think about.
Here SortBy
expects type [[a]] [[b]]
since we are using it in unvectorized once (capitalized) form, but it will still work as expected due to this.
"abc","z" : len SortBy → ["z","abc"]
Note that if the ranks were higher for both args, then this trick wouldn't work since it could now properly match for type b
. This would be a rare case where we would want to control the vectorization levels of the two args independently, which is not possible. However you can achieve this by manually promoting b
. Or if this was a filter
op you can raise or lower the rank of the arg without affecting its truthiness by using len
or countTo
(assuming it is positive or 0).
Special Repetitions
There are a 3 ops (% cut
, & reshape
, and * join
) whose type signature might not have been what you expected. For example join, the arg types are: [[char]] [[char]]
, shouldn't they be [[char]] [char]
?
These ops will repeat their second argument rather than promote it to reach the final desired rank. They use each of the second arg values at each step of the computation. So if you did give a rank too low, it would repeat it, but then use it at each step, behaving identically to how you would expect it. But if you do pass it an arg with sufficient rank it provides more customization of the op behavior.
For example we could get all of the non numbers of a string by cutting out all of the numbers:
"a1,2--33b" dup readAll str cut → ["a",",","--","b"]
Or a reshape example:
10 countTo 1,2,3,4 reshape → [[1],[2,3],[4,5,6],[7,8,9,10]]
Low Rank Overrides
There are some ops such as T
instead of tail
where we actually do a different op (rangeTo
) rather than error or promote. It would be useless to take the tail
of a scalar since even if we promoted it to a singleton list, the tail
would just return an empty list. So we allow T
to mean a different op for a scalar.
4T → [0,1,2,3]
"hi","there" T → ["there"]
All the low rank overrides can also be used vectorized once, but not twice since then ranks would be high enough to mean the non-overriden version of the character. If you do want to use rangeTo
on a 2d list, you still can using T,
(or a sufficient number of ,
s to make it invalid for even higher ranks).
1,2,3T → [[0],[0,1],[0,1,2]]
1,2,3,,4T, → [[[0],[0,1],[0,1,2]],[[0,1,2,3]]]
It is theoretically possible to create more low rank overrides than there currently are, but the goal of iogii is to be simple, not fully optimized for code golf. It also gets very complicated to do this since they need to obey certain invariants for static typing of circular programs to work correctly.
>
Matching
The main tutorial said >
can usually be omitted if your function doesn't go to stack size 1 before you want it to end. What's up with that??
The way matching works is it starts at the first >
and then looks to left for the first function using op that could end here. So if have a nested function, it is possible that this matching could occur for the inner function even if you actually wanted the outer. You could stop this by having an explicit >
for the inner function too.
This should be very rare though because ops like meld
, expand
, and mdup
will not leave the stack size the same as they found it, thus the only way the ambiguity can occur is if using the rotation trick for arg placement in the outer function.
My advice is if you are writing something complicated enough to need nested functions to always explicitly use >
. And only when you are polishing your code to remove the >
s.
Implicit Args
If the program or a function ending with >
ends with a stack size < 1, then it implicitly duplicates the arg as many times as needed to the function or program.
5 ; + > → 10 5
>
will generally not match to a block to cause this if possible, but here the ;
is the only place it could match so it does.
Fold Padding
This isn't something you should need to think about in iogii, but if you are trying to use these techniques in Haskell you may run into a gotcha that iogii takes care of automatically, you can still use the techniques in Haskell but you may need to manually pad your lists.
Let's look at how foldr
and meld
work, they start from the right and move to the left, but how long is the starting list? Folds manually pad their yielded args with the initial value and make the list infinite rather than just append it to the end. So the fold: initial foldr block
is defined as:
@ initial pad tail block @ head
For example:
@ 0 pad tail 1,2,3+ @ head → 6
Using append
wouldn't work, it would create a circular dependency:
@ 0j append tail 1,2,3+ @ head # Stack Overflows
pad
isn't special, in fact you accomplish the same thing by like this:
1,2,3 Iterate tail > 0j Or head 10 keep → [1,2,3,0,0,0,0,0,0,0]
And iterate isn't special it is just:
@ block intial cons @
So that would be:
@ tail 1,2,3 Cons @ 0j Or head 10 keep → [1,2,3,0,0,0,0,0,0,0]
Or in Haskell, pad could be defined as so:
pad a v = h : pad t v
where
~(h,t) =
if null a
then (v,[])
else (head a,tail a)
You don't really need to know about this but I find it very beautiful that all of computation can be built off of just a few simple vectorized primitives, cons
, head
, tail
, if
, nil
. It would be worth writing more about this in a context outside of iogii.
Superpad
Suppose we wanted to get the tails
of each row in a 2d list (similarly to how our manual pad
was written), the code would be the exact same as for a 1d list:
1,2,,3,4 Iterate tail > 4 Take → [[[1,2],[2],[],[]],[[3,4],[4],[],[]]]
No problem, but if you try this in Haskell it will timeout. The reason is because the tail
operation is vectorized inside of a circular program. Haskell (and iogii) need to know how many times Iterate
will run and that depends on the size of the result of applying tail
which in turn depends on the length of the arg passed from Iterate
which is just something cons
ed with what we started with. This would happen anytime you use a vectorized loop. Iogii solves problem by padding the value passed to Iterate
, etc. That is it creates an infinite list that starts with our normal list. The extra values would give you an error "pad value used" if you used them, which you normally can't. This works because it allows vectorization to skip the check for if the list should terminate. But the result isn't full of these error values because they will always be followed by the cons
/ pad
of our foldr
/ iterate
.
You could see these error pad values by looking at the args to our functions directly:
1,2,,3,4 Iterate :4 Take ppi tail > 4 Take
Atlas Solves this problem by detecting self dependencies at run time and then having "faith" that the result will not be empty. But since iogii has dedicated ops for folding we can do this statically. So iterate
is actually defined as:
@ superpad block intial cons @
Where superpad
pads the highest x ranks (where x is the vectorization level of the fold). TODO allow people to use superpad
, it is special in that it doesn't vectorize like other iogii ops.
So to use these techniques in Haskell you would need to manually superpad
for nested zip
s.
This is difficult to explain, but basically if you just follow folding patterns and don't look directly at args to vectorized folds then you won't have any problems and don't need to know any of this! Just know that folds are not special, they are converted to an intermediate representation that is identical to if you created them using regular ops.
Comments
#
is a comment, but only if followed by a space. My editor makes it easy to comment / uncomment lines of iogii if I just tell it to use ruby syntax for .iog
files.
Truthiness
Lists are truthy if non empty.
Chars are truthy if non whitespace.
Ints are truthy if non zero.
This is used by ops like not
, takeWhile
, etc.
Input
Input is automatically parsed and passed as the implicit arg to your main program. Any non numbers or commas will cause it to be treated as a string. Multiple lines will increase rank.
If no input is present, using @
also sets the input - which is useful since it can then be used implicitly.
If you don't want auto parsing or want to do interactive IO, then make the first character of the program >
to process input in raw mode.
In raw mode it is first broken into lines similarly to the lines
function in Haskell, so it has the type [[char]]
. Since iogii is lazy you can do interactive IO with this.
Output
Values that remain in the stack at the end of the main program are implicitly printed.
So for each value:
- Ints are converting to strings.
- If the resulting rank is >= 2 then the inner strings are joined by " "
- While the remaining rank > 1, append to each list "\n" and then concat.
1
would print just 1
(no newline) by rule 1.
1,2
would print 1\n2\n
(by rules 1 and 2).
1,2,,3,4
would print 1 2\n3 4\n
(by rules 1, 2 and 3).
1,2,,3,4,,,5
would print 1 2\n3 4\n\n5\n\n
by rules (1, 2, and 3 applied twice).
Static Typing
Believe it or not iogii is statically typed. Static typing is very useful for lazy programs and I would argue that homogeneous lists are more useful than heterogenous ones for code golf, especially since we can overload and do promotion/coercion this way. It also eliminates the problem of the type of an empty list which sometimes should be known!
There's not much downside since iogii is built so that you never need to specify a type and it can always deduce the correct types. In order to get inference to work with circular programs, all ops where carefully selected so that they obey certain invariants (including overloads since you don't know which op will be selected until types are inferred).
That invariant is:
- If increasing any ops arg's type, then the resulting type must also increase or stay the same.
Types are defined in this order empty
, int
, char
. So by increasing I mean walking up in that list.
- If increasing any ops rank, then the resulting rank must also increase or stay the same (and the base type should remain the same).
Most of the time this is how I would want ops to be defined anyways. However, this is why the read
op symbol isn't the same as the str
op symbol despite them being inverses. Because doing so would violate the first invariant.
Currently the -
op for types char char
and char int
violate this invariant, but it is too intuitive to have this behavior for -
and I expect this to be seldomly used inside a circular program.
Again this is something you don't need to know about but I find it interesting, and technically you could still run into it with -
. I'll be working on a way to detect when violating these invariants results in an ambiguous program.
Nil Type
The type of nil
or the first value in an Expand
/ Meld
is a list of the empty type. It has its own type so that it can become any time without coercion. Its rank will automatically become high enough for any operation and you can never vectorize into it.
Errors
Iogii typically tries to avoid giving you an error and will instead return the default falsey value for that type.
For example:
0 0/ → 0
1,2,3 5 get → 0
Negative values don't make sense for many ops like take, drop, etc. In this case they will just behave like 0.