1. 시작하며
코딩을 취미로 접한 게이들은 "함수"는 뭔지 알아도 "프로시저"는 뭔지 잘 모를 거임. 보통의 경우 두 용어에 대해 차이를 그렇게 크게 두지는 않는데, 정확히 차이는 있음. mcfunction에서 사용되는 function의 경우 "프로시저"에 더 가까운 모습을 보임. 그 이유는 바로 다음과 같음.
종류 | 프로시저 | 함수 |
공통점 | 여러 명령어의 묶음, 작업이 끝나면 호출된 주소의 다음 주소로 돌아가야 함. | |
차이점 | 바깥의 변수를 수정하는 방식으로 동작하기 때문에, 거듭 동작시켰을 때 부작용(side-effect) 이 있음 | 바깥의 변수를 stack-frame에 push해 놓고, 명령어의 순서에 따라 계산 뒤 return값을 반환함 이후 pop. (push : 맨 위에 값을 놓기 pop : 맨 위의 값 제거) |
예를 들어 보겠음. mcfunction으로 작성된 "프로시저"를 보겠음.
project :
add.mcfucntion :
scoreboard players operation #a int += #b int
main.mcfunction :
scoreboard players set #a int 10
scoreboard players set #b int 12
function project:add
function project:add
함수의 경우, 두 번의 function project:add 호출에 대해 같은 값 (10+12)를 내놓아야 하며, #a int 값도 변해서는 아니되나, 다른 값 (첫 번째에서는 22, 두 번째에서는 34)을 내놓을 뿐만 아니라 #a int 또한 function project:add 호출에 따라 값이 바뀌는 것을 볼 수 있음. 이렇게 값이 바뀌는 경우는 예상치 못한 버그를 생성할 뿐더러, 설상가상으로 mcfunction 파일의 이름이 조금이라도 적절하지 못하게 설정 된 경우, "어제 짜 놓은 코드를 내가 못 읽는" 재미난 상황이 벌어져 밥도 못 먹고 꼼짝없이 컴퓨터 앞에서 눈싸움을 해야 할 거임.
2. 약한 함수의 구현
그렇다면 값을 복사하면 함수를 구현할 수 있는 거 아니냐? 라고 생각한 게이들 있을 텐데, 난 그 대답에 100점 만점에 50점을 주고 싶음. "아니 니가 뭔데 짜치게 50점만 주냐"라고 생각한 게이들에게는 "그렇게 짜면 함수의 기능 중 50%만 구현하는 것"이라는 것을 말하고 싶을 뿐이라는 점을 알리고 싶음.
그러나 완전히 함수 100% 자체를 구현하는 것은 최적화 면에서 조금 떨어지는 면도 있을 뿐더러, 매크로를 사용하는 방법 이외에는 없으므로, 일단 반쪽짜리 약한 함수를 구현하는 방법에 대해 말하겠음.
project :
add_a_b.mcfucntion :
scoreboard players operation #a_param int = #a int
scoreboard players operation #b_param int = #b int
scoreboard players operation #a_param int += #b_param int
return run scoreboard players get #a_param int
main.mcfunction :
scoreboard players set #a int 10
scoreboard players set #b int 12
execute store result score #k int run function project:add_a_b
execute store result score #k int run function project:add_a_b
위의 코드의 경우, function project:add_a_b 반복 호출에 따른 #k int의 값이 변하지 않음을 확인할 수 있음.
그렇다면 100% 구현시킨 함수는 어떻게 생겼을까?
3. 강한 함수의 구현
일단 코드 먼저 보겠음.
project :
add_a_b.mcfucntion :
#매크로를 이용한 매개변수 전달
$scoreboard players set #a_param int $(a)
$scoreboard players set #b_param int $(b)
#계산
scoreboard players operation #a_param int += #b_param int
#stack-frame pop
data remove storage minecraft:stackframe stack[-1]
#리턴
return run scoreboard players get #a_param int
main.mcfunction :
#매크로를 이용한 매개변수 전달
data modify storage minecraft:stackframe stack append value {a : 10, b : 12}
execute store result score #k int run function project:add_a_b with storage minecraft:stackframe stack[-1]
data modify storage minecraft:stackframe stack append value {a : 10, b : 12}
execute store result score #k int run function project:add_a_b with storage minecraft:stackframe stack[-1]
위의 코드의 경우, function project:add_a_b 반복 호출에 따른 #k int의 값이 변하지 않음 또한 확인되었음. 하지만 새로운 데이터 구조 minecraft:stackframe stack이 생겨버렸음.
왜 갑자기 스택에다 냅다 매개변수들을 집어넣고 난리냐! 라고 생각할 수도 있겠음. 하지만 "원래 함수는 이렇게 돌아간다."왜냐하면, 재귀 호출도 염두해 두기 때문임. 다음을 보도록 하겠음.
4. 재귀 피보나치 (최악시간복잡도 : O(2^n) -> 낮은 수에서만 실행하셈...)
project :
fibonacci.mcfucntion :
#매크로를 이용한 매개변수 전달
$scoreboard players set #fibbonacci_x int $(x)
execute if score #x int matches 0 run return 0
execute if score #x int matches 1 run return 1
scoreboard players remove #fibbonacci_x int 1
data modify storage minecraft:stackframe stack append value {x : 0}
execute store result storage minecraft:stackframe stack[-1].x run scoreboard players get #fibbonacci_x int
execute store result score #tmp0 int run function project:fibonacci with storage minecraft:stackframe stack[-1]
scoreboard players remove #fibbonacci_x int 1
data modify storage minecraft:stackframe stack append value {x : 0}
execute store result storage minecraft:stackframe stack[-1].x run scoreboard players get #fibbonacci_x int
execute store result score #tmp1 int run function project:fibonacci with storage minecraft:stackframe stack[-1]
#pop
data remove storage minecraft:stackframe stack[-1]
return run scoreboard players operation #tmp1 int += #tmp2 int
main.mcfunction :
#매크로를 이용한 매개변수 전달 : json 자료형 만든 뒤, scoreboard 기준으로 수정
scoreboard players set #x int 4
data modify storage minecraft:stackframe stack append value {x : 0}
execute store result storage minecraft:stackframe stack[-1].x run scoreboard players get #x int
execute store result score #k int run function project:fibonacci with storage minecraft:stackframe stack[-1]
위의 경우에는 stackframe stack에 재귀 호출에 따라 변화된 매개 변수가 계속 push되고 pop된다. 이런 느낌으로...
(아래는 예상되는 stackframe stack의 값 양상을 보인 것임.)
[{x : 4}]
[{x : 4}, {x : 3}]
[{x : 4}, {x : 3}, {x : 2}]
[{x : 4}, {x : 3}, {x : 2}, {x : 1}]
[{x : 4}, {x : 3}, {x : 2}, {x : 0}]
[{x : 4}, {x : 3}, {x : 1}]
[{x : 4}, {x : 2}, {x : 1}]
[{x : 4}, {x : 2}, {x : 0}]
[{x : 4}]
[]
따라서 k의 값은 fibb(4)의 값인 3이 될 것임.
4. 결론
재귀 호출을 사용하는 알고리즘을 사용해야 하는 경우 -> 완전한 함수를 쓴다.
재귀 호출이 필요 없는 경우 -> 반쪽짜리 함수를 쓴다
프로시나를 사용해야 하는 경우 -> 이름을 잘 짓자. (주석을 아주 잘 달자)
https://s.click.aliexpress.com/e/_DBR5yUp 알리 천원마트인데 어차피 똑같이 중국에서 물건 떼오는건데 굳이 비싸게 살 필요 없겠더라ㅋㅋ
fibonacci.mcfunction 2~3번째 명령에서 감지하는 점수가 #x가 아니라 #fibbonacci_x가 되어야 하지 않을까? 재귀호출을 하면서 점수를 깎는 게 #fibbonacci_x라서 #x 점수는 안 바뀌기 때문에 저대로 두면 무한 호출이 나올 것으로 예상이 되는데 (다만 마인크래프트 명령어 블록 길이 제한상 65536번째 명령어 실행에서 그칠 것) 그리고 #pop주석 밑밑 명령어에서는 #tmp1 int += #tmp0 int가 되어야 할 것 같노 (#tmp2는 없음)
깔끔한 지적 고맙다. 사실 디버깅도 안 하고 즉흥적으로 써내려 간 거라 오류가 많이 보이노... ㅠㅠ