4 지역 변수와 흐름

먼저 알아야 할 것: 3장(서명, 이름 있는 입력).

이 장이 답하는 물음

4.1 지역 변수

몸 안에서 to 이름 은 맨 위 값을 그 이름에 넣는다. 처음 쓰는 이름이면 그 값의 타입으로 지역 변수가 생긴다. 따로 선언하는 줄은 없다.

04-locals.pnx

use io
word describe (a int, b int, c int gives str)
  a b add c add to total
  total 3 div to mean
  a b max c max to top
  cat ("total ", total, ", mean ", mean, ", largest ", top)
end
3 9 6 describe io.println

출력

$ pnx 04-locals.pnx
total 18, mean 6, largest 9

to 는 스택의 맨 위 값을 내려서 이름에 담는다. 이름을 쓰면 그 값이 다시 올라간다. 값을 스택에 오래 들고 있지 않고 이름에 넣어 두었다가 필요할 때 꺼내 쓰는 것이다.

토큰그 뒤의 스택일어난 일
a b add12a 는 3, b 는 9.
c add18c 는 6.
to total—18 을 내려 새 지역 변수 total 에 담는다. 스택이 빈다.
total 3 div6total 을 쓰면 18 이 올라가고, 3 으로 나눈다.
to mean—6 을 mean 에 담는다.

지역 변수가 보이는 범위는 처음 넣은 자리부터 그것을 감싼 가지, 루프, 블록의 끝까지다. 가지 안에서 처음 만든 이름은 가지 밖에서 읽지 못한다. 다른 가지로 왔다면 값이 없기 때문이다.

04-scope.pnx

use io
word f (n int gives int)
  n 0 gt if n 2 mul to doubled end
  doubled
end

거절

$ pnx 04-scope.pnx
04-scope.pnx:4:3: this name is not declared before this place

전역에 넣을 때는 to 가 아니라 set 을 쓴다. to 는 지역 변수에만 넣는다.

04-store-global.pnx

use io
global total int
word add_to_total (n int) total n add set total end     rem set: a global is written with set
3 add_to_total
4 add_to_total
cat ("total ", total) io.println

출력

$ pnx 04-store-global.pnx
total 7

흔한 오해: to total 로 전역에 넣는다

to 의 이름이 전역이면 거절되고, 메시지가 set 을 알려 준다. 전역은 가릴 수 없으므로 그 이름의 지역을 만들 수도 없고, 지역을 만들려던 to 가 전역을 조용히 바꾸지도 않게 하려는 규칙이다.

04-store-global-bare.pnx

global total int
word add_to_total (n int) total n add to total end

거절

$ pnx 04-store-global-bare.pnx
04-store-global-bare.pnx:2:42: this name is a global; a global is written with set: value set name

4.2 갈림

조건 if … else … end 는 맨 위의 bool 을 보고 한쪽을 돈다. 두 갈래는 같은 스택을 남겨야 한다. return 으로 떠나는 갈래는 맞추지 않아도 된다.

04-if.pnx

use io
word grade (score int gives str)
  score 90 ge if "A" return end
  score 80 ge if "B" return end
  score 70 ge if "C" else "F" end
end
95 grade io.println
82 grade io.println
40 grade io.println

출력

$ pnx 04-if.pnx
A
B
F

if 는 맨 위의 bool 을 내리고 갈 길을 고른다. 두 갈래가 같은 스택을 남겨야 하는 까닭은, end 뒤의 코드가 어느 갈래에서 왔는지 모르기 때문이다. 어느 길로 왔든 스택이 같아야 처리기가 그다음을 검사할 수 있다.

04-sign.pnx

use io
word sign (n int gives str)
  n 0 lt if                     rem lt leaves a bool; if takes it off the stack
    "negative"                  rem this way leaves one str
  else
    n 0 eq if "zero" else "positive" end   rem and so does this way, whichever inner way it takes
  end                           rem both ways meet here with one str on the stack
end
-4 sign io.println
0 sign io.println
9 sign io.println

출력

$ pnx 04-sign.pnx
negative
zero
positive
토큰그 뒤의 스택일어난 일
n 0 lttruen 이 −4 이면 비교가 true 를 남긴다.
if—bool 을 내리고 첫 갈래로 간다.
"negative""negative"이 갈래는 str 하나를 남긴다.
end"negative"else 갈래도 str 하나를 남기므로 두 길이 맞는다.

else 가 없는 if 는 스택을 그대로 둬야 한다. 값 하나를 남기는 if 에 else 를 빠뜨리면 거절된다.

04-if-unequal.pnx

use io
word label (n int gives str) n 0 lt if "negative" end end

거절

$ pnx 04-if-unequal.pnx
04-if-unequal.pnx:2:51: an if without else must leave the stack as it found it

4.3 루프

loop … end 는 되풀이한다. 안의 조건 until 은 조건이 참일 때, 조건 while 은 거짓일 때, break 는 조건 없이 루프를 떠난다. 한 바퀴는 스택을 그대로 둬야 한다 — 그래서 루프가 만드는 값은 지역 변수에 모은다.

04-loop.pnx

use io
word collatz (n int gives int)              rem steps to reach 1
  0 to steps
  loop
    n 1 eq until
    n 2 mod 0 eq if n 2 div else n 3 mul 1 add end to n
    steps 1 add to steps
  end
  steps
end
cat ("27 takes ", 27 collatz, " steps") io.println

출력

$ pnx 04-loop.pnx
27 takes 111 steps

한 바퀴가 스택을 그대로 둬야 하는 까닭도 같다. 루프의 머리는 처음 들어올 때와 한 바퀴 돌고 돌아올 때 모두 같은 스택을 봐야 한다. 바퀴마다 값이 하나씩 쌓이면 몇 바퀴 돌지 모르는 처리기가 스택의 깊이를 정할 수 없다. 그래서 루프가 만드는 값은 지역 변수에 모은다.

04-loop-sum.pnx

use io
use str
word sum_to (n int gives int)
  0 to total                    rem what the loop builds lives in locals, not on the stack
  1 to i
  loop
    i n gt until                rem leave once i passes n
    total i add to total        rem the stack is empty again at the end of the turn
    i 1 add to i
  end
  total
end
10 sum_to str.to_str io.println

출력

$ pnx 04-loop-sum.pnx
55

04-loop-leaves.pnx

word count_up (n int gives int)
  0 to i
  loop
    i n ge until
    i 1 add                     rem a new value each turn, and nothing takes it
  end
  i
end

거절

$ pnx 04-loop-leaves.pnx
04-loop-leaves.pnx:6:3: one turn of a loop must leave the stack as it found it

count_up 은 바퀴마다 i 1 add 로 값을 하나 만들지만 아무도 가져가지 않는다. to i 를 붙이면 그 값이 i 에 담기고 스택은 바퀴의 처음과 같아진다.

루프에 이름을 붙이면 안쪽 루프에서 바깥 루프까지 한 번에 떠날 수 있다.

04-named-loop.pnx

use io
word is_prime (n int gives bool)
  n 2 lt if false return end
  2 to d
  loop
    d d mul n gt if true return end
    n d mod 0 eq if false return end
    d 1 add to d
  end
end
word first_pair (target int gives str)       rem the first a < b <= 20, both prime, with a + b = target
  2 to a
  0 to b
  loop (outer)
    a 1 add to b
    loop
      b 20 gt until
      a is_prime b is_prime and a b add target eq and until (outer)
      b 1 add to b
    end
    a 1 add to a
    a 20 gt if "none" return end
  end
  cat (a, " + ", b)
end
26 first_pair io.println

출력

$ pnx 04-named-loop.pnx
7 + 19

문답

물음. 루프 몇 겹을 떠날지 수로 적을 수는 없는가?

답. 없다. 수로 적으면 루프를 한 겹 더 감쌀 때 조용히 다른 루프를 떠나게 된다. 이름은 그런 일이 없다.

4.4 고르기

case 는 int 나 str 를 가지의 값과 견준다. 가지 하나에 값이 여럿이면 괄호에 적는다. 맞는 가지가 없을 때 할 일은 otherwise 에 적는다. otherwise 없이 맞는 가지도 없으면 결함이다.

04-case.pnx

use io
word day_kind (day str gives str)
  day case
    ("sat", "sun") of "weekend" end
    ("mon", "tue", "wed", "thu", "fri") of "weekday" end
    otherwise "not a day" end
  end
end
"sun" day_kind io.println
"wed" day_kind io.println
"xyz" day_kind io.println

출력

$ pnx 04-case.pnx
weekend
weekday
not a day

가지의 값은 리터럴이거나 이 소스의 const 여야 한다. err.full 이나 json.key 같은 모듈의 상수는 case 에 쓰지 못하고 eq 로 견준다.

4.5 줄기와 블록

배열이나 목록을 훑는 일은 루프 대신 줄기와 조합 낱말로 쓴다. 배열의 이름이 줄기이고, 뒤에 오는 do … end 블록과 each count fold 같은 낱말이 그것을 받는다. 블록은 그 자리에 펼쳐지므로 바깥의 지역 변수를 쓸 수 있다.

04-stems.pnx

use io
use str
array temps int 7
assign temps (12, 15, 9, 21, 18, 7, 14)
cat ("sum ", temps sum, ", above 13: ", temps do (t int gives bool) t 13 gt end count) io.println
cat ("max ", temps maximum or_else (e) 0 end, ", min ", temps minimum or_else (e) 0 end) io.println
temps do (t int, i int) cat ("day ", i, ": ", t) io.println end each_index
word spread (gives int) 0 temps do (acc int, t int gives int) acc t 10 sub abs add end fold end
cat ("distance from 10, added up: ", spread) io.println

출력

$ pnx 04-stems.pnx
sum 96, above 13: 4
max 21, min 7
day 0: 12
day 1: 15
day 2: 9
day 3: 21
day 4: 18
day 5: 7
day 6: 14
distance from 10, added up: 34

lo hi range 도 줄기다(hi 는 들지 않는다). fold 는 첫 값을 줄기 앞에 두고, 블록이 (지금까지의 값, 원소)를 받아 새 값을 준다.

each, each_index, times 의 블록은 루프처럼 break, while, until 로 일찍 떠난다. 줄기는 멈추고 그 낱말 뒤의 코드가 돈다. 처음 맞는 것에서 멈추는 찾기는 이렇게 쓴다.

04-leave-each.pnx

use io
array prices int 6
word load_prices ()
  0 6 range do (i int) i 7 mul 3 add i set prices end each
end
word first_over (limit int gives r int)
  -1 to found
  prices do (p int, i int)
    p limit gt if i to found break end
  end each_index
  found
end
load_prices
cat ("first over 20 at ", 20 first_over) io.println
cat ("first over 99 at ", 99 first_over) io.println

출력

$ pnx 04-leave-each.pnx
first over 20 at 3
first over 99 at -1

4.6 값인 낱말

조합 낱말에 주지 않은 블록은 값이다. 지역 변수에 넣거나, 돌려주거나, word (서명) 타입의 매개변수에 줄 수 있고, call 로 부른다. 이런 블록은 바깥의 지역 변수를 보지 못한다. 프로그램의 낱말을 값으로 주려면 서명 없이 do 이름 end 라고 쓴다. 그 값은 그 낱말이고, 서명도 그 낱말의 것이다.

04-word-values.pnx

use io
use str
word up (a int, b int gives bool) a b lt end
word down (a int, b int gives bool) a b gt end
word best (before word (int, int gives bool), x int, y int gives int) y x before call if y else x end end
word order_named (how str gives word (int, int gives bool) or fail)
  how case
    "up" of do up end end
    "down" of do down end end
    otherwise err.not_found fail end
  end
end
word demo (or fail)
  "up" order_named or_fail to smallest_first
  "down" order_named or_fail to largest_first
  cat ("smallest ", smallest_first 7 3 best, ", largest ", largest_first 7 3 best) io.println
end
demo or_else (e) e err_text io.println end

출력

$ pnx 04-word-values.pnx
smallest 3, largest 7

문답

물음. str.starts_with 같은 언어의 낱말은 어떻게 값으로 주는가?

답. 블록의 서명을 타입만으로 쓴다: do (str, str gives bool) str.starts_with end. 타입만 쓴 입력은 스택에 남으므로 낱말이 그대로 받는다. 서명이 없으면 블록은 아무것도 받지 않고, 메시지가 두 꼴을 보여 준다.

흔한 오해: case 가지에 낱말 자체를 둘 수 있다: "prefix" of str.starts_with end

가지는 낱말을 실행한다. 값으로 만들지 않는다. 거기서 str.starts_with 가 불리고, 스택에 값이 모자란다. 블록으로 값을 만든다: "prefix" of do (str, str gives bool) str.starts_with end end, 프로그램의 낱말이면 do 이름 end.

04-word-values-branch.pnx

use str
word test_named (how str gives word (str, str gives bool) or fail)
  how case
    "prefix" of str.starts_with end
    otherwise err.not_found fail end
  end
end

거절

$ pnx 04-word-values-branch.pnx
04-word-values-branch.pnx:4:17: the stack does not hold enough values for this word

문답

물음. if 의 두 가지가 다른 타입을 남기고 싶으면?

답. 못 한다. if 뒤의 코드는 어느 가지로 왔는지 모르므로, 두 가지는 같은 개수·같은 타입을 남겨야 한다. 다른 것을 남기고 싶다면 그것은 “값이 있거나 없거나” 이고, 그때는 실패(8장)나 is_none 으로 묻는 줄(7장)을 쓴다.

정리