24 pipe — 하려는 일만 한 줄씩
먼저 알아야 할 것
for 는 슬라이스를 훑고 세는 반복은 while 로 적는다mut slice 여야 원소에 쓸 수 있다돌아보기
7장에서 “for 로 0 부터 n 까지 세는 반복은 어떻게 적는가” 라는 물음에, 슬라이스를 걸러 세거나 모으는 일은 무엇이 맡는다고 했는가?
답. pipe 가 맡는다고 했다. 세는 반복은 while 과 var 로 적고, 슬라이스를 거르고 바꾸고 모으는 일은 pipe 로 적는다. 이 장이 그 pipe 를 다룬다.
이 장의 필요성과 맥락
pipe 는 뼈대를 언어가 맡고 사람은 하려는 일만 줄마다 한 낱말로 적게 한다. 그러면서도 손으로 쓴 반복과 똑같이 한 번만 훑는다. 추상(제6부)의 마지막 장으로, 추상이 비용을 숨기지 않는 모양을 보인다.이 장이 끝나면
pipe <원천> do <스테이지…> <종결자> end 의 모양과, 스테이지 일곱·종결자 다섯을 알게 된다. 스테이지에 이름 붙은 op 을 건네는 법, collect into 로 호출자의 버퍼에 담는 법, fold·any·all·take 가 필요한 만큼만 읽는다는 것을 익힌다. 한 pipe 가 중간 배열 없이 한 번에 훑는 것이 최적화가 아니라 정의라는 점도 이해하게 된다.이 장에서 답할 질문
collect into로 쓰는 op 인데 머리가effects none이다. 호출자의 버퍼를 바꾸는데 효과가 없는가?
24.1 같은 일, 두 가지 모양#
examples/ch24/digits.low
module digits .
rem run: digits_loop [97,49,50,98,51]
rem run: digits_pipe [97,49,50,98,51]
fn is_digit input c u8 . output bool .
do
return and (ge c 48) (le c 57) .
end
fn digits_loop input s slice u8 . output u64 .
do
var n u64 be 0 .
var i u64 be 0 .
while lt i (len s) . do
if is_digit (index s i) . do
set n (add n 1) .
end
set i (add i 1) .
end
return n .
end
fn digits_pipe input s slice u8 . output u64 .
do
return pipe s do
filter is_digit .
count .
end .
end
실행 결과
$ lowentc --run digits_loop digits.low [97,49,50,98,51]
digits_loop([97,49,50,98,51]) = 3
arg0 (written) = [97,49,50,98,51]
$ lowentc --run digits_pipe digits.low [97,49,50,98,51]
digits_pipe([97,49,50,98,51]) = 3
arg0 (written) = [97,49,50,98,51]
두 op 은 같은 답을 낸다. digits_loop 는 카운터 n, 색인 i, 끝 조건, 증가를 사람이 맞춘다. digits_pipe 는 두 줄이다.
filter is_digit .—is_digit이 참인 원소만 남긴다.count .— 남은 원소를 센다. 흐름을 끝내는 낱말(종결자)은 정확히 하나다.
pipe 는 문장이지만 값을 내는 종결자(count·fold·any·all)로 끝나면 식처럼 return pipe s do … end . 로 쓸 수 있다 — 끝의 점은 return 문장의 것이다(end 는 pipe 블록만 닫는다).
원소가 줄지어 흘러가는 관으로 그리면 이렇다.
s = "a1b22"
┌─────────────────┐ ┌───────┐
a 1 b 2 2 ─▶ │ filter is_digit │ ─▶ 1 2 2 ─▶ │ count │ ─▶ 3
└─────────────────┘ └───────┘
스테이지: 거른다 종결자: 흐름을 끝내고 값을 낸다원소는 하나씩 관을 지나간다. 중간에 «숫자만 모은 배열» 같은 것이 따로 만들어지지 않는다.
24.2 스테이지와 종결자#
| 낱말 | op 의 모양 | 하는 일 | 끝·빈 흐름 |
|---|---|---|---|
filter <op> | t → bool | op 이 참인 원소만 보낸다 | — |
map <op> | t → u | 원소마다 op 의 답을 보낸다. 타입이 u 로 바뀔 수 있다 | — |
take <n> | — | 앞의 n 개만 보낸다 | n 개 뒤 훑기를 멈춘다 |
skip <n> | — | 앞의 n 개를 버린다 | — |
enumerate <op> | (u64, t) → u | 순번(0 부터)과 원소를 op 에 준다 | — |
zip <슬라이스> <op> | (t, t2) → u | 원소와 다른 슬라이스의 같은 자리 원소를 op 에 준다 | 짧은 쪽이 끝나면 끝난다 |
scan <초기값> <op> | (a, t) → a | 누산한 뒤의 값을 원소로 보낸다(초기값은 안 보낸다) | — |
collect into <버퍼> | — | 종결자. 버퍼에 앞에서부터 담고 담은 개수(u64)를 낸다 | 버퍼가 모자라면 길이를 알 때 번역이 거절(E-COLLECT-FULL), 모를 때 실행 중 멈춤 |
fold <초기값> <op> | (a, t) → a | 종결자. 누산한 마지막 값(a)을 낸다 | 비면 초기값 |
count | — | 종결자. 원소 수(u64)를 낸다 | 비면 0 |
any <op> | t → bool | 종결자. 참인 원소가 있는가 | 첫 참에서 멈춘다 · 비면 거짓 |
all <op> | t → bool | 종결자. 모두 참인가 | 첫 거짓에서 멈춘다 · 비면 참 |
표 24.1 — pipe 의 낱말 — 원소 타입 t, 누산값 타입 a
filter·any·all 에 주는 op 은 판정이라 bool 을 내야 한다. 수를 내는 op 을 주면 E-PIPE-PRED 로 거절된다 — 0 아닌 수를 참으로 읽는 규칙은 이 언어에 없다(13장의 cast 와 같은 까닭).
pipe 를 return pipe … end . 처럼 값 자리에 쓰면 그 값은 종결자가 낸 값이다. 스테이지는 줄마다 하나씩 원소를 다음 줄로 넘기고, 종결자가 마지막에 받아 값 하나로 만든다.
xs = [1, 5, 2, 7]
│
▼ filter big (big = gt x 2) 5, 7 1·2 는 여기서 버린다
▼ map dbl (dbl = x + x) 10, 14
▼ collect into out out = [10, 14, …] 답 = 2 (담은 개수)examples/ch24/stages.low
module stages .
rem run: big_doubled [1,2,3,4,5] [0,0,0,0,0]
rem run: sum_big [1,2,3,4,5]
rem run: has_zero [4,0,9]
rem run: all_small [1,2,3]
rem run: middle [10,20,30,40] [0,0]
fn over2 input a u8 . output bool .
do
return gt a 2 .
end
fn dbl input a u8 . output u8 .
do
return wrap_add a a .
end
fn addu input acc u64 . input x u8 . output u64 .
do
return wrap_add acc (widen u64 x) .
end
fn is_zero input a u8 . output bool .
do
return eq a 0 .
end
fn under10 input a u8 . output bool .
do
return lt a 10 .
end
proc big_doubled input xs slice u8 . input out mut slice u8 . output u64 . effects none .
do
pipe xs do
filter over2 .
map dbl .
collect into out .
end
return len out .
end
fn sum_big input xs slice u8 . output u64 .
do
return pipe xs do
filter over2 .
fold 0 addu .
end .
end
fn has_zero input xs slice u8 . output bool .
do
return pipe xs do
any is_zero .
end .
end
fn all_small input xs slice u8 . output bool .
do
return pipe xs do
all under10 .
end .
end
proc middle input xs slice u8 . input out mut slice u8 . output u64 . effects none .
do
pipe xs do
skip 1 .
take 2 .
collect into out .
end
return len out .
end
실행 결과
$ lowentc --run big_doubled stages.low [1,2,3,4,5] [0,0,0,0,0]
big_doubled([1,2,3,4,5], [6,8,10,0,0]) = 5
arg0 (written) = [1,2,3,4,5]
arg1 (written) = [6,8,10,0,0]
$ lowentc --run sum_big stages.low [1,2,3,4,5]
sum_big([1,2,3,4,5]) = 12
arg0 (written) = [1,2,3,4,5]
$ lowentc --run has_zero stages.low [4,0,9]
has_zero([4,0,9]) = 1
arg0 (written) = [4,0,9]
$ lowentc --run all_small stages.low [1,2,3]
all_small([1,2,3]) = 1
arg0 (written) = [1,2,3]
$ lowentc --run middle stages.low [10,20,30,40] [0,0]
middle([10,20,30,40], [20,30]) = 2
arg0 (written) = [10,20,30,40]
arg1 (written) = [20,30]
big_doubled는 2 보다 큰 것만 남겨(filter) 두 배로 만들고(map)out에 담는다(collect into). 결과[6,8,10,0,0]에서 담기지 않은 자리는 그대로다. 담을 버퍼는 부르는 쪽이 준다.pipe는 할당하지 않는다.sum_big의fold 0 addu는 0 에서 시작해addu로 누적한다.has_zero는any is_zero,all_small은all under10이다.middle은skip 1과take 2로 가운데 두 원소를 담는다.
스테이지에 주는 것은 이름 붙은 op 이다. 이 언어에는 이름 없는 함수(람다)가 없다. 스테이지 op 은 원소 하나를 받고 권한을 받지 않는다. fold·scan 의 op 은 누산값과 원소 둘을 받는다.
튜플이 없는데 순번이나 짝은 어떻게 다루는가? 쌍을 만들지 않고 op 에 인자로 건넨다.
examples/ch24/pairs.low
module pairs .
rem run: with_index [10,10,10] [0,0,0]
rem run: added [1,2,3] [10,20,30] [0,0,0]
rem run: running [1,2,3,4] [0,0,0,0]
fn idxadd input i u64 . input e u8 . output u8 .
do
return wrap_add e (narrow_wrap u8 i) .
end
fn addb input a u8 . input b u8 . output u8 .
do
return wrap_add a b .
end
proc with_index input xs slice u8 . input out mut slice u8 . output u64 . effects none .
do
pipe xs do
enumerate idxadd .
collect into out .
end
return 0 .
end
proc added input xs slice u8 . input ys slice u8 . input out mut slice u8 . output u64 . effects none .
do
pipe xs do
zip ys addb .
collect into out .
end
return 0 .
end
proc running input xs slice u8 . input out mut slice u8 . output u64 . effects none .
do
pipe xs do
scan 0 addb .
collect into out .
end
return 0 .
end
실행 결과
$ lowentc --run with_index pairs.low [10,10,10] [0,0,0]
with_index([10,10,10], [10,11,12]) = 0
arg0 (written) = [10,10,10]
arg1 (written) = [10,11,12]
$ lowentc --run added pairs.low [1,2,3] [10,20,30] [0,0,0]
added([1,2,3], [10,20,30], [11,22,33]) = 0
arg0 (written) = [1,2,3]
arg1 (written) = [10,20,30]
arg2 (written) = [11,22,33]
$ lowentc --run running pairs.low [1,2,3,4] [0,0,0,0]
running([1,2,3,4], [1,3,6,10]) = 0
arg0 (written) = [1,2,3,4]
arg1 (written) = [1,3,6,10]
enumerate idxadd 는 idxadd 에 순번과 원소를 준다. zip ys addb 는 xs 의 원소와 ys 의 같은 자리 원소를 addb 에 준다. 짧은 쪽이 끝나면 흐름 전체가 끝나서 짝이 늘 맞는다. scan 0 addb 는 누적합을 원소로 흘린다. 쌍을 만들었다 곧장 풀지 않으므로 숨은 할당도 없다.
문. collect into 로 쓰는 op 인데 머리가 effects none 이다. 호출자의 버퍼를 바꾸는데 효과가 없는가?
답. 명세의 예제도 effects none 으로 적는다. collect into 는 effects state 를 요구하지는 않지만, 적어 두면 처리기가 그것을 호출자 저장소 쓰기로 세므로 “선언했지만 하지 않는다” 는 경고는 나지 않는다 — mut·mut_ref 매개변수에 쓰는 자리와 같은 판정이다. 어느 쪽으로 적든 호출자는 mut slice 를 건네는 자리에서 쓰기가 일어날 수 있다는 것을 안다(12장).
24.3 한 번에 훑는 것은 정의다#
한 pipe 는 한 번의 훑기로 실행되고 스테이지 사이에 중간 배열이 만들어지지 않는다. 이것은 처리기가 해 볼 수 있는 최적화가 아니라 pipe 의 정의이고, 중간 배열을 만드는 처리기는 적합하지 않다.
왜 정의로 두는가. 융합을 최적화로 두면 “어디까지 융합되는가” 가 처리기마다 다르고, 쓰는 사람은 절벽을 만난다. 한 줄을 고쳤더니 갑자기 중간 배열이 생기고 느려지는 자리다. 그 절벽은 소스만 봐서는 안 보인다. Lowent 는 융합할 수 없는 연산을 스테이지 낱말에 아예 넣지 않았다. 정렬처럼 전체를 봐야 하는 연산이 스테이지에 없는 이유다.
examples/ch24/bad_stage.low
module bad_stage .
rem expect: E-PIPE-STAGE
fn is_digit input c u8 . output bool .
do
return and (ge c 48) (le c 57) .
end
fn broken input s slice u8 . output u64 .
do
return pipe s do
sort .
count .
end .
end
실행 결과
$ lowentc --check bad_stage.low
bad_stage.low:12:0 E-PIPE-STAGE: this word is not a `pipe` stage. The stage vocabulary is CLOSED (RFC-0010 G5/D-A): stages are `filter <op> .`, `map <op> .`, `take <n> .`, `skip <n> .`, `enumerate <op> .`, `zip <other> <op> .` and `scan <init> <op> .`, and the pipeline ends with exactly one terminal — `collect into <mut slice> .`, `fold <init> <op> .`, `count .`, `any <op> .` or `all <op> .`. Fusion here is the MEANING, not an optimization, so anything that could not fuse into the single loop is not writable as a stage — use an explicit loop, or break the pipeline with an explicit intermediate collect
스테이지 목록은 닫혀 있다. pipe 가 식이 아니라 문장인 것도 같은 이유다. 식이었다면 스테이지를 값으로 떼어 넘길 수 있고, 그러면 어디까지가 한 줄기인지 소스에서 안 보인다. do … end 가 그 경계를 눈에 보이게 한다.
24.4 필요한 만큼만 읽는다#
any 는 참을 내는 첫 원소에서, all 은 거짓을 내는 첫 원소에서, take n 은 n 개를 지나보낸 뒤 훑기를 멈춘다. 멈춘 뒤의 원소는 읽히지 않고 그 원소에 대한 스테이지도 실행되지 않는다. has_zero [4,0,9] 는 9 를 보지 않는다. 이 성질이 의미에 적혀 있어야 끝이 없는 원천도 pipe 로 다룰 수 있다.
종결자는 흐름을 끝낸다. 종결자 뒤에 스테이지가 오면 거절된다.
examples/ch24/no_terminal.low
module no_terminal .
rem expect: E-PIPE-NO-TERMINAL
fn is_digit input c u8 . output bool .
do
return and (ge c 48) (le c 57) .
end
fn broken input s slice u8 . output u64 .
do
return pipe s do
count .
filter is_digit .
end .
end
실행 결과
$ lowentc --check no_terminal.low
no_terminal.low:13:0 E-PIPE-NO-TERMINAL: a `pipe` stage may not come AFTER the terminal — the terminal ends the pipeline (RFC-0010 G1)
흔한 오해. pipe 는 편하지만 손으로 쓴 반복보다 느리다
pipe 는 한 번 훑기가 정의이고 스테이지 op 은 단형화된 직접 호출이다. pipe 하나는 반복 하나로 낮아지고, 비용은 손으로 쓴 반복과 같은 자리에서 생긴다. 편한 쪽과 빠른 쪽이 갈리지 않도록 낱말을 고른 결과다.24.5 이름이 같은 내장 map·filter#
pipe 밖에도 map·filter 가 있다. 이쪽은 스테이지가 아니라 문 하나로 슬라이스를 옮겨 담는 내장 연산이고, 인자가 셋이다 — map <받는 자리> <op> <원천> . · filter <받는 자리> <op> <원천> ..
module sink_map .
fn dbl input a u8 . output u8 . do return wrap_add a a . end
proc doubled input xs slice u8 . input out mut slice u8 . output u64 . effects none .
do
map out dbl xs .
return 0 .
end[1,2] 를 세 칸짜리 out 에 담으면 [2,4,0] 이다. 거꾸로 [1,2,3] 을 두 칸에 담으려 하면 셋째를 쓰는 순간 멈춘다 — 받는 자리가 차도 말없이 멈추는 일은 없다(collect into 와 같다). 받는 자리는 mut slice 여야 하고(E-MAP-SINK), 원소는 스칼라여야 한다(E-MAP-ELEM). 낱말은 같지만 pipe 안에서는 op 하나만 받는다 — 인자 수를 보면 어느 쪽인지 안다.
24.6 흔한 실수#
반례. 스테이지에 식을 적어 람다를 흉내 낸다
examples/ch24/mistake_lambda.low
module mistake_lambda .
rem expect: E-FOLD-OP
fn count_big input xs slice u8 . output u64 .
do
return pipe xs do
rem ✘ 이름 없는 함수를 흉내 냈다 --- 스테이지에는 이름 붙은 op 하나를 건넨다
filter gt 2 .
count .
end .
end
실행 결과
$ lowentc --check mistake_lambda.low
mistake_lambda.low:8:0 E-FOLD-OP: this `pipe` stage names an op that does not exist
다른 언어의 filter(x => x > 2) 를 옮기면 filter gt 2 가 되기 쉽다. 스테이지는 이름 붙은 op 하나를 받으므로 E-FOLD-OP 다(진단은 “그런 op 이 없다” 고만 말한다). 조건에 이름을 붙이는 것은 번거로워 보이지만, over2 라는 이름이 곧 그 줄의 설명이 되고 같은 조건을 다른 pipe 에서도 쓴다.
examples/ch24/lambda_fixed.low
module lambda_fixed .
rem run: count_big [1,2,3,4,5]
rem 조건에 이름을 붙인다 --- 이름이 곧 설명이 된다
fn over2 input a u8 . output bool .
do
return gt a 2 .
end
fn count_big input xs slice u8 . output u64 .
do
return pipe xs do
filter over2 .
count .
end .
end
실행 결과
$ lowentc --run count_big lambda_fixed.low [1,2,3,4,5]
count_big([1,2,3,4,5]) = 3
arg0 (written) = [1,2,3,4,5]
반례. 넓은 값을 내는 map 을 좁은 버퍼에 담는다
examples/ch24/mistake_widecollect.low
module mistake_widecollect .
rem expect: E-TYPE-COLLECT
fn times1000 input a u8 . output u64 .
do
return mul (widen u64 a) 1000 .
end
proc scale input xs slice u8 . input out mut slice u8 . output u64 . effects none .
do
pipe xs do
rem ✘ `u64` 를 내는 `map` 을 `u8` 버퍼에 담는다 --- 1000 이 232 가 된다
map times1000 .
collect into out .
end
return 0 .
end
실행 결과
$ lowentc --check mistake_widecollect.low
mistake_widecollect.low:14:0 E-TYPE-COLLECT: this `collect into` would put a WIDER value into a narrower buffer, and that loses bits silently (measured: 1000 became 232). A conversion that loses value never happens implicitly here (§6.2.5) — put a `map` in front that says which narrowing you mean (`narrow`, `narrow_wrap`, `narrow_sat`, `narrow_try`), or collect into a buffer of the produced width
times1000 은 u64 를 내는데 out 은 u8 버퍼다. 값을 잃는 좁히기는 적어야만 일어나므로(§6.2.5) E-TYPE-COLLECT 로 거절된다. 2026-09-16 까지는 통과했고 1000 과 2000 이 조용히 232 와 208 로 담겼다. 좁혀야 한다면 map 의 op 안에서 narrow·narrow_wrap· narrow_sat 로 어느 좁히기인지 적어 멈출 자리를 드러낸다.
반례. fold 의 op 에서 누산값과 원소의 차례를 바꾼다
examples/ch24/mistake_foldorder.low
module mistake_foldorder .
rem expect: E-FOLD-ORDER
rem ✘ 원소를 먼저 받았다 --- 누산값이 `x` 로, 원소가 `acc` 로 들어온다
fn add_small_swapped input x u8 . input acc u64 . output u64 .
requires le acc 1000000 .
do
guard lt x 100 . else return acc .
return add acc (widen u64 x) .
end
fn sum_small_wrong input xs slice u8 . output u64 .
do
return pipe xs do
fold 0 add_small_swapped .
end .
end
실행 결과
$ lowentc --check mistake_foldorder.low
mistake_foldorder.low:15:0 E-FOLD-ORDER: the accumulator of a `fold`/`scan` is the FIRST input of its op and also its output, because each step computes `acc = op(acc, element)`. Here the first input and the output are declared with different types, so the running total is handed to a parameter that cannot hold it — a `u8` first input takes a total of 600 as 88 and the answer is silently wrong. Write the op as `input acc <out-type> . input x <element-type> .`
fold 는 op 에 누산값을 먼저, 원소를 다음 에 준다. add_small_swapped 는 차례를 거꾸로 받았으므로 x 에 누산값이, acc 에 원소가 들어온다. 한 걸음이 누산값 = op(누산값, 원소) 이므로 첫 입력의 타입은 출력 타입과 같아야 하는데 여기서는 u8 과 u64 로 다르다 — E-FOLD-ORDER 로 거절한다. fold·scan 의 op 은 언제나 input acc … . input x … . 차례로 적는다.
반례. 멈출 수 있는 스테이지 op 을 쓰고 fn 으로 적는다
examples/ch24/mistake_stageeffect.low
module mistake_stageeffect .
rem expect: E-EFFECT-CALC
proc nonzero input a u8 . output bool . effects panic .
do
if eq a 0 . do panic "zero in input" . end
return true .
end
rem ✘ 스테이지 op 이 멈출 수 있는데 `pipe` 를 쓴 op 을 `fn` 으로 적었다
fn count_checked input xs slice u8 . output u64 .
do
return pipe xs do
filter nonzero .
count .
end .
end
실행 결과
$ lowentc --check mistake_stageeffect.low
mistake_stageeffect.low:12:1 E-EFFECT-CALC: this fn is declared pure but performs `panic` — make it a `proc` with `effects …`, or remove the effect
pipe 는 스테이지 op 을 부르는 반복이므로, 스테이지 op 의 효과가 pipe 를 쓴 op 으로 번진다. nonzero 가 panic 할 수 있으니 count_checked 도 panic 을 낸다. 그래서 fn 이면 E-EFFECT-CALC 다. proc … effects panic . 으로 적거나, 멈추는 대신 조건을 만족하지 않는 원소를 걸러 내는 순수한 op 을 쓴다.
흔한 오해. collect into 는 버퍼가 차면 남는 것을 버리고 끝난다
examples/ch24/short_buffer.low
module short_buffer .
rem run: big_doubled [1,3,4] [0,0]
rem trap: big_doubled [3,4,5,6] [0,0]
fn over2 input a u8 . output bool .
do
return gt a 2 .
end
fn dbl input a u8 . output u8 .
do
return wrap_add a a .
end
rem 버퍼는 두 칸 --- 남는 원소가 둘이면 담기고, 넷이면 셋째를 담으려는 순간 멈춘다
proc big_doubled input xs slice u8 . input out mut slice u8 . output u64 . effects none .
do
pipe xs do
filter over2 .
map dbl .
collect into out .
end
return len out .
end
실행 결과
$ lowentc --run big_doubled short_buffer.low [1,3,4] [0,0]
big_doubled([1,3,4], [6,8]) = 2
arg0 (written) = [1,3,4]
arg1 (written) = [6,8]
$ lowentc --run big_doubled short_buffer.low [3,4,5,6] [0,0]
== ir diagnostics (1) ==
0:0 E-VM-BOUNDS: slice index out of bounds on write (panic)
[1,3,4] 에서 남는 원소는 둘(3·4)이라 두 칸에 담긴다. [3,4,5,6] 은 넷이라 셋째를 담으려는 순간 멈춘다. 예전에는 두 칸에서 말없이 끝났고, 남은 둘이 사라진 것을 아무도 몰랐다. 두 길이를 번역 시점에 알면(array 5 u8 입력처럼) 멈출 것도 없이 E-COLLECT-FULL 로 거절된다. pipe 는 할당하지 않으므로 버퍼를 늘려 주지도 않는다. 들어갈 만큼만 담으려면 take 2 를 적는다.
24.7 이 장의 문법 한눈에#
| 모양 | 뜻 | 왜 이렇게 |
|---|---|---|
pipe xs do … end | 원천 xs 를 한 번 훑는다 | 뼈대(색인·끝 조건)를 언어가 맡는다 |
filter over2 . · map dbl . | 남기기 · 바꾸기 — 이름 붙은 op 을 건넨다 | 람다가 없다 — 이름이 설명이 된다 |
take 2 . · skip 1 . | 앞 몇 개만 · 앞 몇 개 버리기 | 필요한 만큼만 읽는다 |
enumerate idxadd . · zip ys addb . | 순번·짝을 op 의 인자로 건넨다 | 튜플을 만들지 않는다 |
scan 0 addb . · fold 0 addu . | 누적해 흘리기 · 누적해 값 하나 | op 은 누산값이 먼저, 원소가 다음 |
count . · any is_zero . · all under10 . | 값을 내는 종결자 | return pipe … end 로 쓸 수 있다 |
collect into out . | 부르는 쪽 버퍼에 담는다 | pipe 는 할당하지 않는다 — 모자라면 거절되거나 멈춘다 |
| 종결자는 정확히 하나, 맨 끝 | 뒤에 스테이지가 오면 E-PIPE-NO-TERMINAL | 흐름의 끝이 한곳에 보인다 |
sort 같은 낱말 | 없다 — E-PIPE-STAGE | 융합할 수 없는 연산은 넣지 않았다 |
표 24.2 — pipe 의 문법 — 모양 · 뜻 · 왜 이렇게 생겼나
복습 정리
pipe <원천> do … end 는 스테이지(filter·map·take·skip·enumerate·zip·scan)를 거쳐 종결자 (collect into·fold·count·any·all) 하나로 끝난다. 스테이지에는 이름 붙은 op 을 건네고, 담을 버퍼는 부르는 쪽이 준다. 한 번 훑기와 중간 배열 없음은 정의이며, 융합할 수 없는 연산은 낱말에 없다. any·all·take 는 필요한 만큼만 읽는다.