Lowent 매뉴얼←↑→

24 pipe — 하려는 일만 한 줄씩

먼저 알아야 할 것

7장 흐름 · for 는 슬라이스를 훑고 세는 반복은 while 로 적는다
9장 줄 · mut slice 여야 원소에 쓸 수 있다
22장 제네릭 · 값 대신 이름 붙은 op 을 건넨다

돌아보기

7장에서 “for 로 0 부터 n 까지 세는 반복은 어떻게 적는가” 라는 물음에, 슬라이스를 걸러 세거나 모으는 일은 무엇이 맡는다고 했는가?

답. pipe 가 맡는다고 했다. 세는 반복은 while 과 var 로 적고, 슬라이스를 거르고 바꾸고 모으는 일은 pipe 로 적는다. 이 장이 그 pipe 를 다룬다.

이 장의 필요성과 맥락

슬라이스를 훑는 반복은 거의 언제나 같은 뼈대다. 색인을 두고, 끝인지 보고, 원소를 꺼내 조건을 보고, 무언가를 쌓고, 색인을 늘린다. 이 뼈대를 손으로 쓰면 하려는 일(“숫자만 남기고 센다”)이 색인과 카운터 사이에 묻히고, 경계를 한 칸 틀리는 실수가 거기서 난다. pipe 는 뼈대를 언어가 맡고 사람은 하려는 일만 줄마다 한 낱말로 적게 한다. 그러면서도 손으로 쓴 반복과 똑같이 한 번만 훑는다. 추상(제6부)의 마지막 장으로, 추상이 비용을 숨기지 않는 모양을 보인다.

이 장이 끝나면

pipe <원천> do <스테이지…> <종결자> end 의 모양과, 스테이지 일곱·종결자 다섯을 알게 된다. 스테이지에 이름 붙은 op 을 건네는 법, collect into 로 호출자의 버퍼에 담는 법, fold·any·all·take 가 필요한 만큼만 읽는다는 것을 익힌다. 한 pipe 가 중간 배열 없이 한 번에 훑는 것이 최적화가 아니라 정의라는 점도 이해하게 된다.

이 장에서 답할 질문

  1. 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 는 두 줄이다.

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 → boolop 이 참인 원소만 보낸다—
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]

스테이지에 주는 것은 이름 붙은 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 는 필요한 만큼만 읽는다.