Elm으로 만든 장난감 스케줄러를 통해 BEAM의 핵심 원리(프로세스 스폰, 메시지 전송/선택적 수신, 감축 예산을 이용한 선점형처럼 보이는 스케줄링, 링크/크래시 전파)를 바닥부터 구현하고 설명합니다.
This is my Code BEAM Europe 2025 talk, converted to a blogpost.
EDIT 2025-11-10: Hacker News folks pointed out it might not be clear to everybody what BEAM is: it’s the virtual machine for languages like Erlang, Elixir and Gleam. See Wikipedia.
저는 항상 BEAM이 매력적이었습니다. 상태를 공유하지 않는 프로세스를 쉽게 스폰하고, 메시지를 보내고 선택적으로 수신할 수 있으며, 서로 링크하여 슈퍼비전 트리를 만들 수 있게 해주는 점 말이죠.
이는 서로 잘 상호작용하는 흥미로운 원시 기능(primitive)들의 묶음이며, 제 관점에서 BEAM 계열 언어의 매력을 크게 책임지고 있습니다. 이 원시 기능들을 지원하려면 얼마나 필요한지 직접 확인해보고 싶었고, 그래서 제 나름의 장난감 수준 MVP BEAM 구현을 작성해 보기로 했습니다.
미리 밝히자면, 저는 아직 The BEAM Book을 읽지 않았고, 제가 하는 방식은 실제 BEAM이 하는 방식과 꽤 다를 수 있습니다. 이 글은 제가 바깥에서 인지하고 있는 BEAM의 모습을 바탕으로, 일종의 제1원리 탐구이며, 참조 구현과의 정확한 일치나 실사용 유용성, 성능을 목표로 하지 않습니다.
아래 예시들은 Elm으로 작성되어 있습니다. 하지만 Elm으로 표현할 수 있다면 무엇으로든 표현할 수 있습니다(Elm은 순수 함수형이라 가변 상태가 없고, 단일 스레드에 동시성 원시 기능도 없습니다 등).
저는 전체 언어나 VM이 아니라 스케줄러와 그 메인 루프만 만들 것입니다. 이렇게 하면 파서나 CLI 같은, 진짜 컴파일러에 필요한 많은 부분을 건너뛰고, 몇 개의 하드코딩된 예시만 유지하면 됩니다.
가능한 일을 최대한 줄이기 위해, 예제 프로그램은 흔한 “문장 리스트” 스타일 대신 CPS(Continuation Passing Style) 로 작성하겠습니다:
-- ☑️ YES: continuations
type Program
= End
| Work Int K
| Spawn Program KPid
| Send Pid String K
| Receive String K
| Crash
| Link Pid K
type alias K =
() -> Program
type alias KPid =
Pid -> Program
-- ❌ NO: list of statements
type Stmt
= Let String Expr
| Work Int
| Spawn Program
| Send Pid String
| Receive String Program
| Crash
| Link Pid
type alias Program =
List Stmt
이는 환경, 바인딩, 스코프, 반환값, 표현식 등을 신경 쓰지 않아도 된다는 뜻입니다. 이러한 것들은 호스트 언어의 continuation 인자로 처리되니까요:
ex5 : Program
ex5 =
Spawn ex5Child <| \childPid ->
Send childPid "Ping" <| \() ->
End
ex5Child =
Work 10 <| \() ->
End
<| 연산자를 읽는 데 어려움이 있다면, 괄호 쌍으로 상상하셔도 됩니다:
ex5 : Program
ex5 =
Spawn ex5Child (\childPid ->
Send childPid "Ping" (\() ->
End
))
ex5Child =
Work 10 (\() ->
End
)
End종결이 아닌 모든 명령의 continuation 때문에, 최소 하나의 종결 지점이 필요합니다. 그렇지 않으면 유효한 Program 값을 쓸 수 없겠지요.
그렇다면 종결 지점 중 하나인 End부터 구현해 봅시다. 아무 일도 하지 않지만, 스케줄러의 구조를 보여주기에 좋습니다.
type Program =
End
ex1 : Program
ex1 =
End
type alias Scheduler =
{ program : Program }
init : Program -> Scheduler
init program =
{ program = program }
step : Scheduler -> Scheduler
step sch =
case sch.program of
End -> sch
Try it online, 또는 아래 시각화 도구를 사용해 보세요:
Step 1
ex1 = End
다음에 실행 예정
End
아직 단계 없음
모든 것은 이 Scheduler 타입과 그 step 함수 중심으로 돌아갑니다. 이제 기능을 확장해 봅시다.
Work실제 작업(수학 연산자, 함수 호출 등)을 위한 명령을 구현하는 데 시간을 쓰지 말고, 더미 Work 명령 하나로 포괄해 봅시다. 이 명령은 작업량(곧 의미가 생길 단위)과 이후에 무엇을 할지에 대한 continuation을 가집니다:
type Program
= End
-- 추가:
| Work Int K
type alias K =
() -> Program
ex2 : Program
ex2 =
Work 5 <| \() ->
End
예제는 5 단위의 “작업”을 한 뒤 종료하는 프로그램입니다.
이 새 명령을 step 함수에 추가해야 합니다:
step : Scheduler -> Scheduler
step sch =
case sch.program of
End -> sch
-- 추가:
Work n k -> { sch | program = k () }
현재는 작업량을 무시하고, 나머지 프로그램(k () 호출 결과)으로 계속 진행하겠습니다.
Try it online, 또는 아래 시각화 도구를 사용해 보세요:
Step 1
ex2 = Work 5 End
다음에 실행 예정
Work: 5
아직 단계 없음
Spawn이제 좀 더 “재미있는” 것을 해봅시다! 다른 프로세스를 스폰할 수 있게 해서, 프로그램을 동시적으로 만들어 보겠습니다.
type Program =
-- ...
| Spawn Program KPid
type alias KPid =
Pid -> Program
type alias Pid =
Int
ex3 : Program
ex3 =
Work 5 <| \() ->
Spawn ex3Child <| \childPid ->
Work 5 <| \() ->
End
ex3Child : Program
ex3Child =
Work 10 <| \() ->
Work 10 <| \() ->
End
프로세스를 스폰하면 continuation으로 그 PID를 받습니다. 이는 나중에 메시징 등에서 유용하게 쓰일 것입니다.
이는 Scheduler에 큰 변화를 가져옵니다. 이제 단일 프로그램이 아닌, 여러 프로세스를 추적해야 합니다!
type alias Scheduler =
{ processes : Dict Pid Proc
, nextUnusedPid : Pid
, readyQueue : Queue Pid
}
type alias Proc =
{ program : Program }
init : Program -> Scheduler
init program =
{ processes = Dict.empty
, nextUnusedPid = 0
, readyQueue = Queue.empty
}
|> spawn program
|> Tuple.first -- 스폰된 PID는 버림
spawn : Program -> Scheduler -> ( Scheduler, Pid )
spawn program sch =
let pid = sch.nextUnusedPid in
( { sch
| processes =
sch.processes
|> Dict.insert pid (initProc program)
, nextUnusedPid = pid + 1
}
|> enqueue pid
, pid
)
initProc : Program -> Proc
initProc program =
{ program = program }
enqueue : Pid -> Scheduler -> Scheduler
enqueue pid sch =
{ sch
| readyQueue =
if List.member pid (Queue.toList sch.readyQueue)
then sch.readyQueue
else sch.readyQueue |> Queue.enqueue pid
}
이제 프로세스들은 Dict에 보관되며, PID 증가 같은 부수적인 작업도 있고, “준비 큐(ready queue)”라는 새 개념도 있습니다.
이 큐는 스케줄러에게 다음에 어떤 프로세스를 실행할지 알려줍니다. 따라서 step 함수도 크게 바뀌어야 합니다. 이전에는 단일 sch.program만 고르면 됐지만, 이제는 큐에서 PID를 뽑고, 딕셔너리에서 찾아, 그제야 실행해야 합니다:
step : Scheduler -> Scheduler
step sch =
case Queue.dequeue sch.readyQueue of
Nothing -> sch
Just ( pid, restOfQueue ) ->
let newSch = { sch | readyQueue = restOfQueue } in
case Dict.get pid newSch.processes of
Nothing -> newSch
Just proc -> newSch |> stepInner pid proc
stepInner : Pid -> Proc -> Scheduler -> Scheduler
stepInner pid proc sch =
case proc.program of
End -> sch
Work n k ->
sch
|> updateProc pid (setProgram (k ()))
|> enqueue pid
updateProc : Pid -> (Proc -> Proc) -> Scheduler -> Scheduler
updateProc pid fn sch =
{ sch | processes =
sch.processes
|> Dict.update pid (Maybe.map fn)
}
setProgram : Program -> Proc -> Proc
setProgram newProgram proc =
{ proc | program = newProgram }
stepInner의 세부사항도 바뀌었습니다. 더는 단일 sch.program을 설정할 수 없고, 해당 PID의 프로세스를 딕셔너리에서 업데이트해야 합니다.
새 명령도 잊지 말아야겠죠:
stepInner pid proc sch =
-- ...
Spawn childProgram kpid ->
let ( schWithChild, childPid ) =
sch |> spawn childProgram
in schWithChild
|> updateProc pid (setProgram (kpid childPid))
|> enqueue pid
아까의 spawn 함수를 재사용합니다. 자식 PID를 얻었고, 이를 continuation에 넘겨 나머지 부모 프로그램을 이어갑니다: kpid childPid.
부모를 다시 큐에 넣는 것도 잊지 말아야 합니다(continuation이 반환한 프로그램은 아직 실행되지 않았습니다). 자식은 spawn 함수에서 이미 큐에 들어갔습니다.
Try it online, 또는 아래 시각화 도구를 사용해 보세요.
이제 스케줄러가 전체 프로그램을 끝내는 데 7단계가 걸립니다. 이는 초기 프로그램의 7개 명령과 대응됩니다.
Step 1
ex3 = Work 5 Spawn ex3Child Work 5 End ex3Child = Work 10 Work 10 End
| PID | 상태 | 프로그램 |
|---|---|---|
| 0 | 다음에 실행 예정 | Work: 5 |
아직 단계 없음
현재 스케줄러에 잠재적 문제가 보이시나요?
지금 구현한 동시성은 협력형(cooperative) 입니다. 시작된 프로세스는 중간에 스케줄러가 멈추지 않습니다. 이 프로그램을 생각해 봅시다:
ex4 : Program
ex4 =
Work 5 <| \() ->
Spawn ex4Child <| \childPid ->
Work 5 <| \() ->
End
ex4Child : Program
ex4Child =
Work 999 <| \() -> -- !!!
Work 10 <| \() ->
End
예제 3과 다른 점은 자식이 하는 작업량뿐입니다. 부모는 자식이 999 단위의 작업을 끝내기 전까지, 스폰 이후의 작은 작업을 마칠 수 없습니다.
BEAM은 이를 감축 예산(reduction budget) 으로 해결합니다. 각 함수 호출 뒤에 양보 지점(yield point)을 삽입하고, 호출마다 예산을 1씩 줄입니다. 예산이 0이 되면, 스케줄러가 해당 프로세스를 일시정지시키고 큐에서 다른 프로세스를 시작합니다. 협력형 위에 선점형처럼 보이는 스케줄링의 환상을 만든 것이죠.
이는 놀라울 정도로 잘 동작합니다. BEAM 언어에서는 리스트를 재귀로 순회하므로 → 함수 호출이 많고 → 양보 지점도 많습니다.
우리의 장난감 구현에서도 비슷하게 하겠습니다. 감축 예산을 도입하고, Work 명령이 예산이 허락하는 만큼만 “작업”을 수행하도록 만듭니다.
reductionBudget : Int
reductionBudget =
7 -- BEAM은 4000으로 설정합니다.
step : Scheduler -> Scheduler
step sch =
-- ...
sch |> stepInner pid proc {- 추가: -} reductionBudget
stepInner : Pid -> Proc -> Int -> Scheduler -> Scheduler
stepInner pid proc budget sch =
if budget <= 0
then sch
|> setProc pid proc
|> (if shouldEnqueue proc
then enqueue pid
else identity)
else -- ...
shouldEnqueue : Proc -> Bool
shouldEnqueue proc =
case proc.program of
-- 최적화: `End`에 도달했다면 다시 실행할 필요가 없음
End -> False
Work _ _ -> True
Spawn _ _ -> True
setProc : Pid -> Proc -> Scheduler -> Scheduler
setProc pid newProc sch =
sch
|> updateProc pid (\_ -> newProc)
위 코드는 프로세스의 예산이 바닥났을 때의 처리입니다. 스케줄러는 어디까지 실행했는지 기억하고, 더 할 일이 있다면 다시 큐에 넣은 뒤(즉 End가 아니라면), 현재 스텝을 멈춥니다.
이제 stepInner의 나머지를 채워 봅시다:
stepInner pid proc budget sch =
-- ...
let
stop : Scheduler -> Scheduler
stop sch_ =
sch_ |> stepInner pid program 0
continue : Program -> Int -> Scheduler -> Scheduler
continue newProgram newBudget sch_ =
sch_ |> stepInner pid newProgram newBudget
in
-- ...
예산을 다루기 위한 헬퍼입니다. stop은 예산을 0으로 만들고 재귀 호출하여, 곧장 if budget <= 0 then ... 분기로 가게 합니다.
continue는 예산을 임의의 수로 설정합니다. 보통은 현재 예산에서 1을 뺍니다. 다만 Work의 경우 더 큰 단위로 점프합니다.
이제 사용해 봅시다:
stepInner pid proc budget sch =
-- ...
in
case program of
End -> sch |> stop
Spawn childProgram kpid ->
let ( schWithChild, childPid ) =
sch |> spawn childProgram
in schWithChild
|> continue (kpid childPid) (budget - 1)
Work n k ->
if n <= 0
then sch |> enqueue pid
|> continue (k ()) budget
else let workDone = min n budget
workRemaining = n - workDone
budgetRemaining = budget - workDone
in sch |> continue (Work workRemaining k)
budgetRemaining
Work 명령은 이제 완전히 다르게 동작합니다. 예전처럼 한 번에 모든 일을 해치우지 않고(k ()로 직행), 이제는 주어진 작업량을 신경 씁니다.
할 일이 더 없다면(n <= 0) 그때에야 k ()로 진행합니다.
그 외에는 할 수 있는 만큼의 일을 계산하고, 남은 작업과 예산을 그에 맞게 갱신합니다.
| 예산 | 작업 | 수행한 작업 | 남은 작업 | 남은 예산 |
|---|---|---|---|---|
| 7 | 5 | 5 | 0 | 2 |
| 7 | 7 | 7 | 0 | 0 |
| 7 | 9 | 7 | 2 | 0 |
Try it online, 또는 아래 시각화 도구를 사용해 보세요.
Work Type:
Step 1
ex4 = Work 5 Spawn ex4Child Work 5 End ex4Child = Work 999 Work 10 End
| PID | 상태 | 프로그램 |
|---|---|---|
| 0 | 다음에 실행 예정 | Work: 5 |
아직 단계 없음
자식이 스폰된 직후 첫 몇 단계를 살펴봅시다:
| PID 0 (부모) | PID 1 (자식) | 준비 큐 | 동작 |
|---|---|---|---|
| Work 4 | Work 999 | 1,0 | PID 1 실행, 999 -> 992 |
| Work 4 | Work 992 | 0,1 | PID 0 실행, 4 -> 0 -> End |
| End | Work 992 | 1 | PID 1 실행, 992 -> 985 |
| End | Work 985 | 1 | … |
따라서 자식에게 할 일이 아주 많더라도, 스케줄러는 선점하여 한 번에 7씩만 일하게 하고, 부모 프로세스도 자신의 일을 할 기회를 얻게 됩니다.
Send프로세스를 스폰해 놓고 통신을 못 하면 썩 유용하지 않습니다. 메인 스레드에서 계산을 떼어내는 데는 도움이 될 수 있지만, 언젠가 프로세스 간 통신이 필요해질 것입니다.
이제 프로세스들에 메시지를 보내는 기능을 추가해 봅시다. (수신은 나중에 합니다.)
type Program
= -- ...
| Send Pid String K
shouldEnqueue : Proc -> Bool
shouldEnqueue proc =
-- ...
Send _ _ _ -> True
ex5 : Program
ex5 =
Spawn ex5Child <| \childPid ->
Send childPid "Ping" <| \() ->
End
ex5Child : Program
ex5Child =
Work 10 <| \() ->
End
Send 명령을 구현하려면 메일박스(mailbox) 개념을 도입해야 합니다:
type alias Proc =
{ program : Program
-- 추가:
, mailbox : Queue String
}
메시지 전송은 이 메일박스에 메시지를 넣는 방식으로 수행합니다. 우리는 현재 프로세스 자원만이 아니라 스케줄러 전체에 접근할 수 있기 때문에 이렇게 할 수 있습니다:
stepInner pid proc budget sch =
-- ...
Send destinationPid message k ->
sch
|> send destinationPid message
|> continue (k ()) (budget - 1)
send : Pid -> String -> Scheduler -> Scheduler
send destinationPid message sch =
sch
|> updateProc destinationPid (enqueueMessage message)
|> enqueue destinationPid
enqueueMessage : String -> Proc -> Proc
enqueueMessage message proc =
{ proc | mailbox = proc.mailbox |> Queue.enqueue message }
메시지를 보낼 때 해당 프로세스를 큐에 넣는 것도 함께 합니다. 이는 나중에 중요해지는데, 선택적 수신에서 흥미로운 메시지를 찾지 못하면 프로세스가 잠들기(즉, 큐에 들어가지 않기) 때문입니다. 곧 살펴보겠습니다!
Try it online, 또는 아래 시각화 도구를 사용해 보세요.
Budget: Current budget: 1
Step 1
ex5 = childPid = Spawn ex5Child Send childPid "Ping" End ex5Child = Work 10 End
| PID | 상태 | 메일박스 | 프로그램 |
|---|---|---|---|
| 0 | 다음에 실행 예정 | 비어 있음 | Spawn |
아직 단계 없음
자식의 메일박스에 메시지는 들어왔지만, 반응할 수 없습니다. 고쳐봅시다!
Receivetype Program
= -- ...
| Receive String K
이는 실제 BEAM이 지원해야 하는 것에 비해 크게 단순화한 버전입니다. Erlang의 receive 구문은 여러 분기, 분기 내부의 패턴 매칭, 일정 시간 동안 흥미로운 메시지가 없을 때의 타임아웃 등을 지원합니다.
우리는 대신 구조 분해 없이 단일 문자열 메시지만 지원합니다. 해당 문자열이 메일박스에서 발견될 때까지 프로세스는 진행하지 않습니다.
ex6 : Program
ex6 =
Spawn ex6Child <| \childPid ->
Send childPid "Ping" <| \() ->
End
ex6Child : Program
ex6Child =
Receive "Ping" <| \() ->
Work 10 <| \() ->
End
shouldEnqueue 함수에서 흥미로운 최적화를 할 수 있습니다:
shouldEnqueue proc =
-- ...
-- 최적화: 흥미로운 메시지가 없다면 `Receive`를 시도할 필요가 없음
Receive wantedMsg _ ->
Queue.toList proc.mailbox
|> List.any (\msg -> msg == wantedMsg)
즉, 메일박스에 기다리는 메시지가 없다면 stepInner 끝에서 해당 프로세스를 다시 큐에 넣지 않습니다. 새 메시지를 받을 때까지 다시 시도할 이유가 없으므로, 프로세스는 잠들고 나중에 send 함수에서 깨우게 됩니다.
이제 코드가 다소 길어집니다. Receive를 해석할 때, 원하는 메시지를 찾을 때까지 메시지를 훑습니다. 찾으면 메일박스에서 제거하고 continuation을 사용하고, 못 찾으면 메일박스를 유지한 채 잠듭니다.
stepInner pid proc budget sch =
-- ...
continue_ : Proc -> Int -> Scheduler -> Scheduler
continue_ newProc newBudget sch_ =
sch_ |> stepInner pid newProc newBudget
-- ...
Receive wantedMsg k ->
let processMessages : List String -> Queue String -> Scheduler
processMessages unmatchedStartRev restOfMailbox =
case Queue.dequeue restOfMailbox of
-- 더 이상 확인할 메시지 없음
Nothing ->
sch |> stop
Just ( msg, restOfMailboxWithoutThis ) ->
if msg == wantedMsg then
-- 찾았다!
let newMailbox =
Queue.fromList
(List.reverse unmatchedStartRev
++ Queue.toList restOfMailboxWithoutThis)
in
sch |> continue_
(proc
|> setMailbox newMailbox
|> setProgram (k ())
)
(budget - 1)
else
-- 다음 것을 시도
processMessages
(msg :: unmatchedStartRev)
restOfMailboxWithoutThis
in
processMessages [] proc.mailbox
setMailbox : Queue String -> Proc -> Proc
setMailbox newMailbox proc =
{ proc | mailbox = newMailbox }
큐의 중간에서 메시지를 뽑아내야 해서 그리 우아하진 않지만, 앞에서 설명한 동작을 수행합니다.
Try it online, 또는 아래 시각화 도구를 사용해 보세요.
Budget: Current budget: 1
Step 1
ex6 = childPid = Spawn ex6Child Send childPid "Ping" End ex6Child = Receive "Ping" -> Work 10 End
| PID | 상태 | 메일박스 | 프로그램 |
|---|---|---|---|
| 0 | 다음에 실행 예정 | 비어 있음 | Spawn |
아직 단계 없음
보시다시피 상황이 잘 맞아떨어졌습니다. 프로세스 1의 메일박스에는 "Ping"이 들어 있고, 곧 Receive "Ping"을 시도하려고 합니다. 다음 단계에서는 메시지가 사라지고, 프로세스가 Work를 수행 중입니다. 성공!
Crash, Link마지막 퍼즐 조각으로, 슈퍼비전 트리를 가능하게 하는 기능인 링크(link) 를 살펴봅시다.
링크는 양방향입니다. 링크된 프로세스가 종료되면(우리 예제에서는 크래시) 스케줄러가 링크 반대쪽에 시스템 메시지를 보냅니다. 받은 쪽에서는 이 종료 신호에 반응할 수 있습니다. 다른 프로세스를 재스폰할까요? 우리도 크래시할까요? 로그로 남기고 정리를 할까요?
참고: BEAM에는 모니터(monitor) 도 있습니다. 이건 단방향인데, 이 글에서는 다루지 않겠습니다.
type Program
= -- ...
| Crash
| Link Pid K
type alias Proc =
{ -- ...
, links : Set Pid
}
initProc program =
{ -- ...
, links = Set.empty
}
ex7 : Program
ex7 =
Spawn ex7Child <| \childPid ->
Link childPid <| \() ->
Receive ("CRASH: " ++ String.fromInt childPid) <| \() ->
End
ex7Child : Program
ex7Child =
Crash
shouldEnqueue proc =
-- ...
Crash -> True
Link _ _ -> True
왜 Crash -> True일까요? Crash 명령은 종결이지만, 내부에서(시스템 메시지 전송) 해야 할 일이 있습니다. 따라서 아직 실행되지 않았다면 큐에 넣습니다. (그 작업이 끝난 뒤에는 Crash를 End로 바꿉니다.)
stepInner pid proc budget sch =
-- ...
Link linkedPid k ->
sch
|> link pid linkedPid
|> continue (k ()) (budget - 1)
link : Pid -> Pid -> Scheduler -> Scheduler
link pid1 pid2 sch =
sch
|> updateProc pid1 (addLink pid2)
|> updateProc pid2 (addLink pid1)
addLink : Pid -> Proc -> Proc
addLink pid proc =
{ proc | links = proc.links |> Set.insert pid }
그리고 Crash에서 proc.links를 실제로 사용합니다:
stepInner pid proc budget sch =
-- ...
stop_ : Proc -> Scheduler -> Scheduler
stop_ newProc sch_ =
sch_ |> stepInner pid newProc 0
-- ...
Crash ->
sch
|> propagateCrashToLinks pid
|> stop_ (proc |> setProgram End)
propagateCrashToLinks : Pid -> Scheduler -> Scheduler
propagateCrashToLinks pid sch =
case Dict.get pid sch.processes of
Nothing -> sch
Just proc ->
proc.links
|> Set.foldl
(\linkedPid accSch ->
accSch
|> send linkedPid
("CRASH: " ++ String.fromInt pid)
)
sch
실제 인터프리터라면 사용자 메시지와 시스템 메시지를 ADT로 구분했겠지만, 이 장난감 구현에서는 위 정도면 충분합니다.
Try it online, 또는 아래 시각화 도구를 사용해 보세요:
Budget: Current budget: 1
Step 1
ex7 = childPid = Spawn ex7Child Link childPid Receive ("CRASH: " ++ childPid) -> End ex7Child = Crash
| PID | 상태 | 메일박스 | 프로그램 |
|---|---|---|---|
| 0 | 다음에 실행 예정 | 비어 있음 | Spawn |
아직 단계 없음
위 Ellie 링크에서는 동작하지만, 시각화 도구에서는 동작하지 않습니다. 왜일까요? 두 곳의 감축 예산이 다르기 때문입니다. Ellie 데모에선 아무 것도 끼어들지 않고 Spawn과 Link가 연달아 실행되지만, 시각화 도구의 감축 예산은 1이라 부모가 Link하기 전에 자식이 Crash해 버립니다.
이 문제는 두 명령을 원자적 한 동작으로 만드는 방식으로 해결할 수 있는데, BEAM은 이를 spawn_link 함수로 제공합니다. 온라인에서 시도해 보시거나, 위 데모에서 Fix the problem 버튼을 눌러 보세요.
여기까지입니다! 우리는 다음을 구현했습니다:
이러한 원시 기능들이 잘 결합되어, BEAM의 명성을 만들어 냅니다. 저는 이들이 꽤 멋지다고 생각하고, 이 장난감 구현이 여러분께 조금이나마 베일을 걷어냈길 바랍니다!