iogiiNitty GrittyQuick RefDownload

Nitty Gritty

Nested arg

In order to use a value from outside the block use ! to consume a value from the stack of the calling block. This is necessary to reference arguments to blocks for which the current block 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 functions like dupm, which would apply the block 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. inspect / ppi can be used to pretty print values. (ppi pops it and shows it before regular program output, inspect just returns the pretty form in string format, similarly for type / ppt.

undefined and error can also be useful for debugging, especially error because you can print the values of things on specified conditions. ifelse would normally not be useful due to its length but can be useful here.

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]]

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 / Overrides

There are just a few cases such as . / pow10 instead of prod where we actually do a different op rather than error for promotion. The reason iogii doesn't take more advantage of the possibility to override is because if you wanted to use these operations vectorized it wouldn't work (since now the original op is valid). These ops are rarely used in a vectorized fashion however (and there is still an easy way to recreate them if you did want to use them in a vectorized way) which is why I included them.

1,2,3 . → 6
2 . → 100

Ops that accept two different types like sortBy 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"]

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"

> Matching.

The main tutorial said > can almost always 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 block 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.

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, ifelse, 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 consed 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 zips.

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!

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

Standard input is passed to iogii as the implicit arg to your main program. 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.

If you don't want it broken into lines you could join with with "\n"* but this isn't quite the same as the raw input since it could still truncate a final newline. You may use the stdin op to get the raw [char]s however.

Output

Values that remain in the stack at the end of the main program are implicitly printed.

So for each value:

  1. Ints are converting to strings.
  2. If the resulting rank is >= 2 then the inner strings are joined by " "
  3. 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. There is one caveat however and that is since you can build circular programs (either with folds or manually), it is theoretically possible to construct programs that don't have a well defined type, but it would be difficult to do so and I'll be working on a way to detect it soon.

All ops are built to obey an invariant so that their types can be solved even in circular programs. That invariant is:

Types are defined in this order empty, int, char. So by increasing I mean walking up in that list.

Most of the time this is how I would want ops to be defined anyways. However, this is why the ord op symbol isn't the same as the chr op symbol. 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.

nil type

todo