-
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
. Many 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
Squares a
in-place. (ATO)
aR:s"..."
Replaces spaces in a
with three dots, in-place. (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
y
is now 42. (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. Before each pass 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
, so 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
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 the two statements Ui
and o*:i
into the single statement o*:Ui
, and so 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 length in this case, but for more-complicated conditions it can be advantageous to use till. (ATO)
What if we're in a loop and we want to generate output each time through? Enter the unary operators P
and O
. 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. If we want to show each step of our factorial program rather than just the final answer, we can rewrite it like this:
Wi<aPo*:Ui
Each time through the loop, the value of o*:Ui
(that is, the new value of o
) gets printed. We don't need the extra o
at the end anymore, since the final value of o
was 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? Remember, 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.
L5O"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 an iterable type; the elements of a Scalar are its characters:
Fi"Hello, World!"Pi.s.Ai
That is: 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 execute another 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)
Here's a challenge that is best approached using loops: Smallest palindrome divisible by the input.
Given a positive integer n, output the smallest integer that is divisible by n and is also a palindrome.
Input | Output |
---|---|
1 | 1 |
2 | 2 |
12 | 252 |
302 | 87278 |
303 | 303 |
For an initial brute-force approach, we can check every positive integer until one of them satisfies the conditions. This looks like a good situation for a till loop. Let's choose o
as our test number, since its initial value is 1. Our program structure will look something like this:
T <some condition>
Uo
o
That is: until the condition is met, increment o
; then autoprint the final value of o
after the loop.
For our condition, we need to string together a divisibility check and a palindrome check using &
. We've already detected palindromes in the previous chapter's example program:
o=Ro
For divisibility checking, we'll use the mod operator %
, which will give 0 if o
is divisible by a
and some positive number otherwise. Since we want a truthy result if o
is divisible, we'll need to logically negate the result:
!o%a
So our full program is:
T o=Ro & !o%a
Uo
o
(ATO)
Let's start by removing the whitespace:
To=Ro&!o%aUoo
This is 13 bytes. (ATO) There isn't much we can do to shorten this program; maybe a different approach will be shorter.
So far, we've been looping over all positive integers and checking each one for divisibility by a
. If we just loop over multiples of a
, we can skip the divisibility check. We'll need to start at a
and increment by a
each time through the loop instead of by 1:
o:a
T o=Ro
o+:a
o
Golfed, this is also 13 bytes:
o:aTo=Roo+:ao
(ATO)
But now, since we're setting a start value explicitly, we don't have to use o
anymore. We can use y
instead and take advantage of the yank operator for the initial assignment:
YaTy=Ryy+:ay
12 bytes. (ATO)
Written by @dloscutoff and @razetime. Do show us your Pip programs at Stack Exchange Chat!