-
Notifications
You must be signed in to change notification settings - Fork 5
3. Variables and control flow
The programs we've seen thus far consist of a single expression. Some programs require multiple statements—in particular, loops. Pip has a handful of control-flow commands for this purpose. But first, let's talk about variables.
Variables in Pip behave much like variables in other imperative languages: they hold a value, and they can be changed by assigning a different value to them. Most of the variables you'll use in Pip programs are lowercase letters, although there are also variables that aren't lowercase.
A Pip variable always has a value, even before it is explicitly set. There is no such thing as an undefined variable. Different variables have different initial values; some of them are constants, while some of them depend on the program and the arguments.
We've already seen one example of this when we used a
to access the first command-line argument to the program. Similarly, if we pass two arguments to the program, we can access the second one as b
:
a*b
(ATO)
In fact, the first five command-line arguments can be accessed as a
, b
, c
, d
, and e
. (We'll cover what f
and g
do when we get to functions and lists, respectively.)
Most of the remaining lowercase letters are preinitialized to useful constants:
Var | Value |
---|---|
h |
100 (hundred) |
i |
0 |
k |
", " |
l |
[] (empty list) |
m |
1000 (Roman numeral m) |
n |
newline |
o |
1 (one) |
p |
"()" (parentheses) |
s |
space |
t |
10 (ten) |
v |
-1 (negative 1) |
x |
"" |
y |
"" |
z |
lowercase alphabet (a to z) |
For instance, here's a program to calculate the first argument as a percentage of the second argument:
a/b*h.'%
(ATO)
The value of any variable can be set using the assignment operator :
.
h:6*7
Instead of 100, h
is now 42. (ATO)
An assignment expression is an ordinary expression; its value is the new value of the variable. It can be used anywhere an expression can be used, including as a subexpression in a larger expression:
(h:6*7)*2
This assigns 42 to h
and then multiplies that value by 2 to get 84. Note that :
has very low precedence, which is why we need the parentheses. (ATO)
The assignment operator is right-associative, which means we can assign the same value to two different variables like this:
s:t:42
(ATO)
Assignment gets interesting when we combine :
with another operator to create a compute-and-assign operator. For instance:
h+:3
This expression adds 3 to h
in-place, modifying the value of h
. It's equivalent to h:h+3
. (ATO)
The compute-and-assign concept is found in many C-like languages, but in Pip there's a crucial difference: it works with any operator. That's because :
is acting as a meta-operator, a symbol that combines with an operator and modifies its behavior. The :
meta-operator even works with unary and ternary operators:
SQ:a
(ATO)
aR:s"..."
(ATO)
If you need to increment or decrement a variable in-place, the U
and D
operators (mnemonic: Up and Down) do just that:
Ut
Dh
(ATO)
Finally, if you need to store a value in a variable and it doesn't matter which variable you use, use y
. Instead of assigning to it using y:
, you can use the yank operator Y
:
Y6*7
(ATO)
In general, a Pip program consists of one or more statements. Any expression counts as a statement, and (as we've seen) if a program ends with an expression, it is autoprinted. There are also several kinds of statements that are not expressions; chief among them are loops.
A while loop is introduced by the command W
, followed by two things: a loop condition and and a loop body.
W i<a {
Ui
o*:i
}
o
Here is a simple ungolfed program to compute the factorial of the input number. (ATO) Let's walk through how it works.
The expression after W
is the loop condition. At the beginning of each time through the loop, the condition is evaluated; the loop continues running as long as the condition is true. In this case, our condition is i<a
, which means the loop will continue as long as i
is less than the program argument a
. Since i
is initially 0
, we will execute the loop at least once if the argument is positive.
The loop body is a series of statements surrounded by curly braces. If the loop body contains only one statement, the curly braces are optional. Here, we have two statements: Ui
increments i
, and o*:i
multiplies o
(initially 1
) by the current value of i
in-place.
Once the loop exits, the desired value is stored in o
, but it hasn't been printed. Therefore, we end the program with the expression o
, which is autoprinted.
Let's golf this factorial program a bit. First, we can get rid of all the whitespace:
Wi<a{Uio*:i}o
(Since lowercase variables are one letter each, the parser knows that io
is two separate variables i
and o
.) Now, we can also get rid of the curly braces if the loop body is one statement. Since U
is just an operator, we can rewrite Uio*:i
to o*:Ui
, and we have:
Wi<ao*:Uio
There are, of course, shorter ways to compute a factorial, but this is the shortest using a while loop. (ATO)
A variation on the while loop is the till loop. It uses the command T
and functions the same way, except that the logic is reversed: the loop continues as long as the condition is false. We could rewrite our factorial program using till:
Ti=ao*:Uio
Instead of looping while i
is less than a
, we now loop until i
is equal to a
. The result is the same in this case, but for more-complicated conditions it can sometimes be advantageous to use till. (ATO)
What if we're in a loop and we want to generate output each time through? Enter the P
and O
operators. P
prints a value with a trailing newline, and O
does the same but without the trailing newline. These operators (together with the yank operator Y
) are the only operators with lower precedence than assignment, which means we can show the steps of our factorial program like this:
Wi<aPo*:Ui
Each time through the loop, the value of o*:Ui
(that is, the new value of o
) gets printed. Now we don't need the extra o
at the end, since the final value of o
gets printed explicitly in the loop. (ATO)
P
and O
work anywhere, not just in loops. We can use them to print multiple values:
Pa+b
Pa-b
Pa*b
Pa/b
(ATO)... Whoops, why did we get the last result twice? Because P
and O
are operators, not statements, so Pa/b
is the final expression in the program. P
prints its operand, but it also returns its value, which means that the same value gets autoprinted. To avoid the duplicate, don't use P
on the last expression:
Pa+b
Pa-b
Pa*b
a/b
(ATO)
Sometimes you just want to loop a certain number of times. The fixed-iteration loop L
is your friend here. It takes a loop count expression and a loop body. Whatever number the loop count evaluates to, that's how many times it loops.
L5P"Hello!"
(ATO)
That's a silly example; but we can actually rewrite our factorial program using L
and save two bytes:
Lao*:Uio
(ATO)
Pip's for loops are modeled after Python's for x in y
. They start with F
followed by a loop variable, an iterable expression to loop over, and a loop body. For each element of the iterable, the loop variable is bound to that element's value, and the loop body is executed. Scalars are iterable; the elements of a Scalar are its characters:
Fi"Hello, World!"Pi.s.Ai
For each character i
in the string "Hello, World!"
, print the character concatenated with a space and the ASCII code of that character. (ATO)
Usually, the ternary operator ?
is the best way to do conditional logic in Pip. However, sometimes you need a statement (for example, if you want to conditionally a statement such as a loop). Pip's if statement is introduced with I
followed by a condition and a block of statements like the while loop. Else-if clauses are introduced with EI
followed by another condition; a final else clause is introduced with EL
. All of these extra clauses are optional.
I a<0
P"Cannot take the factorial of a negative number"
EI a=0
P"By convention, the factorial of 0 is 1"
EL {
W i<a
o *: Ui
Po
}
(ATO)
TODO
Written by @dloscutoff and @razetime. Do show us your Pip programs at Stack Exchange Chat!