<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://eatchangmyeong.github.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://eatchangmyeong.github.io/" rel="alternate" type="text/html" /><updated>2026-03-21T04:05:48+09:00</updated><id>https://eatchangmyeong.github.io/feed.xml</id><title type="html">잇창명 개발 블로그</title><subtitle>잇창명 개발 블로그</subtitle><entry><title type="html">Project::shear 어떻게 만들었는가</title><link href="https://eatchangmyeong.github.io/2025/01/05/how-i-made-project-shear.html" rel="alternate" type="text/html" title="Project::shear 어떻게 만들었는가" /><published>2025-01-05T00:00:00+09:00</published><updated>2025-01-05T00:00:00+09:00</updated><id>https://eatchangmyeong.github.io/2025/01/05/how-i-made-project-shear</id><content type="html" xml:base="https://eatchangmyeong.github.io/2025/01/05/how-i-made-project-shear.html"><![CDATA[<p><em>2025년 1월 6일 수정: 글을 급하게 작성하다 보니 잘못된 부분을 몇 군데 발견했습니다. 꼼꼼히 검토하지 못한 점에 대해 사과드립니다.</em></p>

<blockquote>
  <ul>
    <li><a href="/2021/04/29/how-i-made-honeyhouse.html">셰이더를 쓸 줄 모른다. 물결 모양으로 사라지는 효과 하나 넣자고 서피스와 블렌드 모드를 남용했다.</a></li>
  </ul>
</blockquote>

<p><strong>셰이더와 3D 렌더링을 배우면서 그래도 살만해졌습니다.</strong> 사실 셰이더 없이 개발하면서는 서피스와 블렌드 모드만으로 할 수 있는 게 많이 없어서 <code class="language-plaintext highlighter-rouge">셰이더 언제 배우냐</code>를 연발했는데 버텍스와 프래그먼트(지오메트리는 아직도 모릅니다 🥲) 셰이더를 쓸 수 있게 되면서 그나마 제가 원하는 그래픽을 만들 수 있게 되었네요.</p>

<p>Honeyhouse 회고의 양식을 빌려 첫 문단을 작성했는데, Honeyhouse를 개발하고 난 뒤 2학기에 컴퓨터그래픽스를 수강하면서 OpenGL과 3D 렌더링을 배울 수 있게 되었습니다. 그 뒤로 GameMaker로 처음 개발한 3D 게임이 프로토 게임잼에 출품한 <a href="https://protogamejam.itch.io/degool-push">데굴푸쉬</a>고, 이번에 공개한 <a href="https://cafe.naver.com/crazygm/231904">Project::shear</a>는 두 번째입니다. Honeyhouse 회고는 날짜별로 했던 작업을 정리했는데, 이번 회고는 기술적인 난관이 많았던 만큼 구현에 필요했던 세부사항 위주로 정리해 보려고 합니다. 되도록이면 엔딩까지 보고 나서 읽어주셨으면 좋겠습니다.</p>

<blockquote>
  <p>(많이 찍어놔서 다행이네요! 그럴 시간에 깃 커밋이나 해)</p>
</blockquote>

<p>참고로 이번에는 깃 커밋을 했습니다.</p>

<h1 id="시점-변환-시스템">시점 변환 시스템</h1>

<p>카페 글에서 컴퓨터게임설계 팀프로젝트의 일환으로 개발한 게임이라고 썼는데, 게임의 소재 역시 같은 강의의 2주차 수업 자료에서 착안했습니다. 당시에 (미래의) 팀원들에게 보여주려고 급하게 만들었던 프로토타입이 아직 컴퓨터에 남아 있습니다.</p>

<p><img src="/assets/post-images/project-shear-prototype.png" alt="Project::shear의 프로토타입 화면. 검은 배경 위에 여러 개의 정사각형 블록이 카메라와 각각 거리를 두고 배치되어 있다." /></p>

<p>사실은 이번에도 위에서 언급한 저 한 소재에 대해서 엄청나게 긴 해설을 쓰려고 합니다.</p>

<h2 id="선형대수학을-또-하게-된-사연">선형대수학을 또 하게 된 사연</h2>

<p>Honeyhouse 회고도 <a href="/2021/04/29/how-i-made-honeyhouse.html#무턱대고-행렬부터-만들기">무턱대고 선형대수학 얘기부터 시작했는데</a> 이번에도 무턱대고 선형대수학 얘기부터 시작하게 되네요.</p>

<p>3개의 축(위의 스크린샷에서는 우하단의 빨간색, 초록색, 파란색 화살표)으로 시점을 결정하는 현재의 시스템 역시 이때부터 기획해 두었습니다. 게임 내에서 따로 설명하지는 않았지만, 이 세 축은 아래와 같습니다.</p>

<ul>
  <li>카메라의 <code class="language-plaintext highlighter-rouge">x</code>편차와 <code class="language-plaintext highlighter-rouge">y</code>편차
    <ul>
      <li>이 값이 0이 아닐 경우 카메라가 맵을 수직으로 보는 것이 아니라 편차값만큼 치우친 곳에서 비스듬하게 맵을 봅니다.</li>
      <li>렌더링은 3D이지만 게임플레이는 사실상 2D이기 때문에 (GameMaker의 기본 2D 좌표계를 따라) <code class="language-plaintext highlighter-rouge">x</code>/<code class="language-plaintext highlighter-rouge">z</code> 대신 <code class="language-plaintext highlighter-rouge">x</code>/<code class="language-plaintext highlighter-rouge">y</code>를 사용했습니다.</li>
      <li>블록의 정면 방향과 맵이 사영되는 위치를 유지하면서 카메라의 좌표를 바꾸기 위해 보통 3D 렌더링에서 쓸 일이 거의 없는 <a href="https://ko.wikipedia.org/wiki/전단변환행렬">전단변환</a>을 사용했습니다. 게임 제목인 Project::<strong>shear</strong> 역시 여기서 유래했습니다.</li>
    </ul>
  </li>
  <li>카메라의 원근감 <code class="language-plaintext highlighter-rouge">persp</code>
    <ul>
      <li><code class="language-plaintext highlighter-rouge">persp</code> = 0은 원근감이 아예 없는 경우, 즉 거리와 무관하게 같은 크기로 보이는 경우에 해당하며, 클수록 거리에 따른 크기 차이가 커집니다.</li>
      <li><a href="https://en.wikipedia.org/wiki/Dolly_zoom">돌리 줌</a>과 같은 원리로 구현되어 있습니다. 더 자세하게는 카메라와 피사체의 거리가 \(\frac{1}{\mathrm{persp}}\)이 되도록 설정되어 있습니다.<sup id="fnref:fn-dolly-zoom-coefficient" role="doc-noteref"><a href="#fn:fn-dolly-zoom-coefficient" class="footnote" rel="footnote">1</a></sup></li>
    </ul>
  </li>
</ul>

<p>한편, 3D를 지원하는 그래픽스 파이프라인이나 게임 엔진에서는 보통 아래와 같이 두 종류의 사영 행렬을 제공합니다.</p>

<ul>
  <li><strong>정사영행렬</strong>(orthographic projection matrix): 원근을 고려하지 않고 멀리 있는 것과 가까이 있는 것을 같은 크기로 투영합니다.
    <ul>
      <li>고등학교 교육과정에서 배우는 정사영을 번역어로 사용했습니다.</li>
    </ul>
  </li>
  <li><strong>투시사영행렬</strong>(perspective projection matrix): 가까이 있는 것은 상대적으로 크게, 멀리 있는 것은 상대적으로 작게 투영합니다.
    <ul>
      <li>one/two/three-point perspective가 주로 '1/2/3점 투시'로 번역되는 것을 참고해 번역어를 정했습니다.</li>
    </ul>
  </li>
</ul>

<p>GameMaker에서는 아래와 같이 사영 행렬을 반환하는 3종류의 함수를 제공하고 있습니다.</p>

<ul>
  <li><a href="https://manual.gamemaker.io/monthly/en/#t=GameMaker_Language%2FGML_Reference%2FMaths_And_Numbers%2FMatrix_Functions%2Fmatrix_build_projection_ortho.htm"><code class="language-plaintext highlighter-rouge">matrix_build_projection_ortho(width, height, znear, zfar)</code></a>: 정사영행렬에 해당합니다. 카메라를 기준으로 가로 <code class="language-plaintext highlighter-rouge">width</code>, 세로 <code class="language-plaintext highlighter-rouge">height</code>의 직사각기둥에서 카메라로부터의 거리가 <code class="language-plaintext highlighter-rouge">znear</code>부터 <code class="language-plaintext highlighter-rouge">zfar</code>까지인 직육면체를 잘라 사영합니다.</li>
  <li><a href="https://manual.gamemaker.io/monthly/en/#t=GameMaker_Language%2FGML_Reference%2FMaths_And_Numbers%2FMatrix_Functions%2Fmatrix_build_projection_perspective.htm"><code class="language-plaintext highlighter-rouge">matrix_build_projection_perspective(width, height, znear, zfar)</code></a>: 투시사영행렬에 해당합니다. 카메라의 시야를 이루는 (<code class="language-plaintext highlighter-rouge">znear</code> 기준) 가로 <code class="language-plaintext highlighter-rouge">width</code>, 세로 <code class="language-plaintext highlighter-rouge">height</code>인 직사각뿔에서 카메라로부터의 거리가 <code class="language-plaintext highlighter-rouge">znear</code>부터 <code class="language-plaintext highlighter-rouge">zfar</code>까지인 절두체<sup id="fnref:fn-frustum" role="doc-noteref"><a href="#fn:fn-frustum" class="footnote" rel="footnote">2</a></sup>를 잘라 사영합니다.</li>
  <li><a href="https://manual.gamemaker.io/monthly/en/#t=GameMaker_Language%2FGML_Reference%2FMaths_And_Numbers%2FMatrix_Functions%2Fmatrix_build_projection_perspective_fov.htm"><code class="language-plaintext highlighter-rouge">matrix_build_projection_perspective_fov(fov_y, aspect, znear, zfar)</code></a>: <code class="language-plaintext highlighter-rouge">matrix_build_projection_perspective</code>와 같지만 <code class="language-plaintext highlighter-rouge">fov_y</code>(세로 방향의 시야각)와 <code class="language-plaintext highlighter-rouge">aspect</code>(종횡비)로부터 <code class="language-plaintext highlighter-rouge">width</code>와 <code class="language-plaintext highlighter-rouge">height</code>를 역산합니다.</li>
</ul>

<p>특히 <code class="language-plaintext highlighter-rouge">matrix_build_projection_ortho</code>는 <code class="language-plaintext highlighter-rouge">matrix_build_projection_perspective</code>에서 <code class="language-plaintext highlighter-rouge">persp</code>을 0으로 보낸 특수한 경우로 볼 수 있습니다.</p>

<p>위에서 제공하는 함수를 사용해 맵을 렌더링하려면 0으로 나누기를 방지하기 위해 <code class="language-plaintext highlighter-rouge">persp</code>의 값을 확인해 분기해야 합니다.</p>

<div class="language-javascript gml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 설명의 편의를 위해...</span>
<span class="c1">// * `x`, `y`, `persp`이 지역 변수로 정의되어 있다고 가정합니다.</span>
<span class="c1">// * 맵의 중심이 (0, 0, 0)이고 가로가 `room_width`, 세로가 `room_height`인 것으로 가정합니다.</span>

<span class="c1">// 실제 게임에서와 같이 오른손 좌표계를 사용합니다.</span>

<span class="c1">// 부동소숫점 오차를 방지하기 위해 매우 작은 값 미만인지 테스트하는 방식을 사용했습니다.</span>
<span class="k">if</span><span class="p">(</span><span class="nx">persp</span> <span class="o">&lt;</span> <span class="mf">0.000001</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// 정사영</span>
	<span class="c1">// 뷰 행렬</span>
	<span class="nx">matrix_set</span><span class="p">(</span>
		<span class="nx">matrix_view</span><span class="p">,</span>
		<span class="c1">// 카메라의 배치에서 뷰 행렬을 역산하는 함수</span>
		<span class="nx">matrix_build_lookat</span><span class="p">(</span>
			<span class="nx">x</span><span class="p">,</span>  <span class="nx">y</span><span class="p">,</span> <span class="o">-</span><span class="mi">9</span><span class="p">,</span> <span class="c1">// 카메라의 좌표</span>
			<span class="mi">0</span><span class="p">,</span>  <span class="mi">0</span><span class="p">,</span>  <span class="mi">0</span><span class="p">,</span> <span class="c1">// 카메라가 바라보는 좌표</span>
			<span class="mi">0</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span>  <span class="mi">0</span>  <span class="c1">// 위 방향을 나타내는 벡터</span>
		<span class="p">)</span>
	<span class="p">);</span>
	<span class="c1">// 사영 행렬</span>
	<span class="nx">matrix_set</span><span class="p">(</span>
		<span class="nx">matrix_projection</span><span class="p">,</span>
		<span class="nx">matrix_build_projection_ortho</span><span class="p">(</span>
			<span class="nx">room_width</span><span class="p">,</span> <span class="nx">room_height</span><span class="p">,</span>
			<span class="mi">9</span> <span class="o">-</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">9</span> <span class="o">+</span> <span class="mi">8</span> <span class="c1">// 피사체 앞뒤 8단위만큼을 사영</span>
		<span class="p">)</span>
	<span class="p">);</span>
<span class="p">}</span> <span class="k">else</span> <span class="p">{</span> <span class="c1">// 투시사영</span>
	<span class="nx">matrix_set</span><span class="p">(</span>
		<span class="nx">matrix_view</span><span class="p">,</span>
		<span class="nx">matrix_build_lookat</span><span class="p">(</span>
			<span class="nx">x</span><span class="p">,</span>  <span class="nx">y</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="o">/</span><span class="nx">persp</span><span class="p">,</span> <span class="c1">// 0으로 나누기!!!!!</span>
			<span class="mi">0</span><span class="p">,</span>  <span class="mi">0</span><span class="p">,</span>  <span class="mi">0</span>      <span class="p">,</span>
			<span class="mi">0</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span>  <span class="mi">0</span>
		<span class="p">)</span>
	<span class="p">);</span>
	<span class="nx">matrix_set</span><span class="p">(</span>
		<span class="nx">matrix_projection</span><span class="p">,</span>
		<span class="nx">matrix_build_projection_perspective</span><span class="p">(</span>
			<span class="nx">room_width</span><span class="p">,</span> <span class="nx">room_height</span><span class="p">,</span>
			<span class="mi">1</span><span class="o">/</span><span class="nx">persp</span> <span class="o">-</span> <span class="mi">8</span><span class="p">,</span> <span class="mi">1</span><span class="o">/</span><span class="nx">persp</span> <span class="o">+</span> <span class="mi">8</span>
		<span class="p">)</span>
	<span class="p">)</span>
<span class="p">}</span>
</code></pre></div></div>

<p>이 정도의 코드로 만족할 수 있었겠지만, 겉보기에는 아무런 불연속점 없이 자연스럽게 이어지는 시점 변환을 나타내는 데 0으로 나누기가 생기고 그걸 우회하기 위해 분기를 사용해야 한다는 것이 내심 마음에 들지 않았습니다. 과거의 저를 조금 더 변호하자면, 이때의 사전 작업이 결국 나중의 개발에 도움이 되었고 그래픽스 파이프라인의 이해도를 더 높였다고 생각합니다.</p>

<ul>
  <li>렌더링에 사용하는 위의 행렬을 충돌 판정에도 똑같이 사용하기 때문에 분기로 때우는 위의 방식이 언제 문제로 이어질지 알 수 없었습니다.</li>
  <li>나중에 실제로 충돌 판정을 구현할 때 부동소숫점 오차에 온 신경을 쏟았기 때문에 조금이라도 더 안정적인 수학적 모델을 구축하는 것이 중요했습니다.</li>
  <li>심리적인 안정뿐만 아니라 실제 작업도 간단해지는 효과를 얻었습니다. 예를 들어, 조건부 충돌 판정을 작업할 때 행렬 모양에 따라 분기할 필요 없이 짧은 수식 하나로 넘길 수 있었습니다.</li>
</ul>

<p>불행하게도 제가 원하는 것을 구현하려면 엔진에서 제공하는 안락한 <code class="language-plaintext highlighter-rouge">matrix_build_projection_*</code>에서 벗어나 사영 행렬을 직접 구해야 합니다.</p>

<h2 id="렌더링의-원리">렌더링의 원리</h2>

<p>사영 행렬을 구하기 전에 우선 3D 월드가 렌더링되는 원리를 알아야 합니다. 이 게임에서 사용하는 OpenGL (ES) 파이프라인에서는 다음과 같은 원리로 그래픽을 렌더링합니다.</p>

<ol>
  <li>게임 프로그래머가 <strong>모델 공간</strong>에서 3D 모델을 설계하고 정점 버퍼의 형태로 엔진에 입력한다.</li>
  <li>게임이 모델을 렌더링할 때가 되면 게임 코드는 원하는 셰이더에 정점 버퍼를 전달한다.</li>
  <li>셰이더는 입력된 <strong>모델 공간</strong>의 정점 버퍼를 <strong>버텍스 셰이더</strong>에 통과시켜서 <strong>클립 공간</strong>상의 정점을 얻는다.
    <ul>
      <li>이 과정에는 위의 <code class="language-plaintext highlighter-rouge">matrix_set</code>에서 설정한 <code class="language-plaintext highlighter-rouge">matrix_*</code> 행렬을 적용하는 과정이 포함됩니다.</li>
    </ul>
  </li>
  <li>위에서 얻은 <strong>클립 공간</strong>의 정점 중 특정 영역 안에 들어온 것만 취한 뒤 화면상에 출력될 좌표 데이터로 변환한다.
    <ul>
      <li>클립 좌표 중 \(x\)와 \(y\)는 그대로 화면상의 좌표로 사용하고, \(z\)는 Z 버퍼링에 사용합니다. 일반적인 3D 렌더링에서는 Z 버퍼를 사용해 어떤 물체가 카메라에 더 가까운지, 즉 새로운 픽셀을 기존 픽셀 위에 그려야 하는지 아니면 버려야 하는지 판정합니다.</li>
    </ul>
  </li>
  <li>위에서 얻은 픽셀 데이터를 <strong>프래그먼트 셰이더</strong>에 통과시켜서 픽셀의 색상을 얻는다.</li>
  <li>위에서 얻은 색상을 화면에 그린다.</li>
</ol>

<p>즉, 엔진에서 제공하는 <code class="language-plaintext highlighter-rouge">matrix_build_projection_*</code> 함수를 포함해 <code class="language-plaintext highlighter-rouge">matrix_set</code>에 전달되는 행렬의 역할은 버텍스 셰이더에 전달되어 월드 공간의 좌표를 클립 공간의 좌표로 변환하는 것입니다. 이 중...</p>
<ul>
  <li>월드 행렬은 <strong>모델 공간</strong>을 <strong>월드 공간</strong>으로 변환하는 단계, 즉 모델로 표현된 소품을 월드에 배치하는 단계에 해당합니다.
    <ul>
      <li>이 글의 초점은 월드 공간을 클립 공간으로 변환하는 것이므로 모델 공간과 월드 행렬은 따로 다루지 않겠습니다.</li>
    </ul>
  </li>
  <li>뷰 행렬은 <strong>월드 공간</strong>을 <strong>뷰 공간</strong>['카메라'가 원점에 있고 (GameMaker 기준) 양의 \(z\)축 방향을 바라보는 좌표계]으로 변환하는 단계, 즉 임의로 정해져 있는 월드 좌표계를 '카메라'의 관점으로 변환하는 단계에 해당합니다.</li>
  <li>사영 행렬은 <strong>뷰 공간</strong>을 <strong>클립 공간</strong>으로 변환하는 단계, 즉 카메라가 비추는 실제 장면을 '필름'에 사영하는 단계에 해당합니다.</li>
</ul>

<p>그런데 사실 '카메라'는 렌더링 파이프라인의 이해를 돕기 위한 비유일 뿐 실제로 월드 내에 물리적인 카메라를 만들어서 렌더링을 하는 것은 아니기 때문에 실질적으로 제가 손댈 수 있는 것은 <strong>뷰 행렬</strong>과 <strong>사영 행렬</strong> 두 가지입니다. 어차피 월드-뷰-사영 행렬을 모두 곱해서 결과가 같으면 렌더링한 결과는 같으니까요.</p>

<h2 id="gamemaker와-opengl의-좌표계">GameMaker와 OpenGL의 좌표계</h2>

<p>3D 렌더링의 원리를 이해했으니 이제 변환의 대상과 결과물인 월드 공간과 클립 공간을 알아야 합니다. 우선 위에서 잠깐 다뤘듯이 GameMaker에서 사용하는 2D 좌표계는 다음과 같습니다.</p>

<ul>
  <li>룸의 왼쪽 위가 원점</li>
  <li>\(x\)축은 오른쪽</li>
  <li>\(y\)축은 아래쪽</li>
  <li>내장 인스턴스 변수 <code class="language-plaintext highlighter-rouge">depth</code>를 사용해 인스턴스가 그려지는 순서를 조절할 수 있으며, 클수록 '깊은' 곳에 그려지고 <code class="language-plaintext highlighter-rouge">depth</code>가 작은 인스턴스에 묻힙니다. 이 변수가 실질적인 '제3의 축' 역할을 합니다.</li>
</ul>

<p>Project::shear에서도 이 설정을 존중해 다음과 같이 월드 좌표계를 구성했습니다.</p>

<ul>
  <li>맵의 왼쪽 위가 원점</li>
  <li>\(x\)축은 오른쪽</li>
  <li>\(y\)축은 아래쪽</li>
  <li>\(z\)축은 카메라에서 먼 쪽</li>
  <li>피사체는 \(z\) = 0에 위치</li>
</ul>

<p>마지막으로, 클립 공간에서 렌더링에 사용하는 범위는 \(-1 \le x \le 1\), \(-1 \le y \le 1\), \(0 \le z \le 1\)이며, 카메라에 가까울수록 \(z\)가 작습니다.<sup id="fnref:fn-opengl-es-platform" role="doc-noteref"><a href="#fn:fn-opengl-es-platform" class="footnote" rel="footnote">3</a></sup> 이제 알아야 하는 좌표계는 모두 구했습니다.</p>

<h2 id="3차원-세계-4차원-행렬">3차원 세계, 4차원 행렬</h2>

<p>하지만 본격적으로 사영 행렬을 구하기 전에 알아야 할 것이 또 하나 남았습니다(진짜 마지막입니다. 약속드립니다). 3차원 월드의 렌더링을 다루는 데는 3차원 행렬이 아니라 <strong>4차원</strong> 행렬이 필요합니다.</p>

<p>아주 기본적인 3차원 좌표계의 변환은 3차원 벡터와 3차원(3×3) 행렬만 있으면 되는 게 맞습니다. 다만 3차원 행렬은 <strong>원점에 변환을 가할 수 없다</strong>는 치명적인 한계가 있습니다. 아래 행렬의 \(a\)부터 \(i\)까지에 무엇을 대입하든 원점의 변환 결과는 그대로 원점입니다.</p>

\[\begin{pmatrix}
	a &amp; b &amp; c \\
	d &amp; e &amp; f \\
	g &amp; h &amp; i
\end{pmatrix}
\begin{pmatrix}
	0 \\
	0 \\
	0
\end{pmatrix}
=
\begin{pmatrix}
	0a + 0b + 0c \\
	0d + 0e + 0f \\
	0g + 0h + 0i
\end{pmatrix}
=
\begin{pmatrix}
	0 \\
	0 \\
	0
\end{pmatrix}\]

<p>원점에 변환을 가하려면 행렬로써 \(f(x, y, z) = ax + by + cz + \boldsymbol{d}\)를 표현할 수 있어야 하는데, 그 대신 \(f'(x, y, z, \boldsymbol{w}) = ax + by + cz + \boldsymbol{dw}\)를 표현하는 4차원(4×4) 행렬을 만들고 \(w\)를 항상 1인 것으로 취급하는(!) 수학적 '꼼수'를 사용하면 됩니다.</p>

\[\begin{pmatrix}
	a &amp; b &amp; c &amp; j \\
	d &amp; e &amp; f &amp; k \\
	g &amp; h &amp; i &amp; l \\
	0 &amp; 0 &amp; 0 &amp; 1
\end{pmatrix}
\begin{pmatrix}
	0 \\
	0 \\
	0 \\
	1
\end{pmatrix}
=
\begin{pmatrix}
	0a + 0b + 0c + 1j \\
	0d + 0e + 0f + 1k \\
	0g + 0h + 0i + 1l \\
	0 \cdot 0 + 0 \cdot 0 + 0 \cdot 0 + 1 \cdot 1
\end{pmatrix}
=
\begin{pmatrix}
	j \\
	k \\
	l \\
	1
\end{pmatrix}\]

<p><em>그래도</em> 아직 한 가지 문제가 더 남아 있습니다. 절두체를 직육면체로 변환하려면 나눗셈이 필요한데(절두체의 단면 중 너비가 \(a\)인 것을 너비가 1인 단면으로 바꾸려면 좌표에 \(\frac{1}{a}\)을 곱해야 합니다), 행렬로는 <strong>나눗셈을 표현할 수 없습니다.</strong></p>

<p>선형대수학으로 3D 렌더링까지 할 수 있으려면 한 가지 꼼수가 더 필요합니다. 아까 벡터에 억지로 집어넣은 \(w\) = 1이 보이시나요? \(w\)가 1이 아니면 어떻게 될까요? <strong>그냥 <em>나눠서</em> 1로 만들면 되는 거 아님??</strong></p>

<p>변환한 벡터의 \(w\)좌표가 절두체 단면의 너비(의 상수배)가 되도록 사영 행렬을 구성하면 OpenGL이 <strong>투시 나눗셈</strong>(perspective division)이라는 과정을 거쳐 절두체를 깔끔한 직육면체로 만들어 줍니다. 다행히 절두체 단면의 너비는 선형적으로 증가하므로 행렬로 표현할 수 있습니다. 특히 절두체의 모서리를 연장하면 카메라가 있는 평면에서 단면의 너비가 0이 되므로 카메라의 좌표를 버텍스 셰이더에 넣으면 변환된 \(w\)좌표는 0이 됩니다. (나중에 이 성질을 활용할 예정입니다.)</p>

<p>참고로 여기서 '꼼수'라고 표현한 테크닉을 수학계에서는 <a href="https://ko.wikipedia.org/wiki/동차좌표">동차좌표</a>라고 합니다.</p>

<h2 id="사영행렬-구성하기">사영행렬 구성하기</h2>

<p>위에서 언급한 3개의 시점 축 \(\Delta x\), \(\Delta y\), \(p\)(<code class="language-plaintext highlighter-rouge">ersp</code>)와 맵의 크기 \(w\)(idth), \(h\)(eight), 절두체 단면의 거리 \(n\)(ear), \(f\)(ar)까지 총 7개의 변수를 가지고 월드 좌표계를 클립 좌표계로 변환하는 행렬을 구성해 봅시다. 단...</p>

<ul>
  <li>\(\Delta x\)와 \(\Delta y\)는 플레이어 방향으로 돌출된(즉, <strong>음의</strong> \(z\)좌표를 가지는) 물체가 오른쪽 아래로 튀어나와 보이는 방향이 양인 것으로 정의합니다.</li>
  <li>\(n\)과 \(f\)는 카메라에서 단면까지의 거리가 아니라 <strong>피사체에서</strong> 단면까지의 <strong>부호 있는</strong> 거리이며, 카메라에서 먼 쪽이 양입니다.
    <ul>
      <li>기존의 '카메라 지향' 사영 행렬을 사용하면 카메라의 좌표에서 0으로 나누기가 발생하기 때문에 시점이 바뀌어도 같은 곳에 있는 '피사체 지향' 방법을 사용합니다.</li>
    </ul>
  </li>
</ul>

\[\boldsymbol{M}\]

<p>한 번에 행렬을 구성하기는 부담스러우니 작은 단계로 쪼개봅시다.</p>

<ol>
  <li>맵은 \(0 \le x \le w\), \(0 \le y \le h\)의 공간을 차지하므로 원점을 옮겨 중심을 맞춘다.</li>
  <li>\(\Delta x\)와 \(\Delta y\)를 사용해 전단변환을 수행한다.</li>
  <li>변환된 월드를 클립 공간에 사영한다.</li>
</ol>

\[\boldsymbol{M} = \boldsymbol{P}\boldsymbol{S}\boldsymbol{O}\]

<p>(참고로 행렬 변환은 오른쪽에서 왼쪽으로 이루어집니다.)</p>

<p>원점을 옮기는 행렬 \(\boldsymbol{O}\)(rigin)와 전단변환을 수행하는 행렬 \(\boldsymbol{S}\)(hear)는 쉽게 구성할 수 있습니다.</p>

\[\boldsymbol{O}
=
\begin{pmatrix}
	1 &amp; 0 &amp; 0 &amp; -\frac{w}{2} \\
	0 &amp; 1 &amp; 0 &amp; -\frac{h}{2} \\
	0 &amp; 0 &amp; 1 &amp; 0 \\
	0 &amp; 0 &amp; 0 &amp; 1
\end{pmatrix}\]

\[\boldsymbol{S}
=
\begin{pmatrix}
	1 &amp; 0 &amp; -\Delta x &amp; 0 \\
	0 &amp; 1 &amp; -\Delta y &amp; 0 \\
	0 &amp; 0 &amp; 1 &amp; 0 \\
	0 &amp; 0 &amp; 0 &amp; 1
\end{pmatrix}\]

<p>\(\Delta x\)와 \(\Delta y\)에 음의 부호가 있음에 유의해 주세요.</p>

<p>\(\boldsymbol{P}\)(rojection) 역시 더 작은 단계로 쪼개봅시다.</p>

<ol>
  <li>월드 좌표계를 뷰 좌표계로 변환한다. <strong>단, 카메라가 아닌 피사체가 원점에 온다.</strong></li>
  <li>변환된 뷰를 클립 공간에 사영한다.</li>
</ol>

\[\boldsymbol{P} = \boldsymbol{C}\boldsymbol{V}\]

<p>사실 위에서 맵의 중심을 이미 맞췄고 피사체는 <code class="language-plaintext highlighter-rouge">z</code> = 0에 있는 것으로 정의했기 때문에 \(\boldsymbol{V}\)(iew)는 그냥 단위행렬이 됩니다.</p>

\[\boldsymbol{V} = \boldsymbol{I}
=
\begin{pmatrix}
	1 &amp; 0 &amp; 0 &amp; 0 \\
	0 &amp; 1 &amp; 0 &amp; 0 \\
	0 &amp; 0 &amp; 1 &amp; 0 \\
	0 &amp; 0 &amp; 0 &amp; 1
\end{pmatrix}\]

<p>\(\boldsymbol{C}\)(lip)가 제일 구성하기 어려운 행렬입니다.</p>

\[\boldsymbol{C}
=
\begin{pmatrix}
	C_{11} &amp; C_{12} &amp; C_{13} &amp; C_{14} \\
	C_{21} &amp; C_{22} &amp; C_{23} &amp; C_{24} \\
	C_{31} &amp; C_{32} &amp; C_{33} &amp; C_{34} \\
	C_{41} &amp; C_{42} &amp; C_{43} &amp; C_{44}
\end{pmatrix}\]

<p>그런데 생각해보면 뷰 좌표를 클립 좌표로 변환할 때는 \(x\), \(y\), \(z\) 사이에 뚜렷한 상호작용이 없는 것을 알 수 있습니다. \(z\)가 변함에 따라 클립 좌표의 \(x\)와 \(y\)가 바뀌긴 하지만 그건 투시 나눗셈으로 처리해야 하니 \(z\)가 아니라 \(w\)에 의존하는 것으로 생각할 수 있습니다.</p>

<ul>
  <li>\(x\)와 \(y\)좌표는 각각 독립적으로 클립 좌표계에 맞추어집니다.</li>
  <li>\(z\)좌표는 투시 나눗셈에 간접적으로 활용되면서 동시에 near/far 평면을 \(\frac{z}{w}\) = 0과 \(\frac{z}{w}\) = 1에 맞추는 역할을 합니다.</li>
</ul>

<p>이 사실을 바탕으로 \(\boldsymbol{C}\)에서 \(x\)와 \(y\)와 \(z\)/\(w\)가 상호작용하는 항을 없애면 이렇게 됩니다. 미지수가 16개에서 6개로 줄었네요!</p>

\[\boldsymbol{C}
=
\begin{pmatrix}
	C_{11} &amp; 0 &amp; 0 &amp; 0 \\
	0 &amp; C_{22} &amp; 0 &amp; 0 \\
	0 &amp; 0 &amp; C_{33} &amp; C_{34} \\
	0 &amp; 0 &amp; C_{43} &amp; C_{44}
\end{pmatrix}\]

<p>우선 \(\boldsymbol{C}\)의 우하단을 구해 봅시다. 구하고자 하는 행렬로 (?, ?, \(z\))를 변환하면 다음 성질이 성립해야 합니다.</p>

<ul>
  <li><strong>near 평면의 변환</strong>: 변환 전에 \(z\) = \(n\)이면 변환된 \(\frac{z}{w}\) = 0, 즉 \(z\) = 0</li>
  <li><strong>far 평면의 변환</strong>: 변환 전에 \(z\) = \(f\)이면 변환된 \(\frac{z}{w}\) = 1, 즉 \(z\) = \(w\)</li>
  <li><strong>카메라의 좌표 성질</strong>: 변환 전에 \(z\) = \(-\frac{1}{p}\)이면 변환된 \(w\) = 0</li>
</ul>

<p>위의 세 성질을 행렬 변환으로 나타내면 다음과 같습니다. 편의상 우하단 2×2 부분만 작성합니다.</p>

\[\begin{pmatrix}
	C_{33} &amp; C_{34} \\
	C_{43} &amp; C_{44}
\end{pmatrix}
\begin{pmatrix}
	n \\
	1
\end{pmatrix}
=
\begin{pmatrix}
	C_{33}n + C_{34} \\
	C_{43}n + C_{44}
\end{pmatrix} \\
\therefore C_{33}n + C_{34} = 0\]

\[\begin{pmatrix}
	C_{33} &amp; C_{34} \\
	C_{43} &amp; C_{44}
\end{pmatrix}
\begin{pmatrix}
	f \\
	1
\end{pmatrix}
=
\begin{pmatrix}
	C_{33}f + C_{34} \\
	C_{43}f + C_{44}
\end{pmatrix} \\
\therefore C_{33}f + C_{34} = C_{43}f + C_{44}\]

\[\begin{pmatrix}
	C_{33} &amp; C_{34} \\
	C_{43} &amp; C_{44}
\end{pmatrix}
\begin{pmatrix}
	-\frac{1}{p} \\
	1
\end{pmatrix}
=
\begin{pmatrix}
	-\frac{C_{33}}{p} + C_{34} \\
	-\frac{C_{43}}{p} + C_{44}
\end{pmatrix} \\
\therefore C_{44}p - C_{43} = 0\]

<p>연립방정식을 푼 결과는 다음과 같습니다.</p>

\[C_{33} = \frac{1 + pf}{f - n}C_{44} \\
C_{34} = -n\frac{1 + pf}{f - n}C_{44} \\
C_{43} = pC_{44}\]

<p>등식이 3개였으므로 미지수 중 하나는 결정할 수 없지만, 어차피 투시 나눗셈을 하면 상쇄되므로 편의상 \(C_{44}\) = 1로 정하겠습니다.</p>

\[\boldsymbol{C}
=
\begin{pmatrix}
	C_{11} &amp; 0 &amp; 0 &amp; 0 \\
	0 &amp; C_{22} &amp; 0 &amp; 0 \\
	0 &amp; 0 &amp; a &amp; -na \\
	0 &amp; 0 &amp; p &amp; 1
\end{pmatrix} \;
\mathrm{where} \; a = \frac{1 + pf}{f - n}\]

<p>이제 마지막으로 \(z\) = 0인 경우의 좌표 하나를 대입하면 \(\boldsymbol{C}\)를 완전히 결정할 수 있습니다.</p>

\[\begin{pmatrix}
	C_{11} &amp; 0 &amp; 0 &amp; 0 \\
	0 &amp; C_{22} &amp; 0 &amp; 0 \\
	0 &amp; 0 &amp; a &amp; -na \\
	0 &amp; 0 &amp; p &amp; 1
\end{pmatrix}
\begin{pmatrix}
	\frac{w}{2} \\
	\frac{h}{2} \\
	0 \\
	1
\end{pmatrix}
=
\begin{pmatrix}
	\frac{w}{2}C_{11} \\
	\frac{h}{2}C_{22} \\
	-na \\
	1
\end{pmatrix}
\sim (1, 1, ?) \\
\therefore C_{11} = \frac{2}{w}, C_{22} = \frac{2}{h}\]

<p>완성된 \(\boldsymbol{C}\)는 다음과 같고...</p>

\[\boldsymbol{C}
=
\begin{pmatrix}
	\frac{2}{w} &amp; 0 &amp; 0 &amp; 0 \\
	0 &amp; \frac{2}{h} &amp; 0 &amp; 0 \\
	0 &amp; 0 &amp; a &amp; -na \\
	0 &amp; 0 &amp; p &amp; 1
\end{pmatrix}\]

<p>완성된 \(\boldsymbol{M}\)은 다음과 같습니다. 🎉</p>

\[\boldsymbol{M} = \boldsymbol{C}\boldsymbol{S}\boldsymbol{O}
=
\begin{pmatrix}
	\frac{2}{w} &amp; 0 &amp; 0 &amp; 0 \\
	0 &amp; \frac{2}{h} &amp; 0 &amp; 0 \\
	0 &amp; 0 &amp; a &amp; -na \\
	0 &amp; 0 &amp; p &amp; 1
\end{pmatrix}
\begin{pmatrix}
	1 &amp; 0 &amp; -\Delta x &amp; 0 \\
	0 &amp; 1 &amp; -\Delta y &amp; 0 \\
	0 &amp; 0 &amp; 1 &amp; 0 \\
	0 &amp; 0 &amp; 0 &amp; 1
\end{pmatrix}
\begin{pmatrix}
	1 &amp; 0 &amp; 0 &amp; -\frac{w}{2} \\
	0 &amp; 1 &amp; 0 &amp; -\frac{h}{2} \\
	0 &amp; 0 &amp; 1 &amp; 0 \\
	0 &amp; 0 &amp; 0 &amp; 1
\end{pmatrix}\]

<p>저는 여기서 \(\boldsymbol{S}\boldsymbol{O}\)에 해당하는 아래 행렬을 뷰 행렬로 사용했고...</p>

\[\begin{pmatrix}
	1 &amp; 0 &amp; -\Delta x &amp; -\frac{w}{2} \\
	0 &amp; 1 &amp; -\Delta y &amp; -\frac{h}{2} \\
	0 &amp; 0 &amp; 1 &amp; 0 \\
	0 &amp; 0 &amp; 0 &amp; 1
\end{pmatrix}\]

<p>나머지 \(\boldsymbol{C}\)를 그대로 사영 행렬로 사용했습니다.</p>

<h1 id="충돌-판정">충돌 판정</h1>

<p><a href="https://en.wikipedia.org/wiki/Collision_detection#A_posteriori_(discrete)_versus_a_priori_(continuous)">위키백과</a>에서는 충돌 처리의 두 가지 방법론으로 사후 (이산) 처리와 사전 (연속) 처리를 들고 있습니다.</p>

<ul>
  <li><strong>사후</strong>(a posteriori) 처리는 일단 물체를 움직인 후 겹치는 물체가 생기면 충돌한 것으로 간주해 적절한 처리를 하는 방식입니다. 절대 다수의 게임에서 이 방식을 사용하고 있습니다.</li>
  <li><strong>사전</strong>(a priori) 처리는 모든 물체의 궤적을 사전에 계산한 뒤 두 물체의 궤적 사이에 교점이 생기는 정확한 시점을 예측해 적절한 처리를 하는 방식입니다. 현실에서 물체가 충돌하는 방식과 더 비슷합니다.</li>
</ul>

<p>프로젝트를 막 시작했을 때는 모든 충돌 판정을 사전 처리로 구현하려고 했는데, 학기말이 생각보다 빨리 다가오고 있어서 시점 변환만 사후 처리로 구현했습니다. (원래는 시점 변환 처리에 사차방정식이 필요했다고 서술했는데, 다시 계산해보니 이차방정식으로 충분해 보여서 삭제했습니다. 이차방정식도 기간 안에 구현하기는 무리였을 거라고 생각합니다.)</p>

<h2 id="변환을-거친-맵-요소의-표현">변환을 거친 맵 요소의 표현</h2>

<p>플레이어와 맵 요소 사이의 충돌을 판정하려면 먼저 변환을 거친 맵 요소를 충돌 판정을 할 수 있는 형식으로 표현해야 합니다. 여러 가지 방법을 구상하다가 직육면체를 사영하면 외곽선을 4~6개의 선분으로 표현할 수 있는 것을 기억해내고 방향이 있는 선분을 시계 방향으로 배치해서 충돌 범위를 표현했습니다.</p>

<p><img src="/assets/post-images/projected-block-border.png" alt="게임 내의 블록 한 칸이 사영된 외곽선이 여섯 개의 선분으로 표현되어 있다." /></p>

<p>충돌 판정을 편하게 하기 위해 <a href="https://en.wikipedia.org/wiki/Minkowski_addition">민코프스키 차</a>를 구하고 4~6개의 선분을 기준으로 얼마나 깊이 묻혀있는지 계산해서 모두 양수이면 충돌한 것으로, 하나라도 음수이면 충돌하지 않은 것으로 판단합니다. 이 방법에는 어느 방향으로 얼마나 움직이면 충돌이 해결되는지도 바로 알 수 있다는 장점이 있습니다.</p>

<h2 id="단방향-경계면의-충돌-판정">단방향 경계면의 충돌 판정</h2>

<p>Project::shear에서는 앞면이 보일 때만 충돌 판정에 참여하는 맵 요소를 찾아볼 수 있습니다. 인게임에서 충분히 드러나지 못한 것 같아 아쉬웠던 점 중 하나입니다.</p>

<p><img src="/assets/post-images/project-shear-stage-5.png" alt="Project::shear의 스테이지 5" /></p>

<p>이 판정 역시 아까 구한 사영 행렬에서 유도했는데, 예를 들어 위 스크린샷에 있는 왼쪽 경계면은 \(z\)가 커질수록 사영되는 점이 왼쪽으로 갈 때만(즉, 앞면이 보일 때만) 충돌 판정을 활성화하는 방식으로 구현했습니다. 참고로 \((x, y, z)\)의 변환을 닫힌 형태로 구하면 이렇습니다.</p>

\[\begin{align}
\boldsymbol{C}\boldsymbol{S}\boldsymbol{O}
\begin{pmatrix}
	x \\
	y \\
	z \\
	1
\end{pmatrix}
&amp;=
\begin{pmatrix}
	\frac{2}{w} &amp; 0 &amp; -2\frac{\Delta x}{w} &amp; -1 \\
	0 &amp; \frac{2}{h} &amp; -2\frac{\Delta y}{h} &amp; -1 \\
	0 &amp; 0 &amp; \frac{1 + pf}{f - n} &amp; -n\frac{1 + pf}{f - n} \\
	0 &amp; 0 &amp; p &amp; 1
\end{pmatrix}
\begin{pmatrix}
	x \\
	y \\
	z \\
	1
\end{pmatrix} \\
&amp;=
\begin{pmatrix}
	2\frac{x - z\Delta x}{w} - 1 \\
	2\frac{y - z\Delta y}{h} - 1 \\
	(z - n)\frac{1 + pf}{f - n} \\
	zp + 1
\end{pmatrix} \\
&amp;\sim
(\frac{2x - 2z \Delta x - w}{w(zp + 1)}, \frac{2y - 2z \Delta y - h}{h(zp + 1)}, \frac{(z - n)(1 + pf)}{(zp + 1)(f - n)})
\end{align}\]

<p>여기서 \(x\)좌표를 \(z\)에 대해 미분하고 정리하면 \(2xp &gt; wp - 2 \Delta x\)까지 간단해집니다. 오른쪽, 위쪽, 아래쪽 경계면도 \(x\), \(\Delta x\), \(w\)를 \(y\), \(\Delta y\), \(h\)로 바꾸거나 부등호의 방향만 바꿔서 구현했습니다.</p>

<h2 id="수치-안정성">수치 안정성</h2>

<p>플랫포밍을 처음 구현했을 때는 특정한 조작을 하면 블록 사이로 빠지는 버그가 있었습니다. (참고로 일부 블록이 깜박이는 것은 충돌 판정 디버깅입니다.)</p>

<p>
<video muted="" controls="" poster="/assets/post-images/project-shear-nov24-thumbnail.png">
<source type="video/webm" src="/assets/post-images/project-shear-nov24.webm" />
</video>
</p>

<p>'특정한 조작'을 디버깅할 때마다 다시 입력하는 것이 귀찮아서 자세히 파보지는 않았지만, 부동소숫점 특유의 수치 안정성 문제라고 생각하고 충돌 판정 시 \(\frac{1}{256}\)블록만큼의 완충 지대를 두어 해결했습니다. 이외에도 이동 처리 중에 무한루프에 빠지는 등의 문제가 있었는데, 열심히 디버그를 해서 어느 시점 이후에는 더 이상 문제가 발생하지 않았습니다.</p>

<h1 id="스테이지-8을-보셨나요">스테이지 8을 보셨나요?</h1>

<p><img src="/assets/post-images/project-shear-stage-8.png" alt="Project::shear의 스테이지 8. 플레이어 모델이 카메라의 시야 방향으로 늘어나 있다." /></p>

<p><strong>서프라이즈!!!!!</strong> 당연히 충돌 판정이 이런 식으로 구현된 건 아니고, 게임의 충돌 판정에 그럴듯한 '세계관 내' 설명을 붙이고 동시에 페이크 3D 게임이 아니라는 것을 어필하려는 결과였습니다. (3D 게임에 가산점이 부여된다는 공지가 있었습니다.)</p>

<p>이 게임의 모든 모델은 별도의 외부 파일 없이 게임 내에서 절차적으로 구성했는데, 플레이어 모델은 <code class="language-plaintext highlighter-rouge">persp</code>에 따라 <em>비선형으로</em> 바뀌기 때문에 매번 정점 버퍼를 다시 만들어줘야 했습니다. 지금 생각해보면 행렬을 어떻게 잘 만들어서 모델 하나를 돌려쓸 수 있었을 것 같은데 실제로 가능할지는 잘 모르겠네요.</p>

<p>참고로 8스테이지에 나온 플레이어 모델은 사실 1스테이지부터 계속 쓰고 있었습니다.</p>

<h1 id="결론">결론</h1>

<p>드디어 기억나는 내용을 전부 썼네요!</p>

<p>허니하우스에서 배웠던 <a href="/2021/04/29/how-i-made-honeyhouse.html#결론">기획은 작게 잡으라는</a> 교훈을 잊어버리고 또 커다란 기획을 세웠다가 제가 다 구현해야 한다는 현실의 벽에 부딪혔습니다. 그나마 4명이 조를 짜서 작업해서 그런지 조금 서두르긴 했지만 마감을 못 맞추는 사태는 피했습니다. 아마 이 글의 링크를 작업용 디스코드 서버에도 올릴 텐데 이 자리를 빌려 감사의 말씀을 전하고 싶습니다.</p>

<p>이번 글은 이 정도로 마칠까 합니다. 재밌게 읽으셨길 바라요 🙇‍♂️</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:fn-dolly-zoom-coefficient" role="doc-endnote">
      <p>실제 게임에서는 자주 사용하는 범위가 0 이상 1 이하에 대응하도록 <code class="language-plaintext highlighter-rouge">persp</code>에 상수를 곱하지만, 이 글에서는 무시하겠습니다. <a href="#fnref:fn-dolly-zoom-coefficient" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:fn-frustum" role="doc-endnote">
      <p>Frustum. 주로 각뿔이나 원뿔을 평행한 두 평면으로 자른 도형. 특히 3D 렌더링에서는 카메라의 직사각뿔 모양 시야를 near 평면과 far 평면으로 자른 도형을 말한다. <a href="#fnref:fn-frustum" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:fn-opengl-es-platform" role="doc-endnote">
      <p>최소한 제가 개발에 사용한 Windows 11, x86-64 환경에서는 글에 적은 것과 같이 동작하지만 다른 환경에서도 똑같이 동작할지는 잘 모르겠습니다. 혹시 그래픽이 깨지면 알려주세요. <a href="#fnref:fn-opengl-es-platform" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="GameMaker" /><category term="게임개발" /><category term="선형대수학" /><summary type="html"><![CDATA[2025년 1월 6일 수정: 글을 급하게 작성하다 보니 잘못된 부분을 몇 군데 발견했습니다. 꼼꼼히 검토하지 못한 점에 대해 사과드립니다.]]></summary></entry><entry><title type="html">백준 온라인 저지에 잇창명이 출제한 문제 모음</title><link href="https://eatchangmyeong.github.io/2024/02/28/eatchangmyeong-s-boj-problem-compilation.html" rel="alternate" type="text/html" title="백준 온라인 저지에 잇창명이 출제한 문제 모음" /><published>2024-02-28T00:00:00+09:00</published><updated>2024-02-28T00:00:00+09:00</updated><id>https://eatchangmyeong.github.io/2024/02/28/eatchangmyeong-s-boj-problem-compilation</id><content type="html" xml:base="https://eatchangmyeong.github.io/2024/02/28/eatchangmyeong-s-boj-problem-compilation.html"><![CDATA[<p>그동안 중앙대학교 알고리즘 학회 <a href="http://chaos.or.kr/">ChAOS</a>에 소속되어 백준 온라인 저지에 대회 문제를 출제해보는 귀중한 기회를 몇 번 가졌었는데, 그동안 생각해보니 대회 에디토리얼 이외에 블로그에 문제에 대한 이야기를 한 적이 없었네요. 마침 생각난 김에 그동안 출제했던 문제의 출제 의도를 하나씩 풀어보려고 합니다. (정해 코드는 없습니다!)</p>

<p>글 작성 이후에도 <a href="https://www.acmicpc.net/user/dlaud5379"><code class="language-plaintext highlighter-rouge">dlaud5379</code></a> 명의로 다른 문제를 출제할 때마다 업데이트할 예정입니다.</p>

<h1 id="cpc-2020">CPC 2020</h1>

<p><a href="https://www.acmicpc.net/category/detail/2345">2020 중앙대학교 프로그래밍 경진대회</a>에 한 문제를 출제하고, 두 문제에 삽화를 제공했습니다.</p>

<h2 id="20210번-파일-탐색기-f번"><a href="https://www.acmicpc.net/problem/20210">20210번 "파일 탐색기"</a> (F번)</h2>

<blockquote>
  <p><strong>문제</strong></p>

  <p>Windows의 파일 탐색기를 보면 파일이 정렬된 방식이 보통의 정렬 방식과는 다른 것을 알 수 있다.
보통 문자열을 정렬할 때는 맨 앞부터 한 글자씩 비교하다가 어느 한쪽이 끝나거나 일치하지 않는 글자가 있으면 그 위치의 문자를 비교한 결과가 문자열 전체를 비교한 결과가 된다. 한편 파일 탐색기는 여러 자리의 수를 한 글자로 취급해서 비교하는데, 이 때문에 "<code class="language-plaintext highlighter-rouge">str12ing</code>"과 "<code class="language-plaintext highlighter-rouge">str123ing</code>" 중 후자가 아니라 전자가 앞에 온다. 이러한 정렬 방식을 종종 "natural sort"라고 하기도 한다.
여러 개의 문자열이 주어지면 natural sort 방식으로 정렬한 결과를 출력하는 프로그램을 작성해 보자. 이 문제에서 구현할 알고리즘은 다음을 만족해야 한다.</p>

  <ol>
    <li>문자열은 알파벳 대소문자와 숫자로만 이루어져 있다.</li>
    <li>숫자열이 알파벳보다 앞에 오고, 알파벳끼리는 AaBbCc...XxYyZz의 순서를 따른다.</li>
    <li>문자열을 비교하는 중 숫자가 있을 경우 그 다음에 오는 숫자를 최대한 많이 묶어 한 단위로 비교한다. 예를 들어 "<code class="language-plaintext highlighter-rouge">a12bc345</code>"는 "<code class="language-plaintext highlighter-rouge">a</code>", "<code class="language-plaintext highlighter-rouge">12</code>", "<code class="language-plaintext highlighter-rouge">b</code>", "<code class="language-plaintext highlighter-rouge">c</code>", "<code class="language-plaintext highlighter-rouge">345</code>"의 다섯 단위로 이루어져 있다.</li>
    <li>숫자열끼리는 십진법으로 읽어서 더 작은 것이 앞에 온다. 이때 예제 2에서와 같이 값이 \(2^{63}\)을 초과할 수 있다.</li>
    <li>같은 값을 가지는 숫자열일 경우 앞에 따라붙는 0의 개수가 적은 것이 앞에 온다.</li>
  </ol>

  <p><strong>입력</strong></p>

  <p>첫 줄에 문자열의 개수 <em>N</em>(2 ≤ <em>N</em> ≤ 10,000)이 주어진다. 그 다음 <em>N</em>줄에 정렬할 문자열이 한 줄에 하나씩 주어진다.
모든 문자열의 길이는 100 이하이며, 알파벳 대소문자와 숫자로만 이루어져 있다.</p>

  <p><strong>출력</strong></p>

  <p><em>N</em>줄에 걸쳐 문제에서 설명한 대로 문자열을 정렬한 결과를 출력한다.</p>

  <p><strong>예제 입력 1</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>8
Foo1Bar
Foo12Bar
Foo3bar
Fo6Bar
Foo00012Bar
Foo3Bar
foo4bar
FOOBAR
</code></pre></div>  </div>

  <p><strong>예제 출력 1</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>FOOBAR
Fo6Bar
Foo1Bar
Foo3Bar
Foo3bar
Foo12Bar
Foo00012Bar
foo4bar
</code></pre></div>  </div>

  <p><strong>예제 입력 2</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>5
1234567890123456789012345678901234500500
1234567890123456789012345678901234500000
1234567890123456789012345678901234506000
1234567890123456789012345678901234500002
1234567890123456789012345678901234530000
</code></pre></div>  </div>

  <p><strong>예제 출력 2</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>1234567890123456789012345678901234500000
1234567890123456789012345678901234500002
1234567890123456789012345678901234500500
1234567890123456789012345678901234506000
1234567890123456789012345678901234530000
</code></pre></div>  </div>
</blockquote>

<p>2024년 9월 2일 현재 이 문제의 난이도는 골드 3이며, 알고리즘 분류는 <strong>#구현</strong> <strong>#정렬</strong> <strong>#문자열</strong>입니다. 출제 이후 거의 4년이 지난 지금(2024년 2월 8일) 구글에 <code class="language-plaintext highlighter-rouge">백준 20210</code>으로 검색해 봤더니 생각보다 이 문제에 대한 블로그 글이 많네요. 풀어주셔서 감사합니다 🙇</p>

<p>지문에서 짐작할 수 있듯이 표준 라이브러리에서 제공하는 정렬 함수에 비교 함수를 만들어 호출하면 되는 정직한 문제입니다. 물론 그 비교 함수 구현이 좀 복잡하긴 합니다. 비교 함수의 조건을 간단하게 요약하면 이렇습니다.</p>

<ul>
  <li>숫자는 무조건 문자보다 작다.</li>
  <li>문자끼리 비교
    <ul>
      <li>대소문자 구분 없이 사전순으로 비교하고,</li>
      <li>같은 문자라면 대문자가 소문자보다 작다.</li>
    </ul>
  </li>
  <li>숫자끼리 비교
    <ul>
      <li>최대한 긴 숫자열로 묶는다.</li>
      <li>십진법으로 읽은 값으로 비교하고,</li>
      <li>같은 값이라면 불필요한 0이 적을수록 작다.</li>
    </ul>
  </li>
</ul>

<p><a href="https://www.acmicpc.net/source/23980193">제가 의도한 C++14 풀이</a>(정답자만 열람 가능합니다)에서 비교 함수의 전체적인 구조는 이렇습니다. 입력 문자열의 길이 \(n\)에 대해 비교 함수의 시간 복잡도는 \(O(n)\)입니다.</p>

<div class="language-plaintext pseudocode highlighter-rouge"><div class="highlight"><pre class="highlight"><code>// 좌변이 작으면 음수, 우변이 작으면 양수, 같으면 0
int 문자끼리_비교(char 좌변, char 우변) {
	if(대문자로 변환한 좌·우변이 다름)
		return (대문자로 변환한 좌변이 다르면 -1, 아니면 1);
	return (좌·우변의 대문자 여부만 추출해서 비교);
}

// 좌변이 우변 미만이어야 `true`
bool 비교함수(const string &amp;좌변, const string &amp;우변) {
	while(좌·우변을 첫 글자부터 스캔하면서) {
		if(우변이 일찍 끝남)
			return false;
		if(좌변이 일찍 끝남)
			return true;
		
		if(좌·우변의 숫자/문자 여부가 다름)
			return (좌변이 숫자인지 여부);
		if(좌·우변이 문자) {
			// 문자끼리 비교
			int 비교결과 = 문자끼리_비교(현재 좌·우변에서 스캔하는 글자);
			if(비교결과 != 0)
				return 비교결과 &lt; 0;
		} else {
			// 숫자열끼리 비교

			// 값으로 비교
			if(좌·우변에서 불필요한 0을 뺀 길이가 다름)
				return (불필요한 0을 뺀 좌·우변의 문자열 길이끼리 비교);
			int 비교결과 = (불필요한 0을 뺀 좌·우변을 사전식으로 비교);
			if(비교결과 != 0)
				return 비교결과 &lt; 0;
			// 불필요한 0의 개수로 비교
			if(좌·우변의 불필요한 0의 개수가 다름)
				return (좌·우변의 불필요한 0의 개수로 비교);
			// 두 숫자열이 일치함
		}
	}
}
</code></pre></div></div>

<h1 id="chac-2022">CHAC 2022</h1>

<p><a href="https://www.acmicpc.net/category/detail/3016">ChAOS Hello 2022 Algorithm Content</a>에 두 문제를 출제하고, 세 문제에 삽화를 제공했습니다.</p>

<p>CPC 2020에 문제를 출제할 때 지문에 "잇창명"을 넣지 않은 게 후회되어서 이번에 출제하는 두 문제에는 모두 "잇창명"을 넣었습니다.</p>

<h2 id="24390번-또-전자레인지야-d번"><a href="https://www.acmicpc.net/problem/24390">24390번 "또 전자레인지야?"</a> (D번)</h2>

<blockquote>
  <p><strong>문제</strong></p>

  <p>잇창명의 집에는 오래된 전자레인지가 있다. 백준 온라인 저지에서 문제를 너무 많이 푼 잇창명은 문득 이런 궁금증이 생기기 시작했다.</p>

  <blockquote>
    <p>버튼을 최소 몇 번 눌러야 조리시간 2분을 맞출 수 있을까?</p>
  </blockquote>

  <p>잇창명의 전자레인지에는 다음과 같이 버튼이 4개 있고, 각 버튼을 누르면 다음과 같이 작동한다. 초기 상태에는 조리시간이 0초이고, 조리 중이 아니며, 조리시작 버튼을 눌러야 조리가 시작된다.</p>

  <ul>
    <li>10초: 조리시간이 10초 늘어난다.</li>
    <li>1분: 조리시간이 1분(60초) 늘어난다.</li>
    <li>10분: 조리시간이 10분(600초) 늘어난다.</li>
    <li>조리시작
      <ul>
        <li>조리 중이 아닐 때: 조리가 시작된다. 만약에 조리시간이 0초였다면 30초로 늘어난다.</li>
        <li>조리 중일 때: 조리시간이 30초 늘어난다.</li>
      </ul>
    </li>
    <li>모든 버튼은 조리 중인지의 여부와 무관하게 항상 누를 수 있으며, 별도의 언급이 없을 경우 항상 같은 동작을 한다.</li>
  </ul>

  <p>예를 들어 이 전자레인지로 2분을 맞추려면 조리시작 버튼을 4번 누르면 되지만, 최적의 방법은 아니다. 그 대신 1분-1분-조리시작 순서로 버튼을 누르면 버튼을 누른 횟수가 3번이 되어 최적이다. 1분-1분의 경우에는 조리가 되지 않기 때문에 최적이 아니다. 실제로는 조리 중에는 남은 조리시간이 계속 줄어들고 중간에 조리를 취소할 수 있지만, 이 문제에서는 생각하지 않기로 한다.</p>

  <p>잇창명은 지난 한 학기 동안 전자레인지를 이용할 때마다 매번 문제로 내고 싶은 마음이 들어서 괴로워하고 있다. 잇창명을 도와주자!</p>

  <p><strong>입력</strong></p>

  <p>첫 줄에 잇창명이 원하는 조리시간이 <code class="language-plaintext highlighter-rouge">M:S</code> 형태로 주어진다(0 ≤ <em>M</em> ≤ 60, 0 ≤ <em>S</em> ≤ 59). <em>M</em>은 분, <em>S</em>는 초이며, 항상 두 자리 숫자로 주어진다.</p>

  <p>조리시간은 10초 이상 60분(3600초) 이하이며, 항상 10의 배수이다.</p>

  <p><strong>출력</strong></p>

  <p>주어진 조리시간을 맞추기 위해 버튼을 눌러야 하는 최소 횟수를 출력한다.</p>

  <p><strong>예제 입력 1</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>02:00
</code></pre></div>  </div>

  <p><strong>예제 출력 1</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>3
</code></pre></div>  </div>
</blockquote>

<p>2024년 9월 2일 현재 이 문제의 난이도는 실버 2이며, 알고리즘 분류는 <strong>#그리디 알고리즘</strong>입니다.</p>

<p>처음 출제했을 때는 탐욕법과 너비 우선 탐색만을 정해로 생각했는데, 문제 검수 중에 동적 계획법으로 풀 수 있다는 것을 알게 되어 결과적으로는 <strong>정해가 사실상 3개</strong>인 문제가 되었습니다. 개인적으로 좋은 문제가 되었다고 생각해 꽤 만족스럽네요.</p>

<h3 id="너비-우선-탐색">너비 우선 탐색</h3>

<p>가장 직관적으로 떠오를 만한 풀이입니다. 조리시간(<code class="language-plaintext highlighter-rouge">int</code>)과 조리 여부(<code class="language-plaintext highlighter-rouge">bool</code>)의 순서쌍을 정점으로, 각 버튼마다 가능한 상태 변화를 유향 간선으로 삼고 \((0, \mathrm{false})\) 노드에서 출발하는 너비 우선 탐색을 할 수 있습니다.</p>

<h3 id="탐욕법">탐욕법</h3>

<p><a href="https://www.acmicpc.net/source/38169595">제가 처음 의도했던 C11 풀이</a>이기도 합니다(정답자만 열람 가능합니다). 당시 <a href="https://gather.town/">Gather</a>에서 공개했던 에디토리얼에 탐욕법에 대한 증명을 실었었는데, 대중에 공개된 링크를 못 찾겠네요. 이 블로그에는 좀 더 엄밀하게 다듬은 증명을 싣겠습니다.</p>

<p>30초 버튼의 특수한 성질을 무시하고 문제를 풀면 30초 버튼을 정확히 0번 혹은 1번 누를 수 있습니다(30초 다음 단위가 60초이므로).</p>

<ul>
  <li><strong>30초 버튼을 1번 누르는 경우</strong>: 30초 버튼을 가장 처음에 누르면 정상적으로 30초가 추가되<strong>면서</strong> 조리가 시작되므로 일반적인 탐욕법과 같은 해를 가집니다.</li>
  <li><strong>30초 버튼을 누르지 않는 경우</strong>: 이때는 조리가 시작되지 않으므로 어떻게든 30초 버튼을 추가로 눌러야 합니다. 편의상 추가로 버튼을 누르는 횟수를 \(n\)이라고 하겠습니다.
    <ul>
      <li>모든 경우에 대해 \(n=1\)인 알고리즘이 존재합니다.
        <ul>
          <li>모든 버튼을 누르고 나서 <strong>30초 버튼을 마지막에 추가로 누를</strong> 수 있는데, 아직 조리가 시작되지 않았으므로 조리가 시작되고 원래 조리시간이 0이 아니었으므로 조리시간은 유지됩니다.</li>
        </ul>
      </li>
      <li>탐욕법의 성질에 의해 \(n \le 0\)일 수는 없습니다.</li>
      <li>위의 논리에 의해 일반적인 탐욕법으로 구한 해에 \(n=1\)을 더한 것이 해가 됩니다.</li>
    </ul>
  </li>
</ul>

<p>추가로 입력 범위가 작은 것을 이용해 너비 우선 탐색 코드와 탐욕법 코드가 모든 입력에 대해 같은 출력을 하는지 체크하는, 말 그대로 "proof by AC"<sup id="fnref:fn-proof-by-ac" role="doc-noteref"><a href="#fn:fn-proof-by-ac" class="footnote" rel="footnote">1</a></sup>를 했습니다.</p>

<h3 id="동적-계획법">동적 계획법</h3>

<p>위의 너비 우선 탐색 풀이를 그대로 동적 계획법으로 바꿀 수 있습니다. 조리시간 <code class="language-plaintext highlighter-rouge">i</code>와 조리 여부 <code class="language-plaintext highlighter-rouge">j</code>에 대해 <code class="language-plaintext highlighter-rouge">dp[i][j]</code>를 만들고 풀면 됩니다.</p>

<h2 id="24394번-123456789점-g번"><a href="https://www.acmicpc.net/problem/24394">24394번 "123456789점"</a> (G번)</h2>

<blockquote>
  <p><strong>문제</strong></p>

  <p>잇창명은 취미로 리듬 게임을 한다. 위에서 내려오는 노트를 정확한 타이밍에 처리하면 높은 점수를 얻을 수 있다.</p>

  <p>노트를 처리하면 각각 다음 판정 중 하나를 받을 수 있다.</p>

  <ul>
    <li>Perfect</li>
    <li>Great</li>
    <li>Good</li>
    <li>Miss</li>
  </ul>

  <p>노트를 처리하지 못하면 Miss 판정을 받는다. 위 4가지 판정 이외에 다른 판정은 없다. 노트가 <em>N</em>개 있는 곡에서 Perfect 판정을 <em>a</em>개, Great 판정을 <em>b</em>개, Good 판정을 <em>c</em>개 받았을 때의 점수는 다음 식으로 계산한다. 소숫점 아래는 버린다.</p>

  <p>\(\biggl\lfloor {10^9 \times \frac{2a + 2b + c}{2N} + a} \biggr\rfloor\) </p>

  <p>다시 말해, Good 판정의 점수인 \(\frac{10^9}{2N}\)을 기준으로 각 판정의 점수는 다음과 같다.</p>

  <ul>
    <li>Perfect = 2 Good + 1</li>
    <li>Great = 2 Good</li>
    <li>Good = 1 Good</li>
    <li>Miss = 0</li>
  </ul>

  <p>예를 들어 노트가 100개 있는 곡에서 Perfect를 10개, Great를 20개, Good을 30개 받으면 450,000,010점을 획득한다. 이때 최고 점수는 Perfect 판정만을 <em>N</em>개 받았을 때 \(10^9 + N\)점이 된다.</p>

  <p>잇창명은 이 게임에서 정확히 123,456,789점을 획득해 사람들을 놀라게 하고 싶다. 잇창명은 초고수 플레이어라서 모든 노트의 판정을 원하는 대로 조절할 수 있다. 잇창명이 연주하려는 곡의 노트 수가 주어질 때, 잇창명이 이 곡을 어떻게 연주해야 원하는 점수를 획득할 수 있을지 계산하는 프로그램을 작성하시오.</p>

  <p><strong>입력</strong></p>

  <p>첫 줄에 테스트 케이스의 수 <em>T</em>(1 ≤ <em>T</em> ≤ 2,000)가 주어진다. 다음 줄부터는 <em>T</em>개의 테스트 케이스가 주어진다.</p>

  <p>각 테스트 케이스에서는 한 줄에 노트의 개수 <em>N</em>(1 ≤ <em>N</em> ≤ 20,000)과 잇창명이 원하는 점수 <em>S</em>(0 ≤ <em>S</em> ≤ 109+<em>N</em>)가 공백으로 구분되어 주어진다. <em>N</em>과 <em>S</em>는 정수이다.</p>

  <p><strong>출력</strong></p>

  <p>각 테스트 케이스마다 한 줄에 걸쳐 다음을 출력한다.</p>

  <ul>
    <li>잇창명이 Perfect 판정을 <em>a</em>개, Great 판정을 <em>b</em>개, Good 판정을 <em>c</em>개 받아서 정확히 <em>S</em>점을 획득할 수 있다면 2<em>a</em>+2<em>b</em>+<em>c</em>와 <em>a</em>를 공백으로 구분하여 출력한다. 가능한 (2<em>a</em>+2<em>b</em>+<em>c</em>, <em>a</em>)의 쌍이 여러 개인 입력은 없다.</li>
    <li>잇창명이 곡을 어떻게 연주해도 정확히 <em>S</em>점을 획득할 수 없다면 <code class="language-plaintext highlighter-rouge">-1</code>을 출력한다.</li>
  </ul>

  <p><strong>예제 입력 1</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>3
1000 1000001000
4318 899954000
20000 123456789
</code></pre></div>  </div>

  <p><strong>예제 출력 1</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2000 1000
7772 318
-1
</code></pre></div>  </div>
</blockquote>

<p>2024년 9월 2일 현재 이 문제의 난이도는 골드 3이며, 알고리즘 분류는 <strong>#수학</strong>입니다. 원래 \(O(T)\)를 의도한 문제라서 난이도 의견에 "\(O(TN)\)이 [출제자의] 의도"라는 말을 보고 깜짝 놀랐습니다.</p>

<h3 id="소재와의-연관성">소재와의 연관성</h3>

<p>제가 남긴 solved.ac 난이도 의견에서 볼 수 있듯이 이 문제의 소재는 실존하는 리듬 게임인 <a href="https://arcaea.lowiro.com/">Arcaea</a>, 특히 점수 체계입니다. 특히 문제에서 제시한 판정 체계는 실제 게임의 판정 체계를 더욱 대중적인 용어로 바꾼 것입니다.</p>

<ul>
  <li>Perfect = Shiny PURE</li>
  <li>Great = Early/Late PURE
    <ul>
      <li>인게임에서는 기본적으로 세 판정이 모두 PURE로만 표시되며, Early/Late PURE는 따로 표시하도록 하는 숙련자용 설정이 있습니다.</li>
    </ul>
  </li>
  <li>Good = FAR</li>
  <li>Miss = LOST</li>
</ul>

<p>이 게임을 다루는 비공식 위키인 Arcaea Wiki의 <a href="https://breezewiki.com/arcaea/wiki/Scoring">Scoring</a><sup id="fnref:fn-breezewiki" role="doc-noteref"><a href="#fn:fn-breezewiki" class="footnote" rel="footnote">2</a></sup> 문서에서 이 게임의 점수 체계를 확인해볼 수 있는데, 이 역시 기준 점수(10,000,000점)를 제외하고 문제에서 제시하는 것과 일치합니다.</p>

<ul>
  <li>Shiny PURE = 100% + 1점
    <ul>
      <li>이때 100%는 기준 점수를 노트 수로 나눈 것을 기준으로 합니다.</li>
    </ul>
  </li>
  <li>Early/Late PURE = 100%</li>
  <li>FAR = 50%</li>
  <li>LOST = 0%</li>
  <li>콤보는 점수에 전혀 영향이 없습니다. 문제에서도 혼동을 피하기 위해 콤보와 관련된 언급을 일절 하지 않았습니다.</li>
</ul>

<p>원래는 문제에서도 기준 점수를 10,000,000점으로 두었지만, 범위가 너무 작아서 의도치 않은 풀이를 허용할 수 있다는 검수진의 의견을 수용해 1,000,000,000점으로 늘렸습니다.</p>

<p>마지막으로 같은 문서의 <a href="https://breezewiki.com/arcaea/wiki/Scoring#Trivia">Trivia 문단</a>에는 다음과 같은 언급이 있습니다. 원문에 없는 강조를 추가했습니다.</p>

<blockquote>
  <ul>
    <li>채보<sup id="fnref:fn-chart" role="doc-noteref"><a href="#fn:fn-chart" class="footnote" rel="footnote">3</a></sup>에 충분한 수의 노트가 있을 경우, 이론적으로는 Pure Memory<sup id="fnref:fn-pure-memory" role="doc-noteref"><a href="#fn:fn-pure-memory" class="footnote" rel="footnote">4</a></sup> 없이도 10,000,000점 이상을 획득하는 것이 가능하다.
      <ul>
        <li>예를 들어, 노트가 2,500개 있는 채보에서 2,499 Shiny PURE + 1 FAR를 받으면 10,000,499점이 된다.</li>
        <li>최소 \(\lceil 0.5 \times \left( 1 + \sqrt{\left( 2 \times 10,000,000 \right) + 1} \right) \rceil = 2237\)개의 노트가 필요하다. <strong>현재 최대 콤보가 2237 이상인 채보가 없으므로 이는 불가능하다.</strong></li>
      </ul>
    </li>
  </ul>
</blockquote>

<p>이 서술에서 문제의 정해에 대한 힌트를 얻었습니다. 오래 전에 트위터에서 다른 리듬 게임인 Cytus를 소재로 목표 점수를 입력하면 그 점수를 정확히 맞추는 플레이를 출력하는 콘솔 프로그램을 봤던 것도 어느 정도 영향이 있었습니다.</p>

<p>다 쓰고 나서 Arcaea라는 배경지식을 지우니 "그게 뭔데" 싶은 문제가 되어버렸고, 구글 검색이나 solved.ac 난이도 기여에서도 "문제의 의도를 모르겠다"는 반응이 어느 정도 보여서 정말 아쉽습니다. 노트 개수 제한을 20,000이 아니라 22,361로 뒀으면 의도가 더 드러났을까 싶네요.</p>

<h3 id="출제자가-의도한-풀이">출제자가 의도한 풀이</h3>

<p>먼저 출력 설명의 "가능한 (2<em>a</em>+2<em>b</em>+<em>c</em>, <em>a</em>)의 쌍이 여러 개인 입력은 없다"와 바로 위에서 언급한 "[\(N\)의 제한을] 22,361로 뒀으면 의도가 더 드러났을까 싶네요"에 주목해야 하는데, 22,361은 위에서 제시한 2,237과 수상하게 비슷해 보이는 데서 알 수 있듯이 <strong>모든 노트를 Great 이상으로 처리해야 1,000,000,000점을 달성할 수 있는 최대 노트 수</strong>입니다. 이게 풀이랑 무슨 관련이 있냐고요?</p>

<p>문제에서 Good 판정을 일종의 단위로 사용한 것이 또 하나의 힌트가 되는데, 최종 점수 식을 다음과 같이 두 부분으로 분리할 수 있습니다.</p>

<ul>
  <li>\(10^9 \times \frac{2a + 2b + c}{2N}\) (Good 단위/"큰 단위")</li>
  <li>\(a\) ("작은 단위")</li>
</ul>

<p>모든 점수를 \(a \; \mathrm{Good} + b\)로 적고 \(a\)를 특정한 값으로 고정했을 때 가능한 점수의 집합을 "띠"라고 합시다. 그러면 주어진 조건 내에서 <strong>서로 다른 어떤 띠끼리도 겹치지 않는다</strong>는 것을 알 수 있습니다.</p>

<p>더욱 부연 설명을 하자면, 큰 단위의 점수가 \(a\)일 때 (작은 단위의 점수를 받을 수 있는 유일한 판정인) Perfect의 최대 개수는 \(\lfloor \frac{a}{2} \rfloor\)입니다. 여기서 이 띠가 가질 수 있는 작은 단위의 범위가 \(0 \le b \le \lfloor \frac{a}{2} \rfloor\)이 됨을 알 수 있습니다.</p>

<p>그런데 1 Good = \(\frac{10^9}{2N}\)이므로 작은 단위의 범위가 이 값을 넘어가면 띠끼리 겹치는 상황이 생깁니다. 문제의 범위를 대입하면 1 Good = 25,000, \(\lfloor \frac{a}{2} \rfloor \le 10000\)이므로 그런 일은 발생하지 않습니다. 그래서 이게 풀이랑 무슨 관련이 있냐고요?</p>

<p>이제 띠끼리 겹치지 않는다는 사실을 알았으니 점수의 작은 단위를 신경쓰지 않고 <strong>큰 단위를 먼저 구할 수 있고</strong>, 그 다음에 작은 단위를 구할 수 있습니다. 가능한 큰 단위는 최대 하나뿐이므로 테스트 케이스당 \(O(1)\)에 답을 구할 수 있습니다.</p>

<p><a href="https://www.acmicpc.net/source/38169965">제가 의도한 C11 풀이</a>(정답자만 열람 가능합니다)를 보면 사칙연산만을 사용해 큰 단위와 작은 단위를 하나씩 구하고, 점수가 띠 안에 들어가는지 확인하는 로직 한 번으로 테스트 케이스가 끝나는 것을 볼 수 있습니다.</p>

<p>아무튼 정해가 배배 꼬인 것도, 힌트가 너무 꽁꽁 숨겨져있던 것도 부정할 수는 없을 것 같습니다. 출제를 하면서 <strong>#애드 혹</strong>을 붙일지 말지 고민했는데, 지금 알고리즘 분류를 봐도 의외로 애드 혹은 없네요.</p>

<h3 id="문제-여담">문제 여담</h3>

<p>문제에 나온 것과 같이 실제로 저도 취미로 리듬 게임을 하고(현재는 휴대폰 액정 문제로 Arcaea는 하지 못하고, 대신 사운드 볼텍스를 가장 자주 합니다), "정확히 123,456,789점을 획득해 사람들을 놀라게 하고 싶"은 생각도(<del>생각만</del>) 가끔씩 합니다. <strong>당연히 실제 잇창명은 모든 노트의 판정을 원하는 대로 조절할 수 있는 초고수 플레이어가 아닙니다.</strong></p>

<h1 id="cpc-2024">CPC 2024</h1>

<p><a href="https://www.acmicpc.net/category/1054">2024 중앙대학교 프로그래밍 경진대회</a>에 세 문제(교내 출제 1문제, 오픈 콘테스트 전용 2문제)를 출제하고, 여섯 문제에 삽화를 제공했습니다.</p>

<h2 id="32175번-컵-쌓기-c1번-교내-출제"><a href="https://www.acmicpc.net/problem/32175">32175번 "컵 쌓기"</a> (C1번, 교내 출제)</h2>

<blockquote>
  <p><strong>문제</strong></p>

  <p>주식회사 푸앙의 추종자인 잇창명은 푸앙에서 생산하는 \(N\)종류의 컵을 가지고 있다. 이 회사에서 생산하는 컵은 쉽게 정리할 수 있도록 종류에 상관없이 컵의 입구 사이에 빈틈이 없도록 포개어진다는 장점이 있다. 편의상, 이 문제에서는 컵의 몸통 부분을 제외한 입구 부분만 생각하기로 하자.</p>

  <p><img src="/assets/post-images/cup-stacking-anatomy.png" alt="높이가 다른 두 컵이 나란히 세워져 있다. 아래쪽 몸통 부분의 크기는 같지만, 입구 부분의 높이가 다르다." /></p>

  <p>가지고 있는 컵을 포개어 정리하던 잇창명은 문득 컵의 입구 부분의 높이 총합이 \(H\)가 되도록 컵을 포갤 수 있는 경우의 수가 궁금해졌다. 모든 종류의 컵은 무한히 많이 있으며, 각 종류의 컵은 입구의 높이가 정해진 단위 높이 \(1\)의 양의 정수배에 해당한다. 또한, 컵을 포갤 때는 입구가 위로 오도록 포개어야 한다.</p>

  <p>아래와 같이 입구의 높이가 각각 \(1\), \(1\), \(2\), \(3\)인 컵 세트를 사용해 구체적인 예시를 들어 보자.</p>

  <p><img src="/assets/post-images/cup-stacking.png" alt="네 종류의 컵이 일렬로 세워져 있다. 입구 부분의 높이가 차례대로 1칸, 1칸, 2칸, 3칸이다." /></p>

  <p>위의 네 종류의 컵을 입구 부분의 높이가 \(10\)이 되도록 쌓는 방법은 아래의 그림 이외에도 여러 가지가 있으며, 그 경우의 수를 모두 합하면 \(9\,003\)가지이다.</p>

  <p><img src="/assets/post-images/cup-stacking-samples.png" alt="위의 네 종류의 컵을 입구 부분의 높이가 10칸이 되도록 여러 가지 방법으로 쌓은 모습" /></p>

  <p>잇창명을 위해 컵을 포개는 경우의 수를 구하는 프로그램을 작성해 보자.</p>

  <p><strong>입력</strong></p>

  <p>첫 번째 줄에 \(N\)과 \(H\)가 주어진다.</p>

  <p>두 번째 줄에 \(N\)종류의 컵의 높이 \(A_1\), \(A_2\), \(A_3\), \(\cdots\), \(A_N\)이 공백으로 구분되어 주어진다.</p>

  <p><strong>출력</strong></p>

  <p>첫 번째 줄에 높이가 정확히 \(H\)가 되도록 컵을 포개는 경우의 수를 \(1\,000\,000\,007\)(\(= 10^9+7\))로 나눈 나머지를 출력한다.</p>

  <p><strong>제한</strong></p>

  <ul>
    <li>\(1 \le N \le 100\)</li>
    <li>\(1 \le H \le 100\,000\), \(H\)는 정수</li>
    <li>\(1 \le A_i \le 100\)</li>
    <li>\(1 \le i \le N\)</li>
  </ul>

  <p><strong>예제 입력 1</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>2 3
1 2
</code></pre></div>  </div>

  <p><strong>예제 출력 1</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>3
</code></pre></div>  </div>

  <p><strong>예제 입력 2</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>4 10
1 1 2 3
</code></pre></div>  </div>

  <p><strong>예제 출력 2</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>9003
</code></pre></div>  </div>
</blockquote>

<p>2024년 9월 2일 현재 이 문제의 난이도는 실버 1이며, 알고리즘 분류는 <strong>#다이나믹 프로그래밍</strong>입니다.</p>

<p>생각해보면 동적 계획법이 정해인 문제 중 점화식이 입력 데이터에 따라 동적으로 바뀌는 문제를 거의 못 본 것 같아서 기획한 문제입니다. 실제로 그런 문제가 많은데 제가 기억을 못 하는 것일 수도 있지만 일단 출제 자체는 기획 의도에 맞게 잘 되긴 했습니다.</p>

<p>0개 이상의 컵을 쌓은 것을 "컵탑"이라고 할 때, 컵탑의 높이 \(H\)에 따라 점화식 \(B_H\)를 다음과 같이 구성할 수 있습니다.</p>

<ul>
  <li>\(H &lt; 0\): 높이가 음수가 되게 쌓는 것은 당연히 불가능하므로 \(B_H = 0\)가지입니다.</li>
  <li>\(H = 0\): 베이스 케이스에 해당합니다. 컵을 0개 쌓는 \(B_H = 1\)가지 방법이 있습니다.</li>
  <li>\(H &gt; 0\): 기존에 쌓은 컵탑 위에 높이가 \(A_i\)인 \(i\)번 컵을 쌓을 수 있습니다.
    <ul>
      <li>높이가 \(A_i\)인 컵을 추가로 쌓아서 쌓인 컵탑의 높이를 \(H\)로 만들려면 쌓기 이전에 컵탑의 높이는 \(H - A_i\)여야 합니다. 즉, \(B_{H - A_i}\)가지 방법이 가능합니다.</li>
      <li>데이터에서 제시된 모든 컵에 대한 경우의 수를 합합니다.</li>
    </ul>
  </li>
</ul>

<p>즉, 수식으로 정리하면 다음과 같습니다.</p>

\[B_H = \begin{cases} 
	0 &amp; H &lt; 0 \\
	1 &amp; H = 0 \\
	\Sigma_{i = 1}^N B_{H - A_i} &amp; H &gt; 0
\end{cases}\]

<p>정해에서는 \(H &lt; 0\)인 경우를 생략하고 최근 100개 항만 메모리에 유지하고 있습니다(\(A_i \le 100\)이므로).</p>

<h2 id="32180번-매달린-else-e1번-오픈-콘테스트-전용"><a href="https://www.acmicpc.net/problem/32180">32180번 "매달린 else"</a> (E1번, 오픈 콘테스트 전용)</h2>

<blockquote>
  <p><strong>문제</strong></p>

  <p>아래의 <code class="language-plaintext highlighter-rouge">else</code>문은 둘 중 어느 <code class="language-plaintext highlighter-rouge">if</code>문에 대응하는 <code class="language-plaintext highlighter-rouge">else</code>문일까?</p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if(a) if(b) c; else d;
</code></pre></div>  </div>

  <p>별도로 규칙을 추가하지 않는다면 컴퓨터는 이 <code class="language-plaintext highlighter-rouge">else</code>가 두 <code class="language-plaintext highlighter-rouge">if</code> 중 어느 쪽에 대응하는지 알 수 없다. 이 문제가 컴파일러에 관한 강의에서 들어보았을 매달린 else(dangling else) 문제이다.</p>

  <p>이 문제를 해결하는 방법은 여러 가지가 있지만, 가장 대표적인 방법은 아래의 두 가지이다.</p>

  <ol>
    <li>모호한 <code class="language-plaintext highlighter-rouge">else</code>를 짝지을 수 있는 가장 가까운 <code class="language-plaintext highlighter-rouge">if</code>와 짝짓는다.</li>
    <li>문법적으로 중괄호 생략을 금지한다.</li>
  </ol>

  <p>이 문제에서는 <strong>1번 규칙</strong>을 사용하는 소스 코드를 입력받아 <strong>2번 규칙</strong>을 사용하는 소스 코드로 변환해야 한다.</p>

  <p>문제에서 사용할 소스 코드의 문법은 다음과 같다.</p>

  <ul>
    <li>소스 코드에서 사용하는 <strong>토큰</strong>은 세미콜론("<code class="language-plaintext highlighter-rouge">;</code>"), 여는 중괄호("<code class="language-plaintext highlighter-rouge">{</code>"), 닫는 중괄호("<code class="language-plaintext highlighter-rouge">}</code>"), "<code class="language-plaintext highlighter-rouge">if</code>", "<code class="language-plaintext highlighter-rouge">else</code>", "<code class="language-plaintext highlighter-rouge">end</code>"로 총 6가지이다.</li>
    <li><strong>소스 코드</strong>는 \(0\)개 이상의 구문과 하나의 "<code class="language-plaintext highlighter-rouge">end</code>" 토큰으로 이루어진다.</li>
    <li>각 <strong>구문</strong>은 아래의 셋 중 하나에 해당한다.
      <ul>
        <li>하나의 세미콜론("<code class="language-plaintext highlighter-rouge">;</code>") 토큰</li>
        <li>하나의 <code class="language-plaintext highlighter-rouge">if</code>문</li>
        <li>하나의 <code class="language-plaintext highlighter-rouge">if</code>-<code class="language-plaintext highlighter-rouge">else</code>문</li>
      </ul>
    </li>
    <li><strong><code class="language-plaintext highlighter-rouge">if</code>문</strong>은 "<code class="language-plaintext highlighter-rouge">if</code>" 토큰 하나와 본문 하나가 연이어 있는 형태를 가진다.</li>
    <li><strong><code class="language-plaintext highlighter-rouge">if</code>-<code class="language-plaintext highlighter-rouge">else</code>문</strong>은 "<code class="language-plaintext highlighter-rouge">if</code>" 토큰 하나, 본문 하나, "<code class="language-plaintext highlighter-rouge">else</code>" 토큰 하나, 본문 하나가 연이어 있는 형태를 가진다.</li>
    <li><strong>본문</strong>은 위에서 정의한 두 규칙 중 어느 것을 사용하느냐에 따라 다르게 구성될 수 있다.
      <ul>
        <li>1번 규칙을 사용하는 경우, 본문은 구문 하나로 구성될 수 있다.</li>
        <li>1번 규칙 또는 2번 규칙을 사용하는 경우, 본문은 여는 중괄호("<code class="language-plaintext highlighter-rouge">{</code>") 토큰 하나, \(0\)개 이상의 구문, 닫는 중괄호("<code class="language-plaintext highlighter-rouge">}</code>") 토큰이 연이어 있는 형태를 가질 수 있다.</li>
      </ul>
    </li>
    <li>모든 토큰은 공백 문자 \(1\)개를 사이에 두고 구분한다.</li>
  </ul>

  <p>또한, 문법 요소 간의 <strong>동형 관계</strong>는 다음과 같이 주어진다.</p>

  <ul>
    <li>두 <strong>소스 코드</strong>는 구문 수가 같고 각 구문끼리 순서대로 대응시켰을 때 동형일 경우 동형이다.</li>
    <li>두 <strong>구문</strong>은 다음 중 하나에 해당하면 동형이다.
      <ul>
        <li>두 구문이 모두 단일 세미콜론("<code class="language-plaintext highlighter-rouge">;</code>") 토큰일 경우</li>
        <li>두 구문이 모두 <code class="language-plaintext highlighter-rouge">if</code>문이며 서로 동형일 경우</li>
        <li>두 구문이 모두 <code class="language-plaintext highlighter-rouge">if</code>-<code class="language-plaintext highlighter-rouge">else</code>문이며 서로 동형일 경우</li>
      </ul>
    </li>
    <li>두 <strong><code class="language-plaintext highlighter-rouge">if</code>문</strong>은 그 본문끼리 동형일 경우 동형이다.</li>
    <li>두 <strong><code class="language-plaintext highlighter-rouge">if</code>-<code class="language-plaintext highlighter-rouge">else</code>문</strong>은 두 본문을 순서대로 대응시켰을 때 동형일 경우 동형이다.</li>
    <li>두 <strong>본문</strong>은 다음 중 하나에 해당하면 동형이다.
      <ul>
        <li>두 본문이 모두 중괄호가 있으며, 구문 수가 같고 각 구문끼리 순서대로 대응시켰을 때 동형일 경우</li>
        <li>두 본문이 모두 중괄호의 유무와 상관 없이 단일 구문이며, 구문이 서로 동형일 경우</li>
      </ul>
    </li>
  </ul>

  <p>예를 들어, 1번 규칙을 고려하지 않는다면 소스 코드 <code class="language-plaintext highlighter-rouge">if { if ; } else ; end</code>와 소스 코드 <code class="language-plaintext highlighter-rouge">if if { ; } else { ; } end</code>는 동형이며, 그 이유를 다음과 같이 보일 수 있다.</p>

  <ul>
    <li>두 소스 코드 모두 단일 구문(<code class="language-plaintext highlighter-rouge">if { if ; } else ;</code>와 <code class="language-plaintext highlighter-rouge">if if { ; } else { ; }</code>)으로 이루어져 있으며, 두 구문 모두 <code class="language-plaintext highlighter-rouge">if</code>-<code class="language-plaintext highlighter-rouge">else</code>문에 해당한다.
      <ul>
        <li>첫 번째 본문(<code class="language-plaintext highlighter-rouge">{ if ; }</code>와 <code class="language-plaintext highlighter-rouge">if { ; }</code>)이 모두 단일 구문으로 이루어져 있다.
          <ul>
            <li>두 구문 모두 <code class="language-plaintext highlighter-rouge">if</code>문에 해당하며, 본문(<code class="language-plaintext highlighter-rouge">;</code>와 <code class="language-plaintext highlighter-rouge">{ ; }</code>)이 서로 일치하는 단일 구문으로 이루어져 있다.</li>
          </ul>
        </li>
        <li>두 번째 본문(<code class="language-plaintext highlighter-rouge">;</code>와 <code class="language-plaintext highlighter-rouge">{ ; }</code>)이 서로 일치하는 단일 구문으로 이루어져 있다.</li>
      </ul>
    </li>
  </ul>

  <p>여러분에게 1번 규칙을 사용하는 소스 코드 \(X\)가 주어진다. 여러분은 이 소스 코드와 동형이면서 2번 규칙을 사용하는 소스 코드 \(X'\)를 찾아 출력해야 한다. 문제에서 주어진 소스 코드의 문법을 따를 때 그러한 소스 코드 \(X'\)는 유일함을 증명할 수 있다.</p>

  <p><strong>입력</strong></p>

  <p>첫 번째 줄에 문제에서 제시된 입력 문법을 만족하는 문자열이 주어진다. 토큰의 개수는 "<code class="language-plaintext highlighter-rouge">end</code>"를 포함해서 최대 \(1\,000\)개이며, 입력의 앞뒤에 불필요한 공백이 주어지지 않는다.</p>

  <p>형식적인 입력 문법은 아래와 같다. 이때 <code class="language-plaintext highlighter-rouge">&lt;input_stmt&gt;*</code>는 <code class="language-plaintext highlighter-rouge">&lt;input_stmt&gt;</code>가 \(0\)개 이상 올 수 있다는 의미이다.</p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;input&gt; ::= &lt;input_stmt&gt;* "end"
&lt;input_block&gt; ::= &lt;input_stmt&gt; | "{" &lt;input_stmt&gt;* "}"
&lt;input_stmt&gt; ::= ";" | &lt;input_if&gt;
&lt;input_if&gt; ::= "if" &lt;input_block&gt; | "if" &lt;input_block&gt; "else" &lt;input_block&gt;
</code></pre></div>  </div>

  <p><strong>출력</strong></p>

  <p>첫 번째 줄에 입력받은 프로그램을 파싱한 결과를 문제에서 제시된 출력 문법을 만족하도록 출력한다.</p>

  <p>형식적인 출력 문법은 아래와 같다. 이는 위의 <code class="language-plaintext highlighter-rouge">&lt;input_block&gt;</code>에서 <code class="language-plaintext highlighter-rouge">&lt;input_stmt&gt;</code>만 제거한 것과 동일하다.</p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>&lt;output&gt; ::= &lt;output_stmt&gt;* "end"
&lt;output_block&gt; ::= "{" &lt;output_stmt&gt;* "}"
&lt;output_stmt&gt; ::= ";" | &lt;output_if&gt;
&lt;output_if&gt; ::= "if" &lt;output_block&gt; | "if" &lt;output_block&gt; "else" &lt;output_block&gt;
</code></pre></div>  </div>

  <p><strong>예제 입력 1</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if if ; else ; end
</code></pre></div>  </div>

  <p><strong>예제 출력 1</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if { if { ; } else { ; } } end
</code></pre></div>  </div>

  <p><strong>예제 입력 2</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if { if ; } else ; end
</code></pre></div>  </div>

  <p><strong>예제 출력 2</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if { if { ; } } else { ; } end
</code></pre></div>  </div>

  <p><strong>예제 입력 3</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if { if ; else ; ; ; } end
</code></pre></div>  </div>

  <p><strong>예제 출력 3</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if { if { ; } else { ; } ; ; } end
</code></pre></div>  </div>

  <p><strong>예제 입력 4</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if if if if if if ; end
</code></pre></div>  </div>

  <p><strong>예제 출력 4</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>if { if { if { if { if { if { ; } } } } } } end
</code></pre></div>  </div>

  <p><strong>예제 입력 5</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>end
</code></pre></div>  </div>

  <p><strong>예제 출력 5</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>end
</code></pre></div>  </div>

  <p><strong>예제 입력 6</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>; ; ; ; ; ; ; ; end
</code></pre></div>  </div>

  <p><strong>예제 출력 6</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>; ; ; ; ; ; ; ; end
</code></pre></div>  </div>

  <p><strong>예제 입력 7</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>; if ; else { } if { ; } if { } else ; ; end
</code></pre></div>  </div>

  <p><strong>예제 출력 7</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>; if { ; } else { } if { ; } if { } else { ; } ; end
</code></pre></div>  </div>

  <p><strong>노트</strong></p>

  <p>사실 C에는 <code class="language-plaintext highlighter-rouge">else if</code>문이 따로 없다는 사실을 알고 있는가? 우리가 <code class="language-plaintext highlighter-rouge">else if</code>라고 알고 있는 구문은 사실 일반적인 <code class="language-plaintext highlighter-rouge">else</code>의 본문에서 중괄호를 생략하고 <code class="language-plaintext highlighter-rouge">if</code>문으로 채운 경우에 해당한다.</p>
</blockquote>

<p>2024년 9월 2일 현재 이 문제의 난이도는 플래티넘 5이며, 알고리즘 분류는 <strong>#자료 구조</strong> <strong>#문자열</strong> <strong>#파싱</strong> <strong>#스택</strong> <strong>#재귀</strong>입니다.</p>

<p><strong>이 문제만큼 난이도 예측에 실패한 적이 없을 것 같습니다.</strong> <a href="https://www.acmicpc.net/problem/4949">4949번 "균형잡힌 세상"</a>이나 <a href="https://www.acmicpc.net/problem/2504">2504번 "괄호의 값"</a> 등의 단순 심화 버전으로 기획하고 실버 중상위권 정도의 난이도를 예상했는데, 막상 뚜껑을 열어보니 플래티넘 턱걸이를 하는 문제가 나왔습니다. 추가로 문제 자체가 검수하기 어려운 형태이고 지문도 엄밀하게 작성해야 하다 보니 "다시는 이런 문제 내지 말아야지" 하는 다짐을 유발하기도 했습니다.</p>

<p>위의 괄호 문자열 문제에서 현재 읽고 있는 위치(<code class="language-plaintext highlighter-rouge">|</code>)를 다음과 같이 열거형으로 표현할 수 있었듯이...</p>

<ul>
  <li>소괄호 안 (<code class="language-plaintext highlighter-rouge">( ... | ... )</code>)</li>
  <li>대괄호 안 (<code class="language-plaintext highlighter-rouge">[ ... | ... ]</code>)</li>
</ul>

<p>이 문제에서도 다음과 같은 열거형을 정의해 스택으로 관리할 수 있습니다. 설명의 편의상 이름을 붙이겠습니다.</p>

<ul>
  <li><strong><code class="language-plaintext highlighter-rouge">S_IF</code></strong>: <code class="language-plaintext highlighter-rouge">if</code>문/<code class="language-plaintext highlighter-rouge">if</code>-<code class="language-plaintext highlighter-rouge">else</code>문의 첫 번째 본문 이전 (<code class="language-plaintext highlighter-rouge">if | ...</code> 혹은 <code class="language-plaintext highlighter-rouge">if | ... else ...</code>)</li>
  <li><strong><code class="language-plaintext highlighter-rouge">S_THEN</code></strong>: <code class="language-plaintext highlighter-rouge">if</code>문/<code class="language-plaintext highlighter-rouge">if</code>-<code class="language-plaintext highlighter-rouge">else</code>문의 첫 번째 본문 이후 (<code class="language-plaintext highlighter-rouge">if ... |</code> 혹은 <code class="language-plaintext highlighter-rouge">if ... | else ...</code>)</li>
  <li><strong><code class="language-plaintext highlighter-rouge">S_ELSE</code></strong>: <code class="language-plaintext highlighter-rouge">if</code>-<code class="language-plaintext highlighter-rouge">else</code>의 두 번째 본문 이전 (<code class="language-plaintext highlighter-rouge">if ... else | ...</code>)</li>
  <li><strong><code class="language-plaintext highlighter-rouge">S_BLOCK</code></strong>: 중괄호 안 (<code class="language-plaintext highlighter-rouge">{ ... | ... }</code>)</li>
</ul>

<p>그런 다음에는 토큰을 읽을 때마다 다음과 같이 스택 갱신과 출력을 병행하면 됩니다. "스택의 가장 위에 토큰 X가 있을 때"는 글 작성의 편의상 "X 상태일 때"로 축약합니다.</p>

<ul>
  <li>공통 사항
    <ul>
      <li><code class="language-plaintext highlighter-rouge">S_IF</code>나 <code class="language-plaintext highlighter-rouge">S_ELSE</code> 상태(<em>본문이 올 자리</em>)일 경우 다음 토큰으로는 본문을 구성할 수 있는 토큰만 올 수 있습니다.</li>
      <li><code class="language-plaintext highlighter-rouge">S_THEN</code> 상태에서 <code class="language-plaintext highlighter-rouge">else</code> 토큰이 오지 않을 경우에는 <code class="language-plaintext highlighter-rouge">if</code>-<code class="language-plaintext highlighter-rouge">else</code>문이 아니므로 <code class="language-plaintext highlighter-rouge">S_THEN</code>을 정리해야 합니다(<em><code class="language-plaintext highlighter-rouge">if</code>문 정리</em>).
        <ul>
          <li>처리해야 할 <code class="language-plaintext highlighter-rouge">S_THEN</code>이 여러 겹 있을 수 있음에 유의해야 합니다.</li>
        </ul>
      </li>
      <li>본문이 열릴 때와 닫힐 때는 특수한 처리(<em>본문 여닫기</em>)가 필요합니다. 특히 중괄호가 생략됐는지 판단하여 추가로 출력하고, 본문이 닫힐 때 <code class="language-plaintext highlighter-rouge">S_IF</code>는 <code class="language-plaintext highlighter-rouge">S_THEN</code>으로 갱신, <code class="language-plaintext highlighter-rouge">S_ELSE</code>는 뽑아야 합니다.
        <ul>
          <li><em><code class="language-plaintext highlighter-rouge">if</code>문 정리</em> 도중에도 <em>본문 닫기</em> 판정이 생길 수 있음에 유의해야 합니다.</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>세미콜론(<code class="language-plaintext highlighter-rouge">;</code>)
    <ul>
      <li><em>본문이 올 자리</em>일 경우 <em>본문 여닫기</em>를 합니다.</li>
      <li>그렇지 않으면 세미콜론이 일반 구문이므로 <em><code class="language-plaintext highlighter-rouge">if</code>문을 정리</em>합니다.</li>
    </ul>
  </li>
  <li>여는 중괄호(<code class="language-plaintext highlighter-rouge">{</code>)
    <ul>
      <li>이 토큰은 본문의 첫 토큰으로만 등장할 수 있습니다. <em>본문 열기</em>를 하고 스택에 <code class="language-plaintext highlighter-rouge">S_BLOCK</code>을 넣습니다.</li>
    </ul>
  </li>
  <li>닫는 중괄호(<code class="language-plaintext highlighter-rouge">}</code>)
    <ul>
      <li>이 토큰은 본문의 마지막 토큰으로만 등장할 수 있습니다. <em><code class="language-plaintext highlighter-rouge">if</code>문을 정리</em>하고 스택의 <code class="language-plaintext highlighter-rouge">S_BLOCK</code>을 꺼낸 뒤 <em>본문 닫기</em>를 합니다.</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">if</code>
    <ul>
      <li><em>본문이 올 자리</em>일 경우 <em>본문 열기</em>를 한 뒤 스택에 <code class="language-plaintext highlighter-rouge">S_IF</code>를 넣습니다.</li>
      <li>그렇지 않으면 <code class="language-plaintext highlighter-rouge">if</code>(-<code class="language-plaintext highlighter-rouge">else</code>)문이 일반 구문이므로 <em><code class="language-plaintext highlighter-rouge">if</code>문을 정리</em>하고 스택에 <code class="language-plaintext highlighter-rouge">S_IF</code>를 넣습니다.</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">else</code>
    <ul>
      <li>이 토큰은 본문의 바로 다음에만 등장할 수 있습니다. 스택의 <code class="language-plaintext highlighter-rouge">S_THEN</code>을 <code class="language-plaintext highlighter-rouge">S_ELSE</code>로 갱신합니다.</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">end</code>
    <ul>
      <li>입력의 종료를 나타내는 토큰입니다. <em><code class="language-plaintext highlighter-rouge">if</code>문을 정리</em>합니다.</li>
    </ul>
  </li>
</ul>

<p>여기까지가 제가 의도한 대회 중에 작성할 수 있는 풀이였고, <a href="https://en.wikipedia.org/wiki/Dangling_else#Avoiding_the_conflict_in_LR_parsers">모종의 방법</a>을 이용하면 문제에서 제시하는 언어(문법이 아닙니다!)<sup id="fnref:fn-grammer-vs-language" role="doc-noteref"><a href="#fn:fn-grammer-vs-language" class="footnote" rel="footnote">5</a></sup>를 LR 파싱할 수 있으니 관심이 있다면 도전해보시는 것도 좋겠습니다. 실제로 데이터 검증 코드가 이렇게 구현되어 있습니다.</p>

<h3 id="문제-여담-1">문제 여담</h3>

<p>사실 C에서는 <code class="language-plaintext highlighter-rouge">0</code>이 8진수라는 것도 알고 계셨나요? C17 표준의 최종안인 <a href="https://files.lhmouse.com/standards/ISO%20C%20N2176.pdf#subsection.6.4.4">ISO C N2176</a>에서는 정수 상수의 문법이 다음과 같이 정의되어 있습니다. <sub>opt</sub>는 바로 앞의 문법 요소가 있어도 되고, 없어도 된다는 의미입니다.</p>

<ul>
  <li><em>integer-constant</em>:
    <ul>
      <li><em>decimal-constant</em> <em>integer-suffix</em><sub>opt</sub></li>
      <li><em>octal-constant</em> <em>integer-suffix</em><sub>opt</sub></li>
      <li><em>hexadecimal-constant</em> <em>integer-suffix</em><sub>opt</sub></li>
    </ul>
  </li>
  <li><em>decimal-constant</em>:
    <ul>
      <li><em>nonzero-digit</em></li>
      <li><em>decimal-constant</em> <em>digit</em></li>
    </ul>
  </li>
  <li><em>octal-constant</em>:
    <ul>
      <li><strong><code class="language-plaintext highlighter-rouge">0</code></strong></li>
      <li><em>octal-constant</em> <em>octal-digit</em></li>
    </ul>
  </li>
  <li>...</li>
  <li><em>nonzero-digit</em>: one of
    <ul>
      <li><strong><code class="language-plaintext highlighter-rouge">1</code></strong> <strong><code class="language-plaintext highlighter-rouge">2</code></strong> <strong><code class="language-plaintext highlighter-rouge">3</code></strong> <strong><code class="language-plaintext highlighter-rouge">4</code></strong> <strong><code class="language-plaintext highlighter-rouge">5</code></strong> <strong><code class="language-plaintext highlighter-rouge">6</code></strong> <strong><code class="language-plaintext highlighter-rouge">7</code></strong> <strong><code class="language-plaintext highlighter-rouge">8</code></strong> <strong><code class="language-plaintext highlighter-rouge">9</code></strong></li>
    </ul>
  </li>
</ul>

<p>10진 상수(<em>decimal-constant</em>)는 0으로 시작할 수 없고(<em>nonzero-digit</em>), 0으로 시작하는 것은 모두 8진 상수(<em>octal-constant</em>)인 것으로 정의되어 있습니다. <code class="language-plaintext highlighter-rouge">0</code>도 예외는 아닙니다.</p>

<h2 id="32183번-바이러스-f1번-오픈-콘테스트-전용"><a href="https://www.acmicpc.net/problem/32183">32183번 "바이러스"</a> (F1번, 오픈 콘테스트 전용)</h2>

<blockquote>
  <p><strong>문제</strong></p>

  <p>신종 컴퓨터 바이러스가 전 세계를 휩쓸고 있다! 바이러스를 분석하고 치료하는 데 당신의 도움이 필요하다.</p>

  <p>현재 가용한 정보를 취합해서 얻어낸 바이러스의 비트 패턴은 정규 표현식으로 다음과 같다.</p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>(1(10)+1*|0+10)+
</code></pre></div>  </div>

  <p>정규 표현식은 문자열에 여러 가지 연산을 추가해 원하는 패턴을 기술할 수 있게 한 것으로, 다음과 같이 읽으면 된다.</p>

  <ul>
    <li>비트를 그대로 적으면 정확히 일치하는 한 글자의 비트열을 찾는다.
      <ul>
        <li><code class="language-plaintext highlighter-rouge">0</code> = { <code class="language-plaintext highlighter-rouge">"0"</code> }</li>
        <li><code class="language-plaintext highlighter-rouge">1</code> = { <code class="language-plaintext highlighter-rouge">"1"</code> }</li>
      </ul>
    </li>
    <li>모든 정규 표현식 <code class="language-plaintext highlighter-rouge">x</code>와 <code class="language-plaintext highlighter-rouge">y</code>에 대해 다음 연산이 성립한다.
      <ul>
        <li>괄호를 씌운 <code class="language-plaintext highlighter-rouge">(x)</code>는 <code class="language-plaintext highlighter-rouge">x</code>와 일치하는 비트열을 동일하게 찾는다.
          <ul>
            <li><code class="language-plaintext highlighter-rouge">(0)</code> = { <code class="language-plaintext highlighter-rouge">"0"</code> }</li>
            <li><code class="language-plaintext highlighter-rouge">(1)</code> = { <code class="language-plaintext highlighter-rouge">"1"</code> }</li>
          </ul>
        </li>
        <li>연산자 없이 두 정규 표현식을 연결한 <code class="language-plaintext highlighter-rouge">xy</code>는 두 정규 표현식과 일치하는 비트열 중 하나씩을 순서대로 연결한 비트열을 찾는다.
          <ul>
            <li><code class="language-plaintext highlighter-rouge">000</code> = { <code class="language-plaintext highlighter-rouge">"000"</code> }</li>
            <li><code class="language-plaintext highlighter-rouge">10010110</code> = { <code class="language-plaintext highlighter-rouge">"10010110"</code> }</li>
          </ul>
        </li>
        <li><code class="language-plaintext highlighter-rouge">x|y</code>는 <code class="language-plaintext highlighter-rouge">x</code>와 일치하는 비트열과 <code class="language-plaintext highlighter-rouge">y</code>와 일치하는 비트열 중 아무 비트열을 찾는다.
          <ul>
            <li><code class="language-plaintext highlighter-rouge">0|1</code> = { <code class="language-plaintext highlighter-rouge">"0"</code>, <code class="language-plaintext highlighter-rouge">"1"</code> }</li>
            <li><code class="language-plaintext highlighter-rouge">0(0|1)1</code> = { <code class="language-plaintext highlighter-rouge">"001"</code>, <code class="language-plaintext highlighter-rouge">"011"</code> }</li>
            <li><code class="language-plaintext highlighter-rouge">1|111|11111</code> = { <code class="language-plaintext highlighter-rouge">"1"</code>, <code class="language-plaintext highlighter-rouge">"111"</code>, <code class="language-plaintext highlighter-rouge">"11111"</code> }</li>
          </ul>
        </li>
        <li><code class="language-plaintext highlighter-rouge">x*</code>는 <code class="language-plaintext highlighter-rouge">x</code>와 일치하는 비트열이 \(0\)개 이상 연결되어 있는 비트열을 찾는다. 즉, <code class="language-plaintext highlighter-rouge">x*</code>는 <code class="language-plaintext highlighter-rouge">(빈 문자열)|x|xx|xxx|...</code>와 같은 의미를 가진다.
          <ul>
            <li><code class="language-plaintext highlighter-rouge">0*</code> = { <code class="language-plaintext highlighter-rouge">""</code>, <code class="language-plaintext highlighter-rouge">"0"</code>, <code class="language-plaintext highlighter-rouge">"00"</code>, <code class="language-plaintext highlighter-rouge">"000"</code>, … }</li>
            <li><code class="language-plaintext highlighter-rouge">01*</code> = { <code class="language-plaintext highlighter-rouge">"0"</code>, <code class="language-plaintext highlighter-rouge">"01"</code>, <code class="language-plaintext highlighter-rouge">"011"</code>, <code class="language-plaintext highlighter-rouge">"0111"</code>, … }</li>
            <li><code class="language-plaintext highlighter-rouge">(01)*</code> = { <code class="language-plaintext highlighter-rouge">""</code>, <code class="language-plaintext highlighter-rouge">"01"</code>, <code class="language-plaintext highlighter-rouge">"0101"</code>, <code class="language-plaintext highlighter-rouge">"010101"</code>, … }</li>
            <li><code class="language-plaintext highlighter-rouge">111(01|001)*</code> = { <code class="language-plaintext highlighter-rouge">"111"</code>, <code class="language-plaintext highlighter-rouge">"11101"</code>, <code class="language-plaintext highlighter-rouge">"111001"</code>, <code class="language-plaintext highlighter-rouge">"1110101"</code>, <code class="language-plaintext highlighter-rouge">"11100100101"</code>, <code class="language-plaintext highlighter-rouge">"1110100100101"</code>, … }</li>
          </ul>
        </li>
        <li><code class="language-plaintext highlighter-rouge">x+</code>는 <code class="language-plaintext highlighter-rouge">x</code>와 일치하는 비트열이 \(1\)개 이상 연결되어 있는 비트열을 찾는다. 즉, <code class="language-plaintext highlighter-rouge">x+</code>는 <code class="language-plaintext highlighter-rouge">x|xx|xxx|...</code>와 같은 의미를 가진다.
          <ul>
            <li><code class="language-plaintext highlighter-rouge">0+</code> = { <code class="language-plaintext highlighter-rouge">"0"</code>, <code class="language-plaintext highlighter-rouge">"00"</code>, <code class="language-plaintext highlighter-rouge">"000"</code>, … }</li>
            <li><code class="language-plaintext highlighter-rouge">01+</code> = { <code class="language-plaintext highlighter-rouge">"01"</code>, <code class="language-plaintext highlighter-rouge">"011"</code>, <code class="language-plaintext highlighter-rouge">"0111"</code>, … }</li>
            <li><code class="language-plaintext highlighter-rouge">(01)+</code> = { <code class="language-plaintext highlighter-rouge">"01"</code>, <code class="language-plaintext highlighter-rouge">"0101"</code>, <code class="language-plaintext highlighter-rouge">"010101"</code>, … }</li>
            <li><code class="language-plaintext highlighter-rouge">(01+)+</code> = { <code class="language-plaintext highlighter-rouge">"01"</code>, <code class="language-plaintext highlighter-rouge">"011"</code>, <code class="language-plaintext highlighter-rouge">"01111"</code>, <code class="language-plaintext highlighter-rouge">"0111010101"</code>, <code class="language-plaintext highlighter-rouge">"01111011111011"</code>, … }</li>
          </ul>
        </li>
      </ul>
    </li>
  </ul>

  <p>연산자를 적용하는 순서는 다음과 같다. 예를 들어, <code class="language-plaintext highlighter-rouge">01|10**</code>는 <code class="language-plaintext highlighter-rouge">(01)|(1((0*)*))</code>와 같이 읽는다.</p>

  <ul>
    <li><code class="language-plaintext highlighter-rouge">(x)</code></li>
    <li><code class="language-plaintext highlighter-rouge">x*</code>, <code class="language-plaintext highlighter-rouge">x+</code>
      <ul>
        <li>두 연산자의 우선순위는 같다.</li>
      </ul>
    </li>
    <li><code class="language-plaintext highlighter-rouge">xy</code></li>
    <li><code class="language-plaintext highlighter-rouge">x|y</code></li>
  </ul>

  <p>특히 이 바이러스는 자신의 비트열을 계속 바꾸면서 예측할 수 없는 움직임을 보이기 때문에 비트열이 바뀔 때마다 빠른 연산과 탐지를 필요로 한다. 구체적으로 비트열이 다음과 같이 갱신될 수 있다.</p>

  <ul>
    <li>\(fg\;l\;r\): \(l\)번째 비트부터 \(r\)번째 비트까지가 다음과 같이 갱신된다. 비트 번호는 왼쪽에서 오른쪽으로 <strong>\(0\)부터</strong> 세며, \(l\)과 \(r\)이 모두 포함된다.
      <ul>
        <li>갱신 이전에 <code class="language-plaintext highlighter-rouge">0</code>이었던 비트는 \(f\)로 바뀐다.</li>
        <li>갱신 이전에 <code class="language-plaintext highlighter-rouge">1</code>이었던 비트는 \(g\)로 바뀐다.</li>
      </ul>
    </li>
  </ul>

  <p>미리 탐지한 메모리 구역에 해당하는 비트열이 주어지면, 해당 비트열이 갱신될 때마다 바이러스의 비트 패턴과 일치하는지 확인하는 프로그램을 작성하라.</p>

  <p><strong>입력</strong></p>

  <p>첫 번째 줄에 메모리 구역의 비트 수 \(N\)이 주어진다.</p>

  <p>두 번째 줄에 해당 메모리 구역의 초기 데이터 \(S\)가 숫자 <code class="language-plaintext highlighter-rouge">0</code>과 <code class="language-plaintext highlighter-rouge">1</code>만으로 이루어진 문자열로 주어진다.</p>

  <p>세 번째 줄에는 데이터가 갱신되는 횟수 \(Q\)가 주어진다.</p>

  <p>네 번째 줄부터 \(Q\)개의 줄에 걸쳐 데이터 갱신 정보가 한 줄에 하나씩, \(fg\;l\;r\) 형태로 주어진다. <strong>\(f\)와 \(g\) 사이에 공백 문자가 오지 않음</strong>에 유의하라.</p>

  <p><strong>출력</strong></p>

  <p>첫 번째 줄에 초기 비트열이 바이러스의 비트 패턴과 일치하면 <code class="language-plaintext highlighter-rouge">YES</code>를, 그렇지 않으면 <code class="language-plaintext highlighter-rouge">NO</code>를 출력한다.</p>

  <p>두 번째 줄부터 비트열이 갱신될 때마다 갱신된 비트열이 바이러스의 비트 패턴과 일치하면 <code class="language-plaintext highlighter-rouge">YES</code>를, 그렇지 않으면 <code class="language-plaintext highlighter-rouge">NO</code>를 각각 한 줄로 출력한다.</p>

  <p>출력하는 모든 문자는 대문자이며, 따옴표는 출력하지 않는다.</p>

  <p><strong>제한</strong></p>

  <ul>
    <li>\(1 \le N, Q \le 100\,000\)</li>
    <li>\(\vert S \vert = N\)</li>
    <li>모든 갱신은 \(f, g \in { 0, 1 }\)과 \(0 \le l \le r \le N - 1\)을 만족한다.</li>
  </ul>

  <p><strong>예제 입력 1</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>10
0010110101
4
10 0 4
10 7 9
11 0 5
00 4 9
</code></pre></div>  </div>

  <p><strong>예제 출력 1</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>YES
YES
YES
NO
NO
</code></pre></div>  </div>

  <p>주어진 메모리 구역의 비트열은 다음과 같이 갱신되었다.</p>

  <ul>
    <li><code class="language-plaintext highlighter-rouge">0010110101</code> (초기 데이터)</li>
    <li><code class="language-plaintext highlighter-rouge">1101010101</code> (1차 갱신, <code class="language-plaintext highlighter-rouge">10 0 4</code>)</li>
    <li><code class="language-plaintext highlighter-rouge">1101010010</code> (2차 갱신, <code class="language-plaintext highlighter-rouge">10 7 9</code>)</li>
    <li><code class="language-plaintext highlighter-rouge">1111110010</code> (3차 갱신, <code class="language-plaintext highlighter-rouge">11 0 5</code>)</li>
    <li><code class="language-plaintext highlighter-rouge">1111000000</code> (4차 갱신, <code class="language-plaintext highlighter-rouge">00 4 9</code>)</li>
  </ul>
</blockquote>

<p>2025년 1월 24일 현재 이 문제의 난이도는 다이아몬드 4이며, 알고리즘 분류는 <strong>#자료 구조</strong> <strong>#느리게 갱신되는 세그먼트 트리</strong> <strong>#정규 표현식</strong> <strong>#세그먼트 트리</strong> <strong>#문자열</strong>입니다.</p>

<p>이 문제를 구상하는 데 여러 문제의 영향을 받았습니다.</p>

<ul>
  <li>원래 <a href="https://www.acmicpc.net/problem/1013">1013번 "Contact"</a>/<a href="https://www.acmicpc.net/problem/2671">2671번 "잠수함식별"</a>의 지문을 그대로 차용한 쿼리 버전으로 기획했던 문제였습니다. 문제 출제 당시 커뮤니티 문제가 표절되는 이슈가 발생해 현재의 지문과 정규 표현식으로 변경했습니다.</li>
  <li><a href="https://www.acmicpc.net/problem/17407">17407번 "괄호 문자열과 쿼리"</a>와 <a href="https://www.acmicpc.net/problem/22854">22854번 "고장난 계산기 (Calculator) 게임"</a>의 영향을 받았으며, 특히 이 문제를 후자의 쉬운/접근성 있는 버전으로 포지셔닝하려는 의도도 어느 정도 있었습니다.</li>
</ul>

<p>이 문제를 해결하는 데는 여러 단계의 발상이 필요합니다.</p>

<h3 id="정규-표현식을-dfa로-변환">정규 표현식을 DFA로 변환</h3>

<p>이 문제를 해결하려면 정규 표현식을 DFA로 변환하는 일련의 흐름을 사전 지식으로 알고 있어야 합니다. 간단한 요약을 같이 작성합니다.</p>

<ul>
  <li>(형식언어론 맥락에서의) <strong><a href="https://en.wikipedia.org/wiki/Regular_expression#Formal_language_theory">정규 표현식</a></strong>
    <ul>
      <li>문제에서 정규 표현식의 문법 일부를 설명했는데, 정규 표현식으로 기술할 수 있는 언어를 따로 <em>정규 언어</em>라고 합니다. 문제에는 없지만 상황에 따라 정규 언어의 표현력 이내에서 다음 연산자나 문법을 추가로 도입하기도 합니다.
        <ul>
          <li>아무 문자열도 찾지 않는 공집합 정규 표현식. 관례상 <code class="language-plaintext highlighter-rouge">∅</code>으로 적습니다.</li>
          <li>빈 문자열 <code class="language-plaintext highlighter-rouge">""</code>을 찾는 정규 표현식. 관례상 <code class="language-plaintext highlighter-rouge">ε</code>으로 적습니다.</li>
          <li>0개 혹은 1개를 찾는 연산자 <code class="language-plaintext highlighter-rouge">x?</code>. <code class="language-plaintext highlighter-rouge">ε|x</code>와 같은 의미를 가집니다.</li>
          <li>집합에 속하는 한 문자를 찾는 문법 <code class="language-plaintext highlighter-rouge">[...]</code>. 예를 들어 <code class="language-plaintext highlighter-rouge">[abc]</code>는 <code class="language-plaintext highlighter-rouge">a|b|c</code>와 같은 의미를 가집니다.</li>
          <li>집합에 속하지 <em>않는</em> 한 문자를 찾는 문법 <code class="language-plaintext highlighter-rouge">[^...]</code></li>
        </ul>
      </li>
      <li>참고로 관례상 <code class="language-plaintext highlighter-rouge">x*</code>는 "클레이니 스타", <code class="language-plaintext highlighter-rouge">x+</code>는 "클레이니 플러스"라고 읽습니다.</li>
    </ul>
  </li>
  <li><strong><a href="https://en.wikipedia.org/wiki/Thompson's_construction">톰슨 구성</a></strong> (Thompson's construction)
    <ul>
      <li>정규 표현식을 아래에서 설명할 NFA로 변환하는 알고리즘입니다.
        <ul>
          <li><a href="https://en.wikipedia.org/wiki/Glushkov%27s_construction_algorithm">글루쉬코프 구성 알고리즘</a>(Glushkov's construction algorithm) 등 같은 변환을 수행하는 알고리즘이 더 있지만 톰슨 구성이 가장 유명한 알고리즘입니다.</li>
        </ul>
      </li>
    </ul>
  </li>
  <li><strong><a href="https://en.wikipedia.org/wiki/Nondeterministic_finite_automaton">비결정론적 유한 상태 기계</a></strong> (Nondeterministic Finite Automaton; NFA)
    <ul>
      <li>유한개의 상태를 가지는 기계로, 다음 규칙에 따라 문자열을 인식합니다.
        <ul>
          <li>기계에는 하나의 시작 상태와 한 개 이상의 인식 상태(accepting state)가 있습니다.</li>
          <li>각 상태마다 특정한 문자열을 입력받았을 때 전이할 수 있는 다음 상태가 정해져 있습니다.
            <ul>
              <li>예를 들어 1번 상태에서 <code class="language-plaintext highlighter-rouge">0</code>을 입력받으면 2번 혹은 3번 상태, <code class="language-plaintext highlighter-rouge">1</code>을 입력받으면 2번 혹은 4번 상태로 전이할 수 있습니다.</li>
              <li>각 상태-문자열 쌍마다 전이할 수 있는 상태의 수에는 제한이 없으며, 0개 역시 가능합니다.</li>
            </ul>
          </li>
          <li>추가로 전이할 때만 빈 문자열 <code class="language-plaintext highlighter-rouge">ε</code> 역시 문자로 인정하기도 하며, 이 경우 기계가 작동하는 중 아무 때나 <code class="language-plaintext highlighter-rouge">ε</code> 전이를 하거나, 하지 않을 수 있습니다.
            <ul>
              <li><code class="language-plaintext highlighter-rouge">ε</code> 전이가 있는 NFA를 따로 ε-NFA로 부르기도 하는데, 이 글에서는 구분 없이 모두 NFA로 지칭합니다.</li>
            </ul>
          </li>
          <li>문자열의 모든 문자를 순서대로 입력하면서 보드게임을 하듯이 상태를 전이시킵니다.
            <ul>
              <li>그 결과 상태 전이가 없는 상태에 갇히지 않고 인식 상태에 멈추는 방법이 <em>하나라도</em> 존재한다면 기계는 이 문자열을 <strong>인식</strong>(accept)합니다.</li>
              <li>인식 상태에 멈추는 방법이 하나도 없다면 기계는 이 문자열을 <strong>거부</strong>(reject)합니다.</li>
            </ul>
          </li>
        </ul>
      </li>
    </ul>
  </li>
  <li><strong><a href="https://en.wikipedia.org/wiki/Powerset_construction">멱집합 구성</a></strong> (Powerset construction)
    <ul>
      <li>NFA를 아래에서 설명할 DFA로 변환하는 알고리즘입니다.
        <ul>
          <li>NFA의 상태의 멱집합을 사용해 DFA 상태를 구성한다는 의미에서 이런 이름이 붙어 있습니다.</li>
        </ul>
      </li>
    </ul>
  </li>
  <li><strong><a href="https://en.wikipedia.org/wiki/Deterministic_finite_automaton">결정론적 유한 상태 기계</a></strong> (Deterministic Finite Automaton; DFA)
    <ul>
      <li>NFA와 비슷하지만 각 상태-문자열 쌍마다 최대 하나의 상태로만 전이할 수 있으며, <code class="language-plaintext highlighter-rouge">ε</code> 전이 역시 금지됩니다.</li>
      <li>NFA와 다르게 여러 가지 경로를 추적하지 않아도 되므로("결정론적"이므로) 컴퓨터로 효율적인 구현이 가능합니다.</li>
    </ul>
  </li>
  <li><strong><a href="https://en.wikipedia.org/wiki/DFA_minimization">DFA 최소화</a></strong>
    <ul>
      <li>DFA에서 시작 상태에서 도달할 수 없는 상태, 인식 상태에 도달할 수 없는 상태, 서로 구별할 수 없는 상태를 제거해 상태의 수를 최소화하는 기법입니다.</li>
    </ul>
  </li>
</ul>

<p>위의 모든 과정을 손으로 계산해야 하는 것은 아니고, 웹상의 도구를 이용할 수 있습니다. 저는 <a href="https://cyberzhg.github.io/toolbox/min_dfa">CyberZHG님의 정규 표현식 → 최소 DFA 변환 도구</a>를 이용했습니다.</p>

<p>문제에서 제시한 정규 표현식인 <code class="language-plaintext highlighter-rouge">(1(10)+1*|0+10)+</code>을 <a href="https://cyberzhg.github.io/toolbox/min_dfa?regex=KDEoMTApKzEqfDArMTApKw==">최소 DFA로 변환한 결과</a>는 다음과 같습니다. 시작 상태는 0이며, 모든 상태 번호에서 1을 빼는 가공을 거쳤습니다.</p>

<table>
  <thead>
    <tr>
      <th>상태 번호</th>
      <th>0</th>
      <th>1</th>
      <th>Accept?</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>2</td>
      <td>3</td>
      <td> </td>
    </tr>
    <tr>
      <td>1</td>
      <td>2</td>
      <td>7</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>2</td>
      <td>2</td>
      <td>4</td>
      <td> </td>
    </tr>
    <tr>
      <td>3</td>
      <td> </td>
      <td>5</td>
      <td> </td>
    </tr>
    <tr>
      <td>4</td>
      <td>6</td>
      <td> </td>
      <td> </td>
    </tr>
    <tr>
      <td>5</td>
      <td>1</td>
      <td> </td>
      <td> </td>
    </tr>
    <tr>
      <td>6</td>
      <td>2</td>
      <td>3</td>
      <td>✅</td>
    </tr>
    <tr>
      <td>7</td>
      <td>1</td>
      <td>7</td>
      <td>✅</td>
    </tr>
  </tbody>
</table>

<p>2번째·3번째 세로줄의 빈칸은 상태 전이가 없어서 DFA가 문자열을 거부하는 경우이며, 코드로 구현할 때는 <code class="language-plaintext highlighter-rouge">-1</code>을 대신 사용할 수 있습니다.</p>

<h3 id="상태-전이를-세그먼트-트리로-관리">상태 전이를 세그먼트 트리로 관리</h3>

<p>함수는 정의역의 모든 원소를 공역의 원소와 대응시킨 것인데, 위 상태 전이 함수의 경우에는 정의역의 원소가 8개뿐이므로 배열로 전부 기록할 수 있습니다. 예를 들어 비트열 <code class="language-plaintext highlighter-rouge">0</code>에 해당하는 상태 전이 배열은 C/C++ 문법으로 <code class="language-plaintext highlighter-rouge">{ 2, 2, 2, -1, 6, 1, 2, 1 }</code>입니다. 추가로 길이가 2 이상인 비트열의 상태 전이 배열 역시 비트열을 이루는 비트를 상태 전이 배열로 변환하고 함수 합성을 해서 구할 수 있습니다.</p>

<p>이제 상태 전이 배열에 결합법칙이 성립하는 이항 연산(함수 합성)이 있으니 문자열 전체의 상태 전이 배열을 세그먼트 트리로 관리할 수 있습니다. 문자열 인식은 문자열의 상태 전이 배열을 구하고 0에서의 함숫값, 즉 0번째 원소를 구한 뒤 그 상태가 인식 상태인지 확인하면 됩니다.</p>

<h3 id="갱신-결과를-세그먼트-트리에-같이-저장">갱신 결과를 세그먼트 트리에 같이 저장</h3>

<p>구간 갱신까지 처리하려면 추가로 한 가지 발상이 더 필요합니다. 이 문제에서는 함수 합성을 고려하더라도 가능한 갱신이 <code class="language-plaintext highlighter-rouge">00</code>, <code class="language-plaintext highlighter-rouge">01</code>, <code class="language-plaintext highlighter-rouge">10</code>, <code class="language-plaintext highlighter-rouge">11</code>의 네 종류뿐인데, 상태 전이 함수의 모든 함숫값을 같이 저장했으니 <strong>가능한 모든 갱신 결과도 같이 저장하지 못할 이유가 없다</strong>는 것입니다.</p>

<p>예를 들어, 비트 <code class="language-plaintext highlighter-rouge">0</code>을 세그먼트 트리로 관리하려면 기존의 <code class="language-plaintext highlighter-rouge">("0"의 상태 전이 배열)</code>이 아닌 <code class="language-plaintext highlighter-rouge">{ ("0"의 상태 전이 배열), ("0"의 상태 전이 배열), ("1"의 상태 전이 배열), ("1"의 상태 전이 배열) }</code>을 저장한 뒤, 구간 갱신을 다음과 같이 처리하면 됩니다.</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">00</code> 질의: <code class="language-plaintext highlighter-rouge">{ a, b, c, d }</code>를 <code class="language-plaintext highlighter-rouge">{ a, a, d, d }</code>로 갱신</li>
  <li><code class="language-plaintext highlighter-rouge">01</code> 질의: <code class="language-plaintext highlighter-rouge">{ a, b, c, d }</code>를 <code class="language-plaintext highlighter-rouge">{ a, b, c, d }</code>로 갱신</li>
  <li><code class="language-plaintext highlighter-rouge">10</code> 질의: <code class="language-plaintext highlighter-rouge">{ a, b, c, d }</code>를 <code class="language-plaintext highlighter-rouge">{ a, c, b, d }</code>로 갱신</li>
  <li><code class="language-plaintext highlighter-rouge">11</code> 질의: <code class="language-plaintext highlighter-rouge">{ a, b, c, d }</code>를 <code class="language-plaintext highlighter-rouge">{ a, d, a, d }</code>로 갱신</li>
</ul>

<p>문자열 인식은 <code class="language-plaintext highlighter-rouge">01</code> 질의에 해당하는 <code class="language-plaintext highlighter-rouge">b</code>를 꺼내어 위와 같이 확인하면 됩니다.</p>

<h2 id="대회-여담">대회 여담</h2>

<p><img src="/assets/post-images/그래요-저는-애주가예요.png" alt="빈 배경 앞에 Sheen Estevez가 서 있는 저화질 이미지. 이미지에는 '그래요 저는 애주가예요'와 함께 '형식언어론 같이 덕질할 사람이 없어'를 애주가의 삼행시인 양 적은 문구가 있다." /></p>

<p><em>트위터 신세대 의문(@NewGenHmmm)님의 <a href="https://twitter.com/NewGenHmmm/status/1115834651908759553">밈 이미지</a>를 편집한 이미지입니다.</em></p>

<h1 id="여담">여담</h1>

<p>물론 문제 출제에서도 보람을 느끼고 있지만, 삽화 작업에서도 꽤 보람을 느끼는 편입니다(일단 삽화는 눈에 바로 보이는 편이니까).</p>

<p>위에 언급된 대회의 삽화 중에 뭔가 은근히 귀엽고 나눔스퀘어라운드(이 블로그의 제목체입니다)가 보이면 제 작업물이라고 생각해 주세요. 특히 CHAC 2022와 CPC 2024의 모든 삽화는 제가 그렸습니다. 😜</p>

<p><img src="/assets/post-images/x/1829849604579606873.png" alt="잇창명의 2024년 8월 31일 X 게시물: &quot;사실은 이번 대회 준비하면서 공을 많이 들인 3짤4짤을 보여드리고 싶었는데요&quot;. CPC 2024의 '울타리 공사' 첫 번째 삽화, '컵 쌓기' 세 번째 삽화, '통신 시스템의 성능 저하' 첫 번째 삽화와 두 번째 삽화가 첨부되어 있다. 역시 잇창명의 2023년 10월 16일 게시물 &quot;오랜만에 생각났으니까 올려봐야지 제가 대학교 프로그래밍 대회 운영진으로 참가하면서 그렸던 귀여운 삽화를 봐주세요.&quot;를 인용하였다." /></p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:fn-proof-by-ac" role="doc-endnote">
      <p>직역하면 "<strong>맞았습니다!!</strong>에 의한 증명". 알고리즘의 엄밀한 증명이 필요한 프로그래밍 문제에서, 엄밀한 증명 없이 제출해 AC(Accepted; 백준 온라인 저지의 <strong>맞았습니다!!</strong>에 해당하는 채점 결과)를 받은 것을 "증명"으로 취급하는 농담. <a href="#fnref:fn-proof-by-ac" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:fn-breezewiki" role="doc-endnote">
      <p>Arcaea Wiki가 소속된 Fandom의 <a href="https://youtu.be/qcfuA_UAz3I">지나친 광고</a> 등의 문제로 인해 미러 서비스인 BreezeWiki로 링크합니다. <a href="#fnref:fn-breezewiki" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:fn-chart" role="doc-endnote">
      <p>Chart. 특정한 악곡의 특정한 난이도에서 등장하는 노트의 배열. <a href="#fnref:fn-chart" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:fn-pure-memory" role="doc-endnote">
      <p>특정한 채보에서 모든 노트를 Shiny 여부와 상관 없이 PURE로 처리하면 받는 칭호 <a href="#fnref:fn-pure-memory" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:fn-grammer-vs-language" role="doc-endnote">
      <p>문법은 언어를 기술하는 규칙이고, 언어는 (그 언어에) 포함되는 문자열의 집합이므로 둘은 다른 의미를 가지며, 구분되어야 합니다. 문제에서 제시하는 문법은 그 자체로는 모호하지만, 같은 언어를 기술하면서도 모호하지 않은 문법을 제시할 수 있습니다. <a href="#fnref:fn-grammer-vs-language" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="문제해결" /><category term="알고리즘" /><category term="회고" /><summary type="html"><![CDATA[그동안 중앙대학교 알고리즘 학회 ChAOS에 소속되어 백준 온라인 저지에 대회 문제를 출제해보는 귀중한 기회를 몇 번 가졌었는데, 그동안 생각해보니 대회 에디토리얼 이외에 블로그에 문제에 대한 이야기를 한 적이 없었네요. 마침 생각난 김에 그동안 출제했던 문제의 출제 의도를 하나씩 풀어보려고 합니다. (정해 코드는 없습니다!)]]></summary></entry><entry><title type="html">1,000,000,007로 그냥 나눠도 괜찮나요?</title><link href="https://eatchangmyeong.github.io/2023/11/28/can-we-just-divide-by-1000000007.html" rel="alternate" type="text/html" title="1,000,000,007로 그냥 나눠도 괜찮나요?" /><published>2023-11-28T00:00:00+09:00</published><updated>2023-11-28T00:00:00+09:00</updated><id>https://eatchangmyeong.github.io/2023/11/28/can-we-just-divide-by-1000000007</id><content type="html" xml:base="https://eatchangmyeong.github.io/2023/11/28/can-we-just-divide-by-1000000007.html"><![CDATA[<p><em>이 글의 초안을 검토해 주신 하스켈 학교의 다믜님, Absta님, Ceramif님께 감사드립니다.</em></p>

<p>백준 온라인 저지에서 분수 계산을 하는 문제를 보면 다음과 같은 출력을 요구하는 문제가 많습니다.</p>

<blockquote>
  <p>각 \(i\)번째 줄에 \(u_i\)에 대한 결과 값을 기약분수 \(P/Q\)로 표현했을 때, \(Q\)의 \(10^9+7\)에 대한 곱셈 역원 \(Q^{-1}\)에 대해 \(P \times Q^{-1}\)을 \(10^9+7\)로 나눈 나머지를 출력한다. (<a href="https://www.acmicpc.net/problem/23313">23313번 "K-계산기"</a>)</p>
</blockquote>

<blockquote>
  <p>다각형 넓이의 최댓값이
\(\displaystyle \frac{p}{q}\)(\(p\)와 \(q\)는 서로소인 자연수)일 때, \(q\cdot v\equiv p (\bmod 1\,000\,000\,007\))이 되는 \(0\)이상 \(1\,000\,000\,006\) 이하의 정수 \(v\)를 출력한다. (<a href="https://www.acmicpc.net/problem/24892">24892번 "이차 함수"</a>)</p>
</blockquote>

<blockquote>
  <p>\(\int_{t_1}^{t_2}f(t)dt\)의 값이 정수 \(m\), 양의 정수 \(n\)에 대하여 기약분수
\(\frac{m}{n}\)일 때, \(m\times n^{-1}\bmod (10^9+7)\)을 출력한다. \(n^{-1}\)은 \(n\)의 모듈러 곱셈에 대한 역원이다. (<a href="https://www.acmicpc.net/problem/26519">26519번 "함수와 최소 스패닝 트리"</a>)</p>
</blockquote>

<blockquote>
  <p>고인물들은 문장에 담긴 비밀의 힘이 \(0\)이 될 확률 \(p_M / q_M\)에 대해 채팅방의 비밀번호를
\(p_M \times q_M^{-1} \pmod{10^9 + 7}\)로 설정하였다. 채팅방의 비밀번호는 한 번 설정하면 변경할 수 없으며,
\(q_M^{-1}\)은 \(q_M\)의 모듈러 곱셈에 대한 역원이고,
\(p_M \times q_M^{-1} \pmod{10^9 + 7}\)의 값은 주어진 제약 조건 내에서 유일하게 존재함을 증명할 수 있다. (<a href="https://www.acmicpc.net/problem/28093">28093번 "비밀의 채팅방"</a>)</p>
</blockquote>

<blockquote>
  <p>Formally, it can be shown that the answer can be represented as a fraction \(p / q\) for some coprime non-negative integers \(p\) and \(q\). You have to print the value \(p \cdot q^{-1} \bmod (10^9 + 7)\). (<a href="https://www.acmicpc.net/problem/23144">23144번 "Reasonable Workplace Relationship"</a>)</p>
</blockquote>

<p>위의 다섯 문제에서 요구하는 것을 요약하면 다음과 같습니다.</p>

<blockquote>
  <p>문제의 정답은 기약분수 \(\frac{a}{b}\)로 나타낼 수 있다. \(b\)의 모듈로 곱셈의 역원 \(b^{-1}\)에 대해 \(ab^{-1} \bmod (10^9 + 7)\)을 출력하여라.</p>
</blockquote>

<p>제가 이런 요구사항을 처음 맞닥뜨렸을 때는 "정수를 출력하라고 하는데 왜 \(q^{-1}\)이 있는 거지?"라는 생각부터 들었습니다. \(q^{-1} = \frac{1}{q}\)는 적어도 유리수에나 취할 수 있는 연산이라는 고정관념이 있었던 시절이었는데, 그 이후 <a href="https://www.acmicpc.net/problem/13172">13172번 "Σ"</a>의 지문을 읽고 나서 이 방식을 어느 정도 수긍할 수 있게 되었습니다.</p>

<p>위의 문제 지문을 읽고 나서도 개인적으로 여전히 이해가 되지 않는 점들이 있었는데, 마침 이게 주제로 떠올랐고 검색해도 잘 나오지 않으니 블로그 글로 써보려고 합니다.</p>

<h1 id="역원이라는-단어를-남용하는-거-아닌가요">"역원"이라는 단어를 남용하는 거 아닌가요?</h1>

<p>저도 처음에는 그렇게 생각했는데, 아닙니다.</p>

<p>그동안 이 블로그에서 <a href="/2022/06/05/how-is-type-derivative-a-thing.html">대수적 구조를</a> <a href="/2022/07/11/boj-14939-lights-out-but-with-matrix.html">이미 여러 번</a> <a href="/2023/05/02/the-missing-factor-of-floyd-warshall-algorithm.html">언급했었는데</a>, 소수 \(p\)에 대해 모듈로 \(p\) 정수의 집합 \(\mathbb{Z}/p\mathbb{Z}\)<sup id="fnref:fn-quotient-set" role="doc-noteref"><a href="#fn:fn-quotient-set" class="footnote" rel="footnote">1</a></sup>도 역시 대수적 구조를 이룹니다. 이번에는 그냥 군도 그냥 환도 아닌 <strong>체</strong>입니다.</p>

<p><strong><a href="https://ko.wikipedia.org/wiki/체_(수학)">체</a></strong>는 유리수나 실수 따위의 사칙연산 <em>전부</em>를 일반화한 대수적 구조라고 볼 수 있는데, 다음 두 이항 연산이 다음 성질을 만족하면 됩니다.</p>

<ul>
  <li>"덧셈" 연산 \(\oplus\)
    <ul>
      <li><strong>교환법칙</strong>: \(a \oplus b = b \oplus a\)</li>
      <li><strong>결합법칙</strong>: \((a \oplus b) \oplus c = a \oplus (b \oplus c)\)</li>
      <li><strong>항등원 \(0\)의 존재</strong>: \(0 \oplus a = a\)</li>
      <li><strong>역원 \(-a\)의 존재</strong>: \(a \oplus -a = 0\)</li>
    </ul>
  </li>
  <li>"곱셈" 연산 \(\otimes\)
    <ul>
      <li><strong>교환법칙</strong>: \(a \otimes b = b \otimes a\)</li>
      <li><strong>결합법칙</strong>: \((a \otimes b) \otimes c = a \otimes (b \otimes c)\)</li>
      <li><strong>항등원 \(1\)의 존재</strong>: \(1 \otimes a = a\)</li>
      <li><em>\(0\)이 아닌</em> 원소에 대해 <strong>역원 \(a^{-1}\)의 존재</strong>: \(a \otimes a^{-1} = 1\)</li>
    </ul>
  </li>
  <li>\(\oplus\)에 대한 \(\otimes\)의 <strong>분배법칙</strong>: \(a \otimes (b \oplus c) = (a \otimes b) \oplus (a \otimes c)\)</li>
</ul>

<p>뺄셈은 \(a - b = a \oplus -b\)로, 나눗셈은 \(\frac{a}{b} = a \otimes b^{-1}\)로 나타낼 수 있습니다. 사칙연산에 대해 학교에서 배운 웬만한 성질이 전부 공리로 들어가 있는 만큼 다른 대수적 구조에 비해 공리의 개수도 많습니다. 여기서부터는 글 작성의 편의를 위해 \(\oplus\), \(\otimes\) 대신 흔히 사용하는 사칙연산 기호를 사용하겠습니다.</p>

<p>또 한 가지 중요한 것은, \(p\)가 <strong>소수이기만 해도</strong> \(\mathbb{Z}/p\mathbb{Z}\)가 체가 되는 데는 충분하다는 것입니다. 즉, 1,000,000,007뿐만 아니라 원하는 아무 소수 \(p\)를 정하고 "\(ab^{-1} \bmod p\)를 출력하여라"라고 해도 문제가 되지 않습니다. 물론 1,000,000,007이 유독 자주 쓰이는 이유가 있긴 합니다.</p>

<ul>
  <li>\(\mathbb{Z}/p\mathbb{Z}\)는 분모가 \(p\)의 배수인 유리수를 나타낼 수 없다. \(p\)를 큰 소수로 잡으면 이런 경우를 줄일 수 있다.
    <ul>
      <li>위의 13172번 문제에서 설명한 이유와 동일합니다.</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">int</code>(32비트임을 가정할 때)의 최댓값 2,147,483,647의 절반을 넘지 않는다.
    <ul>
      <li><code class="language-plaintext highlighter-rouge">(a + b) % p</code> 연산을 할 때 오버플로우가 발생하지 않는 범위입니다.</li>
    </ul>
  </li>
  <li><code class="language-plaintext highlighter-rouge">long long</code>(64비트임을 가정할 때)의 최댓값 9,223,372,036,854,775,807의 제곱근을 넘지 않는다.
    <ul>
      <li><code class="language-plaintext highlighter-rouge">(a * b) % p</code> 연산을 할 때 오버플로우가 발생하지 않는 범위입니다.</li>
    </ul>
  </li>
  <li>1,000,000,000과 가장 가까운 소수이므로 외우기 쉽다.</li>
</ul>

<p>얘기가 조금 삼천포로 빠지긴 했는데, 지금까지 이 블로그에서 온갖 이상한 대상의 "항등원"과 "역원"을 아무렇지 않게 논했으면서 다른 것도 아니고 <strong>체</strong>를 이루는 \(\mathbb{Z}/p\mathbb{Z}\)의 곱셈의 역원만 "단어의 남용"이라면서 거부하는 것은 앞뒤가 맞지 않는다고 생각합니다. <strong>모듈로 곱셈의 역원도 엄연한 곱셈의 역원입니다.</strong></p>

<h1 id="페르마의-소정리를-그렇게-비틀어도-되나요">페르마의 소정리를 그렇게 비틀어도 되나요?</h1>

<p>모듈로 곱셈의 역원을 구할 때는 보통 <a href="https://ko.wikipedia.org/wiki/베주_항등식">베주 보조정리</a>를 응용해 <a href="https://en.wikipedia.org/wiki/Extended_Euclidean_algorithm">확장 유클리드 호제법</a>으로 구하거나, <a href="https://ko.wikipedia.org/wiki/페르마의_소정리">페르마의 소정리</a>를 응용해 <a href="https://en.wikipedia.org/wiki/Exponentiation_by_squaring">분할 정복을 이용한 거듭제곱</a> 등의 모듈로 거듭제곱으로 구합니다. 이 중 후자의 논리는 다음과 같습니다.</p>

<blockquote>
  <p>페르마의 소정리에 의해 \(a^p \equiv a \pmod{p}\)니까 양변을 \(a^2\)로 나누면 \(a^{p - 2} \equiv a^{-1} \pmod{p}\) 아니냐?</p>
</blockquote>

<p>\(\mathbb{Z}/p\mathbb{Z}\)가 체인(즉, \(a^2\)로 나누는 게 문제가 없는) 것을 아는 지금은 이 논법을 자연스럽게 받아들일 수 있지만, 모듈로 곱셈의 역원이라는 개념을 처음 받아들일 때는 그렇지 못했습니다. 일단 체라는 개념을 무시하고 생각하더라도 가장 작은 소수는 2이므로 일단 \(a^{p - 2}\)가 말이 된다는 것은 수긍하겠는데<sup id="fnref:fn-zeroth-power" role="doc-noteref"><a href="#fn:fn-zeroth-power" class="footnote" rel="footnote">2</a></sup> \(a^2\)를 곱해서 \(a\)가 되는 원소가 <em>유일하게 존재</em>한다는 건 어떻게 아나요?</p>

<p>이 글을 쓰기 위해서 조사해본 결과 모듈로 곱셈의 역원의 존재성과 유일성을 페르마의 소정리로 직접 증명하는 게 아니라, 보통 위에서 언급했던 베주 보조정리를 이용해 증명하는 것 같습니다. 베주 보조정리의 내용은 다음과 같습니다.</p>

<blockquote>
  <p>임의의 정수 \(a\)와 \(b\)에 대해...</p>
  <ol>
    <li>어떤 정수의 쌍 \(x\)와 \(y\)가 존재해서 \(ax + by = \gcd(a, b)\)이다.</li>
    <li>또한, 임의의 정수 \(x\)와 \(y\)에 대해 \(ax + by\) 꼴로 나타낼 수 있는 수는 항상 \(\gcd(a, b)\)의 배수이다.</li>
  </ol>
</blockquote>

<p>글 태그로 <em>#정수론</em>을 추가하기는 했는데, 저는 정수론에 정말 약하기 때문에 글에 증명을 싣지 않고 넘어가겠습니다. 혹시 증명이 궁금하시다면 <a href="https://en.m.wikipedia.org/wiki/Bézout's_identity#Proof">영문 위키백과</a>에서 확인할 수 있습니다.</p>

<p>위의 두 정리 중 1번에 \(0 &lt; a &lt; b = p\)라는 조건을 걸면 \(\gcd(a, p) = 1\)이 됩니다(\(a\)와 \(p\)가 서로소이므로). 이제 \(ax + py = 1\)의 양변에 \(\bmod p\)를 하면 \(ax \equiv 1 \pmod{p}\)가 되고, \(x\)가 곱셈의 역원이 됩니다.</p>

<p>일단 곱셈의 역원의 존재성을 증명했다면 유일성은 체의 다른 공리를 이용해 증명할 수 있습니다. 구체적으로 \(b\)와 \(c\)가 모두 \(a\)의 역원이라면,</p>

\[\begin{align}
b &amp;= b \cdot 1 \\
&amp;= b(ac) \\
&amp;= (ba)c \\
&amp;= 1 \cdot c \\
&amp;= c
\end{align}\]

<p>이므로 \(b = c\)이고, 즉 \(a\)는 서로 다른 둘 이상의 역원을 가질 수 없습니다. 이제 곱셈의 역원이 유일하게 존재한다는 것을 증명했으니 페르마의 소정리 등식을 위와 같이 조작해도 문제가 없겠습니다.</p>

<h1 id="모듈로-정수끼리-계산하면-값이-어긋나지-않을까요">모듈로 정수끼리 계산하면 값이 어긋나지 않을까요?</h1>

<p>예전에는 이런 문제를 보면서 "중간 값에 \(\bmod p\)를 걸고 계산해도 되나? 최종 값이 어긋나지는 않을까?"라는 걱정을 하면서 출제자들이 어떻게 신경을 써두었겠지라는 믿음을 가지고 풀었던 적이 있습니다.</p>

<p>다행히 <strong>준동형사상</strong>이라는 개념 덕분에 그런 걱정은 하지 않아도 됩니다. 이 글은 예상 독자를 백준 온라인 저지에서 문제를 푸는 사람으로 생각하고 작성했기 때문에 수학적으로 엄밀하지 않아도 전달하기 쉽도록 설명하며, 또한 문제의 답을 계산하는 과정에서 0으로 나누기가 일어나지 않거나 쉽게 피할 수 있음을 가정합니다.</p>

<p>우선 위에서 언급했던 체의 정의를 다시 살펴봅시다.</p>

<h2 id="다르게-정의하는-체">다르게 정의하는 체</h2>

<p>위에서 체는 "두 이항 연산 \(+\)과 \(\times\)이 아홉 가지의 체 공리를 만족하는 대수적 구조"라고 소개한 바가 있습니다. 그런데 여기서 \(+\)와 \(\times\)의 항등원을 각각 무항 연산자, 역원을 각각 단항 연산자로 생각하고 다음과 같은 형태의 체 공리를 제시할 수도 있습니다. 이 관점에서 체는 연산자 6개와 공리 9개로 이루어진 대수적 구조입니다.</p>

<ul>
  <li>이항 연산 \(+\)
    <ul>
      <li><strong>교환법칙</strong></li>
      <li><strong>결합법칙</strong></li>
    </ul>
  </li>
  <li>무항 "연산" \(0\)
    <ul>
      <li><strong>\(+\)의 항등원으로 작용</strong>: \(a + 0 = a\)</li>
    </ul>
  </li>
  <li>단항 연산 \(-\)
    <ul>
      <li><strong>\(+\)의 역원으로 작용</strong>: \(a - a = 0\)</li>
    </ul>
  </li>
  <li>이항 연산 \(\times\)
    <ul>
      <li><strong>교환법칙</strong></li>
      <li><strong>결합법칙</strong></li>
    </ul>
  </li>
  <li>무항 "연산" \(1\)
    <ul>
      <li><strong>\(\times\)의 항등원으로 작용</strong>: \(a \cdot 1 = a\)</li>
    </ul>
  </li>
  <li>단항 연산 \(^{-1}\)
    <ul>
      <li><em>\(0\)이 아닌</em> 원소에 대해 <strong>\(\times\)의 역원으로 작용</strong>: \(\frac{a}{a} = 1\)</li>
    </ul>
  </li>
  <li>\(+\)에 대한 \(\times\)의 <strong>분배법칙</strong></li>
</ul>

<h2 id="체-준동형사상">체 준동형사상</h2>

<p><strong><a href="https://ko.wikipedia.org/wiki/준동형">준동형사상</a></strong>이라는 말을 해석해보자면, "사상"은 함수를 더 유식하게 이르는 말이고<sup id="fnref:fn-morphism" role="doc-noteref"><a href="#fn:fn-morphism" class="footnote" rel="footnote">3</a></sup> "준동형"은 "비슷한 모양", 즉 "구조를 보존한다"는 의미입니다.<sup id="fnref:fn-isomorphism" role="doc-noteref"><a href="#fn:fn-isomorphism" class="footnote" rel="footnote">4</a></sup></p>

<p>위에서 풀어 쓴 의미를 생각해 보면, <strong><a href="https://en.wikipedia.org/wiki/Glossary_of_field_theory#Homomorphisms">체 준동형사상</a></strong>은 "체의 구조를 보존하는 함수"라고 생각할 수 있습니다. 조금 더 엄밀하게는 체 \(F\)에서 체 \(G\)로 가는 함수 \(f\)가 다음과 같이 체의 연산자 6개 모두에 대해 아래 "보존 법칙"이 성립하면 \(f\)를 체 준동형사상이라고 합니다. \(f\)의 정의역과 공역이 다르고, 이에 따라 <strong>체 연산도 \(f\) 안팎에서 다른 것을 사용함</strong>에 유의해 주세요.</p>

<ol>
  <li>\(f(a + b) = f(a) + f(b)\)</li>
  <li>\(f(0) = 0\)</li>
  <li>\(f(-a) = -f(a)\)</li>
  <li>\(f(ab) = f(a)f(b)\)</li>
  <li>\(f(1) = 1\)</li>
  <li>\(a \ne 0\)에 대해 \(f \left( \frac{1}{a} \right) = \frac{1}{f(a)}\)</li>
</ol>

<p>이 중 2번, 3번, 6번은 다른 법칙과 체 공리로부터 유도할 수 있기 때문에 실질적으로는 1번, 4번, 5번이 성립하는 것만 보이면 됩니다.</p>

<h2 id="정말-체-준동형사상일까">정말 체 준동형사상일까?</h2>

<p>나중에 정의와 증명에 도움이 될 보조함수와 보조정리를 제시해 보겠습니다.</p>

\[f' : \mathbb{Z} \rightarrow \mathbb{Z}/p\mathbb{Z} \\
f'(x) = x \bmod p\]

<p>이 함수는 \(f'(0) = 0\), \(f'(1) = 1\)을 만족함을 쉽게 유추할 수 있습니다. 또한, 이 함수는 \(f'(x + y) = f'(x) + f'(y)\), \(f'(xy) = f'(x)f'(y)\)를 만족합니다. 증명은 <a href="https://www.acmicpc.net/problem/10430">10430번 "나머지"</a>로 갈음합니다.</p>

<p>위의 다섯 문제에서 제시하는 체 준동형사상은 다음과 같이 정의할 수 있습니다. 이 함수는 \(b\)가 \(p\)의 배수일 때 정의되지 않기 때문에 엄밀히 말해서 체 준동형사상은 아니지만, 그래도 개념 자체는 유효합니다. 위에서 0으로 나누기를 생각하지 않는다고 한 것은 그것까지 고려하면 글이 훨씬 복잡해지기 때문입니다.</p>

<p>\(f : \mathbb{Q} \nrightarrow \mathbb{Z}/p\mathbb{Z} \\
f \left( \frac{a}{b} \right) = f'(a)f'(b)^{-1}\)
<sup id="fnref:fn-partial-function" role="doc-noteref"><a href="#fn:fn-partial-function" class="footnote" rel="footnote">5</a></sup></p>

<p>5번 법칙은 단순 대입으로 증명할 수 있습니다.</p>

\[\begin{align}
f(1) &amp;= f \left( \frac{1}{1} \right) \\
&amp;= f'(1)(f'(1))^{-1} \\
&amp;= 1 \cdot 1^{-1} \\
&amp;= 1
\end{align}\]

<p>4번 법칙도 이렇게 증명할 수 있습니다.</p>

\[\begin{align}
f \left( \frac{a}{c} \cdot \frac{b}{d} \right) &amp;= f \left( \frac{ab}{cd} \right) \\
&amp;= f'(ab)(f'(cd))^{-1} \\
&amp;= f'(a)f'(b)(f'(c)f'(d))^{-1} \\
&amp;= f'(a)f'(b)(f'(c))^{-1}(f'(d))^{-1}
\end{align}\]

<p>그런데 한편,</p>

\[\begin{align}
f \left( \frac{a}{c} \right) f \left( \frac{b}{d} \right) &amp;= f'(a)(f'(c))^{-1}f'(b)(f'(d))^{-1} \\
&amp;= f'(a)f'(b)(f'(c))^{-1}(f'(d))^{-1}
\end{align}\]

<p>두 식을 같은 식으로 변형할 수 있으므로 \(f \left( \frac{a}{c} \cdot \frac{b}{d} \right) = f \left( \frac{a}{c} \right) f \left( \frac{b}{d} \right)\)임을 알 수 있습니다.</p>

<p>1번 법칙을 증명하는 것은 연습 문제로 남기겠습니다.</p>

<h2 id="그래서-뭐가-좋나요">그래서 뭐가 좋나요?</h2>

<p>일단 체 준동형사상이 있으면 아래와 같이 체 연산만으로 이루어진 수식에 분배법칙을 적용해 아래처럼 바꿀 수 있습니다.</p>

\[f \left( \frac{ab - 1}{c + d} + e \right) = \frac{f(a)f(b) - 1}{f(c) + f(d)} + f(e)\]

<p>그 다음에는 \(f\)의 안팎에서 체 공리가 똑같이 성립하므로 "원래 세계"의 수식에 어떤 조작을 가하든 "거울 세계"의 수식에도 똑같이 반영할 수 있습니다.</p>

\[f \left( \frac{ab + ce + de - 1}{c + d} \right) = \frac{f(a)f(b) + f(c)f(e) + f(d)f(e) - 1}{f(c) + f(d)}\]

<p>최종 값에 모듈로를 한 번 적용해서 답을 내는 것은 좌변, 중간 값에 미리 모듈로를 적용하고 그 값끼리 연산하는 것은 우변에 해당하므로, 글 제목대로 <strong>1,000,000,007로 그냥 나눠도 괜찮습니다!</strong> 물론 자료형이나 연산자 실수로 오버플로우가 나지 않게 코드를 조심히 작성해야 하겠습니다.</p>

<p>글을 시작하면서 언급했듯이 이 글을 쓰기 시작한 계기는 제가 이런 문제를 풀면서 선뜻 이해가 되지 않았던 점들을 스스로 해결하기 위해서였고, 글을 마친 지금은 쓰기 전에 비해 조금 더 안심하고 문제를 풀 수 있을 것 같습니다. 글을 읽어주신 여러분께도 도움이 되기를 바랍니다.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:fn-quotient-set" role="doc-endnote">
      <p>"\(\mathbb{Z}/p\mathbb{Z}\)"라는 표기는 몫집합을 나타내는 표기인데, 이런 개념이 낮설다면 "정수의 집합(\(\mathbb{Z}\))인데 \(p\)의 배수(\(p\mathbb{Z}\))를 모두 같은 원소로 취급하겠다(\(/\))" 정도로 이해해도 될 것 같습니다(<a href="https://math.stackexchange.com/a/594479">참고</a>). \(p\)의 배수가 같은 원소가 되면 \(p\)로 나눈 나머지가 같은 수끼리도 같은 원소가 됩니다. <a href="#fnref:fn-quotient-set" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:fn-zeroth-power" role="doc-endnote">
      <p>\(p = 2\)일 때는 \(a^{p - 2} = a^0 = 1\)인 것으로 정의하면 됩니다. <a href="#fnref:fn-zeroth-power" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:fn-morphism" role="doc-endnote">
      <p>사상(morphism)은 특히 범주론에서 자주 사용하는 단어로, 함수보다 일반적인 개념입니다. 이 글에서는 집합론의 관점에서는 사상과 함수가 같은 의미라고 생각해도 충분하겠습니다. <a href="#fnref:fn-morphism" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:fn-isomorphism" role="doc-endnote">
      <p>왜 "준"이 붙는지 궁금한 분들을 위해 설명하자면, <em>양방향으로</em> 구조를 보존하는 함수, 즉 역함수도 준동형사상인 준동형사상을 <a href="https://ko.wikipedia.org/wiki/동형_사상">동형사상</a>이라고 합니다. <a href="#fnref:fn-isomorphism" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:fn-partial-function" role="doc-endnote">
      <p>\(f\)는 분모가 \(p\)의 배수일 때 정의되지 않는 부분함수이기 때문에 \(\mathbb{Q} \rightarrow \mathbb{Z}/p\mathbb{Z}\) 대신 \(\mathbb{Q} \nrightarrow \mathbb{Z}/p\mathbb{Z}\)로 표기합니다. <a href="#fnref:fn-partial-function" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="문제해결" /><category term="정수론" /><category term="추상대수학" /><summary type="html"><![CDATA[이 글의 초안을 검토해 주신 하스켈 학교의 다믜님, Absta님, Ceramif님께 감사드립니다.]]></summary></entry><entry><title type="html">TIL: 일급 함수, 그 정의로 충분할까?</title><link href="https://eatchangmyeong.github.io/2023/06/03/til-is-that-definition-enough-for-first-class-functions.html" rel="alternate" type="text/html" title="TIL: 일급 함수, 그 정의로 충분할까?" /><published>2023-06-03T00:00:00+09:00</published><updated>2023-06-03T00:00:00+09:00</updated><id>https://eatchangmyeong.github.io/2023/06/03/til-is-that-definition-enough-for-first-class-functions</id><content type="html" xml:base="https://eatchangmyeong.github.io/2023/06/03/til-is-that-definition-enough-for-first-class-functions.html"><![CDATA[<p>어느 순간 문득 깨달았는데, 어떤 면에서는 <strong>C도 일급 함수를 지원하는 게 아닌가</strong>라는 생각이 들었습니다. 정말 뚱딴지같은 소리긴 하지만 일단 들어보세요.</p>

<h1 id="일급-객체의-조건">일급 객체의 조건</h1>

<p>보통 <a href="https://ko.wikipedia.org/wiki/일급_객체">일급 객체</a>라고 하면 "다른 객체들에 일반적으로 적용 가능한 모든 연산을 지원하는 객체"를 의미하고, 구체적으로는 이 세 가지 조건을 드는 경우가 많습니다.</p>

<ol>
  <li>변수에 대입할 수 있을 것.</li>
  <li>함수에 인자로 넘길 수 있을 것.</li>
  <li>함수의 반환값으로 쓰일 수 있을 것.</li>
</ol>

<p>예를 들어 반론의 여지 없이 함수가 일급 객체인 JavaScript에서는 이렇게 할 수 있습니다.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 변수에 대입할 수 있을 것</span>
<span class="kd">let</span> <span class="nx">cmp_asc</span> <span class="o">=</span> <span class="p">(</span><span class="nx">l</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">l</span> <span class="o">-</span> <span class="nx">r</span><span class="p">;</span>
<span class="kd">let</span> <span class="nx">cmp_desc</span> <span class="o">=</span> <span class="p">(</span><span class="nx">l</span><span class="p">,</span> <span class="nx">r</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">r</span> <span class="o">-</span> <span class="nx">l</span><span class="p">;</span>

<span class="c1">// 함수에 인자로 넘길 수 있을 것</span>
<span class="p">[</span><span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">2</span><span class="p">].</span><span class="nx">sort</span><span class="p">(</span><span class="nx">cmp_asc</span><span class="p">);</span>

<span class="c1">// 함수의 반환값으로 쓰일 수 있을 것</span>
<span class="kd">function</span> <span class="nx">cmp</span><span class="p">(</span><span class="nx">desc</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">return</span> <span class="nx">desc</span>
		<span class="p">?</span> <span class="nx">cmp_desc</span>
		<span class="p">:</span> <span class="nx">cmp_asc</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>그런데 한편 C에서도 이렇게 할 수 있습니다.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;stdlib.h&gt;</span><span class="cp">
</span>
<span class="kt">int</span> <span class="nf">cmp_asc</span><span class="p">(</span><span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="n">l</span><span class="p">,</span> <span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="n">r</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">return</span> <span class="o">*</span><span class="p">(</span><span class="k">const</span> <span class="kt">int</span> <span class="o">*</span><span class="p">)</span><span class="n">l</span> <span class="o">-</span> <span class="o">*</span><span class="p">(</span><span class="k">const</span> <span class="kt">int</span> <span class="o">*</span><span class="p">)</span><span class="n">r</span><span class="p">;</span>
<span class="p">}</span>
<span class="kt">int</span> <span class="nf">cmp_desc</span><span class="p">(</span><span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="n">l</span><span class="p">,</span> <span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="n">r</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">return</span> <span class="o">*</span><span class="p">(</span><span class="k">const</span> <span class="kt">int</span> <span class="o">*</span><span class="p">)</span><span class="n">r</span> <span class="o">-</span> <span class="o">*</span><span class="p">(</span><span class="k">const</span> <span class="kt">int</span> <span class="o">*</span><span class="p">)</span><span class="n">l</span><span class="p">;</span>
<span class="p">}</span>

<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
	<span class="c1">// 변수에 대입할 수 있을 것</span>
	<span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">fn</span><span class="p">)(</span><span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="p">,</span> <span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="p">)</span> <span class="o">=</span> <span class="n">cmp_asc</span><span class="p">;</span>
	
	<span class="kt">int</span> <span class="n">arr</span><span class="p">[</span><span class="mi">3</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="mi">2</span> <span class="p">};</span>
	<span class="c1">// 함수에 인자로 넘길 수 있을 것</span>
	<span class="n">qsort</span><span class="p">(</span><span class="n">arr</span><span class="p">,</span> <span class="mi">3</span><span class="p">,</span> <span class="k">sizeof</span><span class="p">(</span><span class="kt">int</span><span class="p">),</span> <span class="n">fn</span><span class="p">);</span>
<span class="p">}</span>

<span class="c1">// 함수의 반환값으로 쓰일 수 있을 것</span>
<span class="kt">int</span> <span class="p">(</span><span class="o">*</span><span class="n">cmp</span><span class="p">(</span><span class="kt">int</span> <span class="n">desc</span><span class="p">))(</span><span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="p">,</span> <span class="k">const</span> <span class="kt">void</span> <span class="o">*</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">return</span> <span class="n">desc</span>
		<span class="o">?</span> <span class="n">cmp_desc</span>
		<span class="o">:</span> <span class="n">cmp_asc</span><span class="p">;</span>
<span class="p">}</span>
</code></pre></div></div>

<p>(놀랍게도 C에서도 함수 포인터를 반환할 수 있습니다. 저번에 올렸던 <a href="/2020/12/30/all-about-c-type-system.html">이 글</a>도 읽어보시면 좋습니다.)</p>

<p>"함수가 아니라 함수 포인터를 반환하는 거 아니냐"는 반응을 할 수도 있을 것 같은데, 애초에 C의 언어 명세상 <a href="/2020/12/30/all-about-c-type-system.html#배열과-함수는-포인터로-바뀐다">모든 함수는 함수 포인터의 형태로 호출됩니다</a>. 그리고 그 문제가 아니더라도 <em>특정한 동작을 전달하고 실행하는 데 그 형태가 함수냐 함수 포인터냐는 중요하지 않다</em>는 것이 제 사견입니다.</p>

<p>이렇게 (포인터의 형태로) 일급 객체의 조건을 모두 충족하는데도 C의 함수가 일급 객체라고 생각하는 사람은 아무도 없습니다. 무엇이 잘못된 것일까요?</p>

<h1 id="다른-시각으로-보는-일급-함수">다른 시각으로 보는 일급 함수</h1>

<p>이쯤에서 위에서 인용한 위키백과 글을 더 꼼꼼히 읽어 봅시다. <a href="https://ko.wikipedia.org/wiki/일급_객체#함수">함수 문단</a>을 보면 이런 내용을 찾을 수 있습니다.</p>

<blockquote>
  <p>대다수의 언어에서 함수를 다른 함수에 매개 변수로 전달하거나 리턴 값으로 받을 수 있는데, 이러한 속성이 일급 객체의 조건으로 충분한 지에 대해서는 논쟁의 여지가 있다.</p>

  <p>일부 저자들의 경우 함수가 '일급 객체'가 되기 위한 조건으로 런타임에 함수 생성 가능 여부를 드는데, 이 조건에 의하면 C와 같은 언어에서의 함수는 일급 객체가 아니다. C의 함수와 같은 객체들은 경우에 따라서 <strong>이급 객체</strong>로 불리기도 하는데, 비록 일급 객체의 속성을 모두 갖추지는 못했다 하더라도 그에 상응하는 방식으로 다뤄질 수 있기 때문이다.</p>
</blockquote>

<p>이외에도 영문 위키백과의 <a href="https://en.wikipedia.org/wiki/First-class_function">First-class function</a> 항목에는 이러한 시각도 있습니다.</p>

<blockquote>
  <p>어떤 프로그래밍 언어론자들은 [일급 함수의 조건으로] 익명 함수(함수 리터럴)를 추가로 제시하기도 한다.</p>

  <p>Some programming language theorists require support for anonymous functions (function literals) as well.</p>
</blockquote>

<p>이 글을 처음 썼을 때는 익명 함수보다는 클로저를 지원해야 한다고 생각한다는 단순 의견만 적었었는데, 그동안 생각이 더 정리되어서 부연 설명을 더 적기로 했습니다. 위의 인용문에서 "런타임에 함수 생성"에 집중해 봅시다.</p>

<div class="language-c highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">#include</span> <span class="cpf">&lt;stdio.h&gt;</span><span class="cp">
</span>
<span class="kt">int</span> <span class="nf">main</span><span class="p">(</span><span class="kt">void</span><span class="p">)</span> <span class="p">{</span>
	<span class="kt">double</span>
		<span class="n">a</span> <span class="o">=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">22561007516091158</span><span class="p">,</span>
		<span class="n">b</span> <span class="o">=</span> <span class="mi">0</span><span class="p">.</span><span class="mi">5653166761141877</span><span class="p">,</span>
		<span class="n">c</span> <span class="o">=</span> <span class="n">a</span> <span class="o">+</span> <span class="n">b</span><span class="p">;</span>
	<span class="n">printf</span><span class="p">(</span><span class="s">"%f"</span><span class="p">,</span> <span class="n">c</span><span class="p">);</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">0.22561007516091158</code>과 <code class="language-plaintext highlighter-rouge">0.5653166761141877</code>은 방금 생성한 무작위의 실수입니다. (최적화를 생각하지 않는다면) C 표준 라이브러리에도 위의 코드에도 <code class="language-plaintext highlighter-rouge">c = 0.7909267512750993</code>이라는 값이 있을 리가 없는데, 컴파일된 C 프로그램은 이 값을 문제 없이 잘 처리합니다. <strong>런타임에 새로운 <code class="language-plaintext highlighter-rouge">double</code> 값이 생성</strong>된 것입니다.</p>

<p>반면 처음 언급했던 "다른 객체들에 일반적으로 적용 가능한 모든 연산을 지원", 즉 "다른 객체를 다루는 것처럼 다룰 수 있다"는 측면에서 보면, 함수는 소스 코드에 정의한 것 이외에 런타임에 다른 것을 추가로 만들거나 포인터로 참조할 수 없습니다. C의 함수가 일급 객체가 아니라고 하는 것은 아마 이것을 의미하는 것 같습니다.</p>

<p>제가 알고 있는 "런타임에 함수 생성" 방법은 크게 두 가지로 나뉩니다.</p>

<h2 id="완전히-동적으로-생성">완전히 동적으로 생성</h2>

<p>많은 인터프리터 언어에는 <code class="language-plaintext highlighter-rouge">eval</code>이나 <code class="language-plaintext highlighter-rouge">exec</code>이라는 함수가 있어서 문자열을 코드로 해석하고 실행할 수 있고, 이를 <del>악용</del> 활용해서 동적으로 함수를 생성할 수 있습니다.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">exec_test</span><span class="p">(</span><span class="n">a</span><span class="p">,</span> <span class="n">b</span><span class="p">):</span>
	<span class="k">exec</span><span class="p">(</span><span class="s">"print(a + b)"</span><span class="p">)</span>

<span class="n">exec_test</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="c1"># 3
</span></code></pre></div></div>

<p>JavaScript에서는 추가로 <code class="language-plaintext highlighter-rouge">Function</code> 생성자를 이용해 문자열을 바로 실행하는 대신 <em>함수로</em> 만들 수 있습니다.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// function(a, b) { console.log(a + b); }와 같습니다.</span>
<span class="kd">const</span> <span class="nx">dynamic_function</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">Function</span><span class="p">(</span><span class="dl">'</span><span class="s1">a</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">b</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">console.log(a + b);</span><span class="dl">'</span><span class="p">);</span>

<span class="nx">dynamic_function</span><span class="p">(</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">)</span> <span class="c1">// 3</span>
</code></pre></div></div>

<p>이 분야의 원조인<sup id="fnref:fn-lisp-eval" role="doc-noteref"><a href="#fn:fn-lisp-eval" class="footnote" rel="footnote">1</a></sup> Lisp에서는 <em>아무 자료구조를</em> <code class="language-plaintext highlighter-rouge">eval</code>할 수 있습니다. 코드와 데이터를 똑같은 자료구조로 표현하기 때문에(이 성질을 <a href="https://en.wikipedia.org/wiki/Homoiconicity">homoiconicity</a>라고 합니다) 가능한 묘기입니다. 아래는 <a href="https://en.wikipedia.org/wiki/Eval#Lisp">영문 위키백과</a>에서 가져온 예제입니다.</p>

<div class="language-lisp highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">; `form1`에 `'(+ 1 2 3)`을 대입합니다.</span>
<span class="c1">; `'`은 "인용"(quotation) 표시로, 인용된 코드는 실행되는 대신 데이터로 취급합니다.</span>
<span class="c1">; * 그냥 `(+ 1 2 3)`이라고 작성하면 1, 2, 3을 더해서 6으로 평가되는 코드가 됩니다.</span>
<span class="c1">; * 아래와 같이 `'(+ 1 2 3)`이라고 작성하면 차례대로 심볼 `+`, 수 `1`, `2`, `3`을 원소로 가지는 링크드 리스트가 됩니다.</span>
<span class="p">(</span><span class="k">setq</span> <span class="nv">form1</span> <span class="o">'</span><span class="p">(</span><span class="nb">+</span> <span class="mi">1</span> <span class="mi">2</span> <span class="mi">3</span><span class="p">))</span>

<span class="c1">; `form1`을 "실행"해서 6을 반환합니다.</span>
<span class="p">(</span><span class="nb">eval</span> <span class="nv">form1</span><span class="p">)</span> <span class="c1">; 6</span>
</code></pre></div></div>

<p>그런데 사실 이 방식은 소 잡는 칼이라는 느낌이 듭니다. 그냥 함수 몇 개만 새로 만들고 싶었을 뿐인데 (Lisp과 같은 경우를 제외하면) 문자열을 해석해서 실행하는 엔진이 딸려오니 엄청나게 무겁고, 또한 컴파일 언어에서는 지원하기 어려우며 보안 문제가 있다는 단점도 있습니다.</p>

<p>그 대신 대다수의 언어에서 지원하는 방식은 이렇습니다.</p>

<h2 id="데이터를-동반하는-함수">데이터를 동반하는 함수</h2>

<p>"일급 함수"를 지원한다고 여겨지는 언어와 아닌 언어의 차이점을 생각해 보면, 함수 안에서 참조할 수 있는 변수의 범위가 다릅니다.</p>

<div class="language-rust highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Rust도 일급 함수를 지원하지만, "일급" 함수(클로저)와 "이급" 함수(전통적 함수)가 서로 다른 문법과 의미론을 가집니다.</span>
<span class="c1">// 이 코드에서 정의하는 함수는 모두 이급 함수입니다.</span>

<span class="k">const</span> <span class="n">GLOBAL</span><span class="p">:</span> <span class="nb">i32</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>

<span class="k">fn</span> <span class="nf">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="k">let</span> <span class="n">outer_local</span><span class="p">:</span> <span class="nb">i32</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>

	<span class="k">fn</span> <span class="nf">inner</span><span class="p">()</span> <span class="p">{</span>
		<span class="k">let</span> <span class="n">local</span><span class="p">:</span> <span class="nb">i32</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span>

		<span class="nd">println!</span><span class="p">(</span><span class="s">"{}"</span><span class="p">,</span> <span class="n">GLOBAL</span><span class="p">);</span> <span class="c1">// 1</span>
		<span class="nd">println!</span><span class="p">(</span><span class="s">"{}"</span><span class="p">,</span> <span class="n">outer_local</span><span class="p">);</span> <span class="c1">// can't capture dynamic environment in a fn item</span>
		<span class="nd">println!</span><span class="p">(</span><span class="s">"{}"</span><span class="p">,</span> <span class="n">local</span><span class="p">);</span> <span class="c1">// 3</span>
	<span class="p">}</span>

	<span class="nf">inner</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nb">global</span> <span class="o">=</span> <span class="mi">1</span><span class="p">;</span>

<span class="kd">function</span> <span class="nx">main</span><span class="p">()</span> <span class="p">{</span>
	<span class="kd">const</span> <span class="nx">outer_local</span> <span class="o">=</span> <span class="mi">2</span><span class="p">;</span>
	
	<span class="kd">function</span> <span class="nx">inner</span><span class="p">()</span> <span class="p">{</span>
		<span class="kd">const</span> <span class="nx">local</span> <span class="o">=</span> <span class="mi">3</span><span class="p">;</span>
		
		<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nb">global</span><span class="p">);</span> <span class="c1">// 1</span>
		<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">outer_local</span><span class="p">);</span> <span class="c1">// 2</span>
		<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">local</span><span class="p">);</span> <span class="c1">// 3</span>
	<span class="p">}</span>
	
	<span class="nx">inner</span><span class="p">();</span>
<span class="p">}</span>
</code></pre></div></div>

<p>언어마다 자세한 사항은 다르겠지만, 전역 변수와 현재 함수 안에 있는 지역 변수(매개변수 포함)는 이급 함수에서도 참조할 수 있지만 <em>바깥 범위의 지역 변수</em>는 일급 함수에서만 참조할 수 있습니다. 즉, 이급 함수로 일급 함수를 모사하려면 바깥 범위의 지역 변수를 전역 변수로든 매개변수로든 받을 수 있어야 합니다.</p>

<p>이를 구현하는 방법이 몇 가지가 있는데...</p>

<ul>
  <li><strong><a href="https://ko.wikipedia.org/wiki/클로저_(컴퓨터_프로그래밍)">클로저</a></strong>: JavaScript 등 인터프리터/스크립트 언어에서 주로 사용하는 방법으로, 함수 자체에 바깥 함수의 범위를 묶어놓아서 이를 통해 참조할 수 있도록 합니다.</li>
  <li><strong><a href="https://en.wikipedia.org/wiki/Partial_application">부분 적용</a></strong>: 구현 기법이라기보다는 패턴에 가까워 보이는데, 다변수 함수의 매개변수 중 몇 개를 미리 채워놓고 채우지 않은 것만을 매개변수로 하는 새로운 함수를 만드는 기법입니다.
    <ul>
      <li>다만 Haskell 등 커링을 지원하는 일부 함수형 언어에서는 실제로 부분 적용된 함수를 데이터로서 주고받는 방식으로 구현된 것 같습니다.</li>
    </ul>
  </li>
  <li><strong><a href="https://en.wikipedia.org/wiki/Function_object">함수 객체</a></strong>: C++ 등 컴파일 언어에서 주로 사용하는 방법으로, 클로저 대신 외부 상태를 복사해서 담는 일회용 구조체/클래스를 만들고 그 구조체를 함수처럼 호출할 수 있도록 합니다.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">this</code> 값 묶어놓기</strong>: 자유도는 조금 떨어지지만, 함수가 <code class="language-plaintext highlighter-rouge">this</code>를 지원하는 객체지향 언어에서는 함수에 <code class="language-plaintext highlighter-rouge">this</code> 값을 묶어서 들고 다니기도 합니다. C++의 (<code class="language-plaintext highlighter-rouge">.*</code>과 <code class="language-plaintext highlighter-rouge">-&gt;*</code> 연산자로 만드는) 멤버 함수 포인터와 JavaScript의 (<a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Function/bind"><code class="language-plaintext highlighter-rouge">Function.prototype.bind</code></a>로 만드는) 바인딩한 함수가 여기에 속합니다.</li>
  <li>이외에 제가 모르는 방식이 또 있다면 제보 부탁드립니다.</li>
</ul>

<p>구현 방식은 서로 다르지만, 모두 <em>함수에 외부 데이터를 묶어놓는다</em>는 공통점을 띠고 있습니다. 위의 방식과 비교해서 꽤 제한적이지만 이 정도만 지원해도 <em>웬만한</em> 함수형 프로그래밍은 다 할 수 있습니다<sup id="fnref:fn-sk-combinator" role="doc-noteref"><a href="#fn:fn-sk-combinator" class="footnote" rel="footnote">2</a></sup>.</p>

<p>결론을 말씀드리자면, 위에 나열한 일급 함수 구현 방식 중 <strong>처음 세 개 중 하나만 구현하면 일급 함수라고 부를 수 있다</strong>는 것이 제 의견입니다.</p>

<h1 id="여담-this-값-묶어놓기에-대하여">여담: <code class="language-plaintext highlighter-rouge">this</code> 값 묶어놓기에 대하여</h1>

<p>마지막 문단을 읽고 나서 "그럼 <code class="language-plaintext highlighter-rouge">this</code> 값 묶어놓기는 불충분한가요?"라고 생각하고 계신다면, <a href="https://gamemaker.io">GameMaker</a>로 예시를 들어 보겠습니다.</p>

<p>게임메이커 언어는 스튜디오 2.3 시점부터 익명 함수를 지원하기 시작했고, 여기에 <a href="/2020/04/24/gms-tips.html#오묘한-함수형-프로그래밍의-세계">적당히 흑마술을 부려서 클로저와 비슷한 것을 구현해본 적이 있었습니다</a>. 이때 작성했던 코드는 다음과 같습니다.</p>

<div class="language-javascript gml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">plus_function</span><span class="p">(</span><span class="nx">_x</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">return</span> <span class="nx">method</span><span class="p">({</span>
		<span class="na">_x</span><span class="p">:</span> <span class="nx">_x</span>
	<span class="p">},</span> <span class="kd">function</span><span class="p">(</span><span class="nx">_y</span><span class="p">)</span> <span class="p">{</span>
		<span class="k">return</span> <span class="nb">self</span><span class="p">.</span><span class="nx">_x</span> <span class="o">+</span> <span class="nx">_y</span><span class="p">;</span>
	<span class="p">});</span>
<span class="p">}</span>

<span class="kd">var</span> <span class="nx">plus_three</span> <span class="o">=</span> <span class="nx">plus_function</span><span class="p">(</span><span class="mi">3</span><span class="p">);</span>
<span class="nx">show_message</span><span class="p">([</span> <span class="nx">plus_three</span><span class="p">(</span><span class="mi">5</span><span class="p">),</span> <span class="nx">plus_three</span><span class="p">(</span><span class="mi">123</span><span class="p">)</span> <span class="p">]);</span> <span class="c1">// [ 8, 126 ]</span>
</code></pre></div></div>

<p>여기서 <code class="language-plaintext highlighter-rouge">self</code>와 <a href="https://manual.gamemaker.io/monthly/en/#t=GameMaker_Language/GML_Reference/Variable_Functions/method.htm"><code class="language-plaintext highlighter-rouge">method(struct, fn)</code></a>은 각각 JavaScript의 <code class="language-plaintext highlighter-rouge">this</code>와 <code class="language-plaintext highlighter-rouge">fn.bind(struct)</code>와 비슷한 동작을 합니다. 즉, <strong><code class="language-plaintext highlighter-rouge">this</code> 값 묶어놓기</strong>에 해당하는 구현입니다. 당시에도 게임메이커 언어가 클로저를 지원하지 않았고, 2023.11 업데이트가 적용된 현재에도 지원하지 않고 있습니다.</p>

<p><code class="language-plaintext highlighter-rouge">method</code> 호출에서 볼 수 있듯이, 이 방식은 <em>외부 변수를 수동으로 가져와야 합니다</em>. JavaScript에서는 변수 범위 전체를 클로저로 잡아주기 때문에 무슨 변수를 사용할지 일일이 작성할 필요가 없습니다.</p>

<div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">plus_function</span><span class="p">(</span><span class="nx">_x</span><span class="p">)</span> <span class="p">{</span>
	<span class="k">return</span> <span class="kd">function</span><span class="p">(</span><span class="nx">_y</span><span class="p">)</span> <span class="p">{</span>
		<span class="k">return</span> <span class="nx">_x</span> <span class="o">+</span> <span class="nx">_y</span><span class="p">;</span>
	<span class="p">};</span>
<span class="p">}</span>
</code></pre></div></div>

<p>함수 객체 방식에서도 일회용 구조체 타입은 언어가 알아서 만들어주며, 외부 변수도 일일이 작성할 필요가 없거나 작성하더라도 보통 더 깔끔한 문법을 제공해줍니다.</p>

<p>추가로 GameMaker에 원래 있는 <code class="language-plaintext highlighter-rouge">self</code>를 원래의 의미대로 사용할 수 없다는 단점도 있습니다. 클로저와 <code class="language-plaintext highlighter-rouge">self</code>를 둘 다 쓰고 싶다면 아래와 같이 우회해야 합니다. 못생겼네요.</p>

<div class="language-javascript gml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">closure_and_self</span><span class="p">(</span><span class="nx">_x</span><span class="p">)</span> <span class="p">{</span>
	<span class="kd">var</span> <span class="nx">_self</span> <span class="o">=</span> <span class="nb">self</span><span class="p">;</span>
	<span class="k">return</span> <span class="nx">method</span><span class="p">({</span>
		<span class="na">_x</span><span class="p">:</span> <span class="nx">_x</span><span class="p">,</span>
		<span class="na">_self</span><span class="p">:</span> <span class="nx">_self</span>
	<span class="p">},</span> <span class="kd">function</span><span class="p">()</span> <span class="p">{</span>
		<span class="k">return</span> <span class="nb">self</span><span class="p">.</span><span class="nx">_x</span> <span class="o">+</span> <span class="nb">self</span><span class="p">.</span><span class="nx">_self</span><span class="p">.</span><span class="nx">y</span><span class="p">;</span>
	<span class="p">});</span>
<span class="p">}</span>
</code></pre></div></div>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:fn-lisp-eval" role="doc-endnote">
      <p>John McCarthy의 History of Lisp에 따르면 <a href="http://www-formal.stanford.edu/jmc/history/lisp/node3.html">프로그래밍 언어 인터프리터가 발명된 계기가 Lisp의 <code class="language-plaintext highlighter-rouge">eval</code>이었다고 합니다</a>. <a href="#fnref:fn-lisp-eval" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:fn-sk-combinator" role="doc-endnote">
      <p>사실 <code class="language-plaintext highlighter-rouge">K(x)(y) = x</code>, <code class="language-plaintext highlighter-rouge">S(x)(y)(z) = x(z)(y(z))</code>의 두 함수와 부분 적용만 지원해도 계산가능한 모든 함수를 표현할 수 있다는 것이 <a href="https://en.wikipedia.org/wiki/SKI_combinator_calculus">알려져 있습니다</a>. <a href="#fnref:fn-sk-combinator" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="언어" /><category term="함수형" /><category term="TIL" /><summary type="html"><![CDATA[어느 순간 문득 깨달았는데, 어떤 면에서는 C도 일급 함수를 지원하는 게 아닌가라는 생각이 들었습니다. 정말 뚱딴지같은 소리긴 하지만 일단 들어보세요.]]></summary></entry><entry><title type="html">기술 업계의 독성 말투 문제, 고칩시다! (임시 업로드)</title><link href="https://eatchangmyeong.github.io/2023/05/21/tech-has-a-toxic-tone-problem-let-s-fix-it.html" rel="alternate" type="text/html" title="기술 업계의 독성 말투 문제, 고칩시다! (임시 업로드)" /><published>2023-05-21T00:00:00+09:00</published><updated>2023-05-21T00:00:00+09:00</updated><id>https://eatchangmyeong.github.io/2023/05/21/tech-has-a-toxic-tone-problem-let-s-fix-it</id><content type="html" xml:base="https://eatchangmyeong.github.io/2023/05/21/tech-has-a-toxic-tone-problem-let-s-fix-it.html"><![CDATA[<p><em>이 글은 April Wensel님의 <a href="https://compassionatecoding.com/blog/2016/8/25/tech-has-a-toxic-tone-problemlets-fix-it/">Tech has a Toxic Tone Problem — Let's Fix It!</a>의 <a href="https://edykim.com/ko/post/tech-has-a-toxic-tone-problem-lets-fix-it/">edykim님 번역본</a>을 수정한 번역본으로, edykim님의 사정상 원본에 반영되기 전까지 임시로 잇창명 개발 블로그에 게시합니다. edykim님의 블로그에 반영되면 이 글은 삭제한 뒤 원본 링크만 남길 예정입니다.</em></p>

<p><em>원본 글을 작성해 주신 April Wensel님과 해당 글을 번역해 주시고 번역 개선을 흔쾌히 허락해 주신 edykim님께 감사드립니다.</em></p>

<p>의사소통에 관해서, 특히 엔지니어가 연관된 경우라면 기술 업계에서 독성 말투 문제가 존재합니다. 저는 이 문제를 지난날 동안 주변에서 겪었기 때문에 알기도 하지만, 저도 이런 순간을 마주했을 때는 그 문제에 기여를 했었기 때문입니다.</p>

<p>먼저 <a href="https://www.merriam-webster.com/dictionary/tone">말투(tone)</a>는 "누군가의 말이나 글에서 표현되는 태도"라는 의미에서, <a href="http://www.hbs.edu/faculty/Publication%20Files/16-057_d45c0b4f-fa19-49de-8f1b-4b12fe054fea.pdf">독성(toxic)</a>은 "조직의 자산 또는 구성원을 포함해서 그 조직에 위해를 주는 것"이란 의미에서 사용하고 있습니다.</p>

<p>소프트웨어 엔지니어는 일반적으로 뛰어난 커뮤니케이션 스킬을 갖고 있지 않다고 얘기합니다. 이런 경향은 여러 이유로 설명되기도 합니다. 전문 지식을 갖고 있기 때문에, 인간관계보다 컴퓨터와 더 많은 시간을 썼기 때문에, 전통적으로 성격이 프로그래밍에 가까워서 등으로 말이죠. 하지만 이런 대부분의 잠재적 이유는 문제점이 있으며 특히 마지막 이유는 고정관념이기도 합니다. 게다가 엔지니어가 이런 문제점을 핑계로 주변 사람들에게 멀쩡한 사람처럼 행동하지 않을 구실로 삼을 수 없다는 점입니다.</p>

<p>엔지니어의 부족한 커뮤니케이션은 새로운 현상이 아닙니다. 제럴드 와인버그는 1971년에 <a href="http://www.worldcat.org/title/psychology-of-computer-programming/oclc/216809">The Psychology of Computer Programming</a>에서 다음처럼 적었습니다.</p>

<blockquote>
  <p>프로그래밍에서는 엄청나게 똑똑할지 몰라도 자신의 지적 능력을 사용해서 자신의 사회적 행동이나 대화 방식을 고칠 수 있을 정도로 똑똑하지 않을 수 있습니다.</p>
</blockquote>

<p>이런 점은 단순히 팀 생산성을 망치는 일에 그치지 않습니다. 커뮤니케이션 문제가 기술을 공부하는 사람들을 낙담하게 하며 사회적 활동에 참여하는 기존 엔지니어의 의지까지도 꺾게 됩니다. 여기서 사회적 활동이란 스택오버플로우에 질문을 올리고 답변을 달거나 오픈 소스에 기여하는 활동을 예로 들 수 있습니다. <strong>그러므로 프로그래밍을 둘러싼 커뮤니케이션을 향상하는 일은 더 포용적인 개발 커뮤니티를 만드는 일에 필수적이라 할 수 있습니다.</strong></p>

<p>이 문제를 해결하려면 먼저 문제를 제대로 이해해야 합니다. 저는 그동안에 이 말투 문제에 세 가지의 형태가 있는 것을 관찰했습니다. 물론 이 세 가지가 전부인 것은 아니고, 엔지니어만 이런 커뮤니케이션 문제를 겪는 것도 아닙니다.</p>

<p>이런 독성 말투는 일반적으로 거들먹거리는 말투, 기계적 말투, 또는 비관적인 말투로 나타나기도 합니다. 어떤 경우에는 달콤한 독성 말투 비빔밥이라도 되는 것처럼 이 세 가지를 모두 섞어 놓은 때도 있습니다.</p>

<h2 id="거들먹거리는-말투">거들먹거리는 말투</h2>

<p>어떤 기술을 습득하게 된다면 주변 사람들은 가지고 있지 않을 전문 지식도 가지게 됩니다. 나쁘게도 새로운 기술을 습득한다고 해서 그 기술이 없는 사람과 효과적으로 의사소통하는 능력까지 함께 따라오지 않습니다. 다른 사람과 대화하게 될 때 당신이 아는 만큼 알지 못하는 사람에 공감하는 능력이 부족하다면 거들먹거리는 녀석으로 보일 뿐입니다.</p>

<p>지미 펄론이 새터데이 나이트 라이브에서 했던 <a href="https://youtu.be/sqXm6h8A_UE">닉 번즈: 당신 회사의 컴퓨터 담당자</a>를 본 적이 있나요? 닉은 따지고 보면 허구의 IT 전문가며 실제 세계의 프로그래머가 아니긴 하지만 극에서 묘사된 그의 행동은 놀랍게도 기술 업계에서 만연하게 볼 수 있던 거들먹거리는 말투를 명확하게 보여주고 있습니다.</p>

<p>저는 동료들이 다른 엔지니어나 면접 대상자를 "종이봉투 뚫고 나올 간단한 방법조차도 프로그래밍하지 못하는 멍청이"라고 부르는 경우를 수 년간 들어왔습니다. 초급 엔지니어가 질문했다고 눈을 부라리는 경우도 봤습니다. 부트캠프 졸업생이나 독학으로 프로그래머가 된 사람들을 평가하는 지적도 들은 적이 있습니다.</p>

<p>또한 많은 엔지니어가 마케팅, 영업, 제품, 고객 응대 직군에게, 그리고 이들에 대해 말하는 방식에서도 이를 알 수 있습니다. 특히 이들을 "비개발자(non-technical)"라고 부르면서 이들의 의견을 묵살할 때 더 잘 드러나며, 이는 제가 이 단어를 <a href="https://compassionatecoding.com/blog/2019/4/17/if-you-can-use-a-fork-youre-technical">아예 사용하지 말아야 한다고 생각하는 이유</a>이기도 합니다.</p>

<p>이런 거들먹거리는 태도는 항상 노골적으로 드러나지 않습니다. 다음 예제는 조금 교묘합니다. 누군가 <a href="http://stackoverflow.com/q/13744450">스택오버플로우에 남긴 질문</a>인데 온라인에서 한참 검색하기도 했지만, 이해에 어려움이 있어서 <em>옵저버</em>와 <em>옵저버블</em>의 차이점이 무엇인지 물어봤습니다. 다음 답변은 100회 이상의 최다 추천을 받은 답변입니다.</p>

<p><img src="/assets/post-images/toxic-tone-stack-overflow-answer.png" alt="거들먹거리고 도움 안되는 부분: 난 이 내용을 어떻게 더 쉬운 영어로 설명할 수 있을지 모르겠네요. 지금 위 내용에 정의가 있습니다. 딱 두 문장입니다. 10번 읽고 나면 더 이상 이해할 것도 없을 것 같은데요. 도움 되는 부분: 올려주신 코드는 Student와 MessageBoard를 이용한 구체적인 예제입니다. Student는 옵저버의 목록에 스스로를 등록해서 MessageBoard에 새 메시지가 등록되면 알림을 받을 수 있습니다. MessageBoard는 옵저버의 목록을 돌며 발생한 이벤트를 모두에게 알리고 있습니다. 트위터를 생각해보세요. 누군가를 팔로우하면 트위터는 그 사람의 팔로워 목록에 당신을 추가하게 됩니다. 그 사람이 새 트윗을 등록하면 그 내용을 당신이 보게 됩니다. 동일한 개념입니다. 이 예시에서는 당신의 트위터 계정이 옵저버고 팔로우한 사람이 옵저버블입니다. 이 분석이 딱 맞다고는 얘기하기 어렵습니다. 트위터는 중재자(Mediator) 패턴에 가깝기 때문입니다. 하지만 요점은 잘 묘사하고 있습니다." /></p>

<p>읽기 좋게 거들먹거리는 부분을 가져왔습니다.</p>

<blockquote>
  <p>난 이 내용을 어떻게 더 쉬운 영어로 설명할 수 있을지 모르겠네요.</p>

  <p>지금 위 내용에 정의가 있습니다. 딱 두 문장입니다. 10번 읽으면 더는 이해할 것도 없을 것 같은데요.</p>
</blockquote>

<p>질문에 답변하는 사람에게는 용어의 차이가 명확해서 어떻게 이걸 이해하기 위해 더 "쉬운 영어"로 설명해야 하는지 상상조차 하지 못하고 있습니다. 이 질문을 남긴 사람은 이 답변으로 어떤 기분이 들었을지 궁금하게 합니다. 질문자가 이 답변에서 응원을 받은 기분이거나 답을 읽고 더 자신감을 느끼게 되었을 거라고는 상상되질 않습니다. 이 문답이 충격적이라는 점을 <a href="https://twitter.com/loooorenanicole/status/745685279940411396">트위터에서도</a> 마주할 수 있었습니다.</p>

<p>운 좋게도 누군가 커뮤니티에서 답변을 수정할 수 있는 사람도 트위터 토론을 봤는지 거들먹거리는 서두를 지워서 유용한 답변으로 바꿔놨습니다. 커뮤니티 구성원 모두가 친절함에도 가치를 둬서 투표를 따라 줬더라면 얼마나 좋았을까요.</p>

<p>근본적으로 어떤 경우든 거들먹거리는 말투 뒤에 숨어 있는 태도는 "내 주변에 있는 사람들은 다 모르지만 나는 알고 있다. 내가 우월하기 때문에 그들을 굳이 존중할 필요가 없다."와 비슷할 겁니다.</p>

<p>이런 태도는 위험합니다. <a href="http://www.worldcat.org/title/crime-and-punishment/oclc/27188192">죄와 벌</a>에 나오는 라스콜니코프의 "초인" 콤플렉스를 연상하게 합니다. 이 사람은 스스로를 다수인간 위에 있는 법적 존재인 소수인간이라 여기고 다른 사람을 살해하기에 이릅니다.</p>

<p>물론 이처럼 극단적인 경우를 다루지는 않지만, 여전히 이런 사람은 우리 개발팀에 있는 것은 건강하지 않습니다. RailsBridge를 창업한 사라 메이는 <a href="https://twitter.com/sarahmei/status/673599699819958273">이렇게 말했습니다.</a></p>

<p><img src="/assets/post-images/x/673599699819958273.png" alt="Sarah Mei의 2015년 12월 7일 X 게시물: &quot;Anti-social behavior in any employee invisibly &amp; silently costs you money - WAY more money than their extraordinary productivity gains you.&quot;" /></p>

<blockquote>
  <p>어떤 직원이든 반사회적 행동에서는 보이지 않고 조용한 비용이 발생합니다. 그런 사람들의 비범한 생산성보다 정! 말! 큰 비용을 지출하게 합니다.</p>
</blockquote>

<p>이에 대한 해결책은 무엇일까요?</p>

<h3 id="인정적인-대안">인정적인 대안</h3>

<p>먼저 대표직에 있다면 거만하거나 거들먹거리거나 공격적인 행동에 대해 보상하는 일을 중단해야 합니다. 개념을 진정으로 숙련했다면 경험이 적은 사람에게 더 간단한 용어를 사용해서 개념을 설명할 수 있어야 합니다. 이런 행동이 보상을 받아야 하는 행동입니다.</p>

<p>개인 수준에서는 무얼 할 수 있을까요?</p>

<p>동료가 도움을 요청할 때 우월감이 들고 스스로 무언가 거들먹거리는 내용을 말할 것 같다는 느낌이 들면, 내가 이런 답변을 누군가에게 들었을 때 어떤 기분이 들까 하고 스스로에게 물어봐야 합니다. 사용하는 단어가 듣는 상대의 기분을 상하게 하나요 아니면 좋게 하나요? 정말 그 사람보다 더 잘 알고 있는 게 맞나요? 더 
알고 있다는 점이 상대에게 예의 없게 행동해야 한다는 의미인가요? 상대방도 당신이 모르는 부분을 더 잘 알 수도 있지 않을까요? 단순히 동일한 문제를 다른 관점에서 보고 있는 것은 아닐까요?</p>

<p>당신이 대화하고 있는 사람이 정말로 해당 주제에 대해 무지하다고 가정해봅시다. 상대방이 문제를 이해하는데 얼마나 어려움을 겪을지 상상해봅니다. 이해하는 동안 겪는 고통에 동정심을 갖고 그 어려움을 최소화할 수 있도록 하려면 어떻게 도울 수 있을지 살펴봅시다. 자신이 이 주제에 대해 처음으로 배우던 당시를 생각해본다면 어떻게 알려줘야 하는지 생각하는데 도움 될 겁니다. 어떤 점이 도움이 되었습니까? 다음을 고려해보세요.</p>

<ol>
  <li>어떤 질문들을 하고 있었고 가장 유용한 답변은 어떤 답변이었습니까?</li>
  <li>어떤 자료가 이 문제를 이해하는데 있어 추가적인 관점을 제공할 수 있습니까?</li>
</ol>

<p>또한 자기 자신에게 인정을 베풀기 바랍니다. 이 사람을 도와주는 일이 에너지를 소비하나요? 만약 그렇다면 좀 쉬는 것이 낫지 않을까요? 도와줄 수 있는 다른 사람은 없나요? 이 문제를 다루는 데 도움이 되는 연관 자료가 있나요? (주의하세요: "<a href="http://lmgtfy.com/?q=how+not+to+be+a+jerk">내가 널 위해 Google 해주마!</a>" 링크를 보내는 일은 일반적으로 거들먹거리는 거나 다름없습니다. 상대방이 농담으로 받아들일 것이라는 확신이 있는 것이 아니라면 말입니다.)</p>

<p>스스로 이런 행동을 한다면 당신의 동기(motivation)를 확인해보세요. 사실, 이 지식에 대해 약간 불안전하게 느껴서 그런 행동을 보일 가능성이 있지 않나요? 자신의 자아를 방어하기 위해서 거들먹거려야겠다는 유혹을 받는 것은 아닌가요? 정말 이런 문제로 그렇다면 스스로를 채찍질하지 않기 바랍니다. 기술 산업에 있는 면접과 직원 보상 시스템은 주로 부풀려진 자아를 선호하기 때문에 아마도 이런 태도가 당신의 잘못만은 아닐 겁니다. 그러므로 조금의 자기 성찰로 무언가 공격적인 말을 하는 것을 방지할 수 있을 것입니다.</p>

<p>거들먹거리는 응답 대신에 연민이 있는 응답을 할 수 있다면 듣는 상대방도 기분 상하지 않아 당신과 함께 일하기 즐겁게 여길 것입니다. 올리비아 폭스 카반의 저서 <a href="https://book.naver.com/bookdb/book_detail.nhn?bid=7241510">카리스마, 상대를 따뜻하게 사로잡는 힘</a> (<a href="http://www.worldcat.org/title/charisma-myth-how-anyone-can-master-the-art-and-science-of-personal-magnetism/oclc/729341503">The Charisma Myth</a>)에서 이런 이야기가 나옵니다.</p>

<p>1886년, 한 여성이 두 명의 영국 총리 후보와 각각 저녁 식사를 했습니다. 이 여성이 저녁 식사에서 언론에 다음과 같이 말했습니다.</p>

<blockquote>
  <p>"글래드스턴 씨와 저녁 식사를 한 후에는 그가 영국에서 가장 현명한 사람이라고 생각했습니다. 디즈레일리 씨와 저녁 식사를 한 후에는 <strong>제가</strong> 영국에서 가장 현명한 사람이라는 생각이 들었습니다."</p>
</blockquote>

<p>말할 필요도 없이 궁극적으로 디즈레일리가 선거에서 승리했습니다. 어떤 타입의 사람이 되고 싶은가요? 거들먹거리며 당신의 우월함을 자랑하는 사람이 되고 싶은가요? 아니면 다른 사람들의 가능성을 일깨우는 데 도움을 주는 사람이 되고 싶은가요? 단순하게, <em>당신은</em> 어떤 타입의 사람과 일을 하고 싶은가요?</p>

<p>이 주제에 대해 더 알고 싶다면 책 <a href="https://www.aladin.co.kr/shop/wproduct.aspx?itemid=919883">또라이 제로 조직</a> (<a href="http://www.worldcat.org/title/no-asshole-rule-building-a-civilized-workplace-and-surviving-one-that-isnt/oclc/70176901">The No A**hole Rule</a>)을 확인해보세요.</p>

<h2 id="기계적-말투">기계적 말투</h2>

<blockquote>
  <p>"안녕 패트, 네 사촌이 죽었다."</p>
</blockquote>

<p>이 메시지는 제가 사랑하는 할머니가 제 유년 시절에 자동 응답기에 남긴 메시지입니다. 목소리에 억양도 없었고 요점을 전달하는데 감정적 수사 여구도 없이 단순히 효율적인 사실만 전달하는 말이었습니다. 저는 할머니의 직선적인 태도를 물려받았다고 생각합니다.</p>

<p>아쉽게도 모두가 감정 없는 직선적인 태도를 좋아하지 않습니다. 앞서 이야기한 것과 같이 저 또한 기술 분야의 독성 말투 문제에 기여를 했는데 이 로봇 같이 행동하는 문제가 바로 제 약점이기도 합니다.</p>

<p>엔지니어로서 우리는 컴퓨터를 다룰 때 보통 주의 깊게 사람의 감정을 고려하지 않아도 됩니다. 컴퓨터에게 매우 직선적으로 무엇을 할지 이야기하면 컴퓨터는 명령을 처리합니다. 아름답게 효율적이고 논리적인 흐름입니다. 아마 이런 특징이 많은 사람으로 하여금 엔지니어링에 첫눈에 반하고 빠지게 하는 그런 부분일 겁니다.</p>

<p>좋든 싫든, 인간은 컴퓨터가 아닙니다. 우리는 감정이 있습니다. 상대가 어떻게 받아들일지 고려하지 않고 사실을 직접 공유하면 안 됩니다. 제 말은 물론 그렇게 직접 해도 되긴 하겠지만 그러면 당신에게 "거칠다" 혹은 "직설적"이라는 이름표가 붙게 됩니다. (저를 <em>믿으세요</em>.) 인간에게 기계에 말하듯 한다면 사람들 대부분은 당신과 일하는 것을 좋아하지 않을 겁니다.</p>

<p>제 할머니 덕분일지는 모르겠지만, 저는 이런 직선적인 태도가 엔지니어에게는 자산이 될 수 있다고 항상 생각합니다. (사실 엔지니어뿐만 아니라 모두에게 말입니다.) 하지만 말을 듣는 청중을 주의 깊게 생각하는 일, 어떻게 말을 전달해야 하는지 그 모양을 다듬는 일이 중요하다는 점에 저는 동의하게 되었습니다.</p>

<p>중대한 순간에 웹사이트가 닫히게 되었다고 가정해봅시다. 지금 문제를 해결하고 있다는 확신을 보여주지 않고 직설적으로 이 소식만 전달한다면 아마 회사에서 가장 인기 있는 사람이 될 수는 없을 겁니다. 이런 종류의 소식도 기계적인 말투로 전달한다면 당신이 고객에게 공감하고 있는 것인지도 의문입니다. 또한 침착함을 유지하는 것도 중요하지만 최소한 고객에게 어떤 영향을 미치는지 인지하는 것도 중요합니다.</p>

<p>기계적 말투는 피드백을 전달하는 과정에서도 문제입니다. 직접적인 피드백을 좋아한다고 여러 차례 언급한 디자이너와 함께 협업한다고 상상해봅시다. 이 디자이너는 지금 함께 작업하고 있는 반려동물 관리 앱의 가장 최근 목업(mockup)에 대해 조언을 구했습니다. 사용자의 반려동물 정보를 볼 수 있는 상세 페이지를 만들고 있었습니다. 이 목업에는 디자이너의 고양이, 닐에 대한 통계가 표시되고 있습니다. 페이지 상단에는 "나"라는 제목이 붙어 있습니다. 대략 다음 같은 화면이지만 훨씬 멋지다고 생각해봅시다.</p>

<p><img src="/assets/post-images/toxic-tone-mockup.png" alt="반려동물 관리 앱의 목업 이미지. '나'라는 제목 아래에 고양이 이미지와 꺾은선그래프가 있다." /></p>

<p>이 디자인을 본 후에 시나리오를 논리적으로 생각해보면 이렇게 말하게 될 겁니다. "이 화면의 제목은 반려동물에 대한 하위 페이지라면 적어도 '내 고양이' 또는 '내 반려동물' 아니면 '닐'이 되어야 하지 않나요?" 논리적이기도 하고 제목이 페이지 가장 위에 있으니 가장 먼저 보이기도 하니 이런 지적을 가장 먼저 할 수 있을 겁니다.</p>

<p>이제 디자이너와의 관계가 좋고, 디자이너가 스스로 능력에 자신감이 있으며, 기분이 좋은 날이고, 정말 앞서 말한 것처럼 직접적인 피드백을 좋아하는 사람이라면 디자이너는 오류를 지적한 점에 고마워하고 제목을 갱신하는데 동의할 겁니다. 아니면 왜 여기서는 이런 제목이 맞는지 기쁘게 설명할 수도 있겠습니다.</p>

<p>하지만 모든 상황이 잘 맞아서 돌아가지 않아서 위의 긴 조건문 중 하나라도 참이 아니게 된다면, 먼저 디자이너가 그동안 디자인을 하면서 공들인 부분을 알아주지 않고 어떤 부분에서도 이 디자인의 장점을 언급하지 않으며 가장 먼저 문제를 딱 집어내서 그다지 기분이 좋지 않을 수도 있습니다.</p>

<p>스스로는 효율적이라고 생각할 수 있겠습니다. 왜 디자인에서 올바른 부분을 이야기하거나 잘못된 부분에 대해 조심해서 얘기하면서 시간을 낭비해야 하죠? 그냥 바뀌어야 하는 부분을 직접 집어 말해줘서 빠르게 고치는 게 뭐가 잘못인가요? 음, 사람들은 감정이 있고 그 감정은 생산성에 영향을 준다는 점이 문제입니다.</p>

<p>상대방의 강점을 인지하지 않고 상대의 잘못을 냉담하게 지적한다면 상대방은 위협으로 느낄 수도 있으며 생산성에 악영향을 줄 수 있습니다. 연구자 <a href="https://web.archive.org/web/20120413051133/http://www.scarf360.com/files/SCARF-NeuroleadershipArticle.pdf">데이비드 록</a>은 다음처럼 설명합니다.</p>

<blockquote>
  <p>위협적인 응답은 분석적 사고, 창의적인 통찰, 문제 해결력을 저해합니다.</p>
</blockquote>

<p>즉, 가장 효과적이라고 생각하는 방식이 고통스러운 감정의 원인이 되고 실제로 팀의 발목을 붙잡는 일이 됩니다.</p>

<p>코드 리뷰에도 동일합니다. 만약 누군가의 오류를 억양 없이 지적하는 일은 역효과를 낳아 상대방의 열정을 죽이고 성장해야겠다는 동기를 짓누르는 일이 될 수 있습니다.</p>

<h3 id="인정적인-대안-1">인정적인 대안</h3>

<p>그러면 거들먹거리는 녀석이 되지 않는 것만으로는 충분하지 않습니다. 진정으로 팀 내의 긍정적 협력을 지원하려면, 한 발 더 나아가 긍정적인 감정을 통해 팀원들에게 최대한 동기를 부여하고 고통받지 않도록 해야 합니다.</p>

<p>기계에 대고 말하는 것이 아니라 사람과 대화한다는 사실을 잊지 마세요. 카렌 암스트롱은 다음처럼 <a href="http://www.worldcat.org/title/twelve-steps-to-a-compassionate-life/oclc/630500252">경고했습니다</a>.</p>

<blockquote>
  <p>"자비와 공감으로 담금질하지 않은 이성은 인간을 도덕적 공허로 이끌 수 있습니다."</p>
</blockquote>

<p>피드백으로 돌아와서, 여기에 사용할 수 있는 하나의 기법은 질문에 의지하는 방법이 있습니다. <em>진짜</em> 질문 말입니다. 위에서 이야기한 애완동물 앱의 예제 피드백은 "하지 않나요?"로 끝나고 있습니다. 문장은 질문의 형식을 빌리고 있지만 평가하는 문구에 가깝습니다. "이 화면의 제목 선택에 관해 설명해주실 수 있을까요?" 정도가 더 나은 선택지가 되겠습니다.</p>

<p>또한 비판적인 피드백도 긍정적인 피드백으로 중화할 수 있습니다. 어떤 사람들은 먼저 칭찬할 점을 언급하고, 건설적인 비판을 제공한 후 또 다른 칭찬을 하는 "샌드위치" 기법으로 피드백을 주는 것을 좋아하기도 합니다. 또 어떤 사람들은 (저 자신을 포함해서) 이를 알아채고 요점만 말하길 원하는 경우도 있습니다.</p>

<p>상대방이 어떤 성향이든, 나쁜 소식을 전달하는 것이든 자아에 상처가 될 만한 피드백을 전달하는 것이든, 가장 중요한 것은 상대방의 감정을 염두에 두고 가능한 한 적은 고통으로 당신의 관점을 어떻게 전달할 수 있을지 생각하는 것입니다.</p>

<p>저도 제 경력 대부분 동안 이 조언을 거부했기 때문에 거부 반응이 생기는 것도 어느 정도 예상이 됩니다. 영 에이프릴은 "뭐, 사람들이 진실을 대하는 방법에 대해 배워야 할 뿐이라고" 같은 말을 했었습니다. 이 말에 무언가 있는 것 같다는 생각도 듭니다. 저는 사람들과 끈끈한 관계를 쌓았다면 라포가 형성되지 않았을 때는 부적절할 수 있는 직설적인 의사소통도 어느 수준까지는 할 수 있다고 믿기도 합니다. 저는 또한 진정으로 비자아적인 프로그래밍 및 제품 개발<sup id="fnref:tn-egoless-programming" role="doc-noteref"><a href="#fn:tn-egoless-programming" class="footnote" rel="footnote">1</a></sup>에서는 피드백을 직접 줘도 괜찮다고 생각합니다. 이런 경우에는 (이론적으로) 자아가 없기 때문이죠.</p>

<p>그렇더라도 사람 일은 모르는 법입니다. 직접적인 피드백을 줘도 잘 받는 누군가가 있다 하더라도 그 피드백을 받는 그 날에 그 사람의 개가 죽었다면 평소처럼 받지 못하고 무너질지도 모릅니다. 그래서 어느 때라도 소통하는 사람들의 감정에 당신의 말이 어떤 영향을 주는지 고려하지 않아도 되는 경우는 차라리 없다고 생각해야 맞습니다.</p>

<h1 id="비관적-말투">비관적 말투</h1>

<p>마지막 독성 말투는 비관적 말투입니다. 먼저 저는 회의적인 입장도 어느 정도 팀에 존재하는 것이 건강하다고 생각하는 편이라서 그런 경우는 문제가 아닙니다.</p>

<p>문제는 거의 모든 창의적인 새 아이디어, 도구, 접근 방식에 대해서 비관적으로 대하는 경우인데 보통 "모든 것을 다 보았고" 모든 것이 최악이라고 생각하는 독기 있는 시니어 엔지니어 같은 경우가 일반적입니다.</p>

<p>"그거 안 돌아갈 거야."</p>

<p>"그거 확장 안될 거야."</p>

<p>"그 새 도구는 그냥 예전 것만큼 별로야."</p>

<p>어떤 이유에서인지 어떤 엔지니어는 종종 전 USC 총장인 스티븐 샘플이 <a href="http://www.worldcat.org/title/contrarians-guide-to-leadership/oclc/47140745">말한</a> "습관적 반대론자"에 속하기도 합니다. 그는 다음처럼 설명합니다.</p>

<blockquote>
  <p>"새로운 아이디어가 어떻게 돌아갈지 상상하는 일보다 이들은 본능적으로 왜 안 되는가에 대한 온갖 이유를 생각합니다."</p>
</blockquote>

<p>엔지니어가 비관주의의 매력을 느끼는 이유가 두 번째 문장에서 정확하게 묘사되고 있습니다. 시간을 절약하는 것 말입니다. 당신의 비관주의적 예측이 옳다면 그들의 아이디어를 거부함으로 모든 사람들의 시간을 잘 절약하고 있을 것입니다.</p>

<p>하지만 그 대신 항상 일어나는 일은, 엔지니어는 그 아이디어가 정말 실패할지 잘 모르는 상황에서도 작은 확신이라도 있으면 아이디어를 거부한다는 것입니다. 모든 사실을 검증하기도 전에 판단을 내립니다. 아마 과거에 비슷한 접근 방식이 실패한 것을 봤을 수도 있습니다. 또는 수많은 불필요한 프로젝트 관리 도구를 지난 기간 동안 봐서 어떤 새로운 도구든 제대로 동작할 거란 상상을 하지 못할 수도 있습니다. 아니면 매번 위키를 도입하려고 시도했지만, 팀이 계속 관리하지 못했기 때문에 이번에도 별다르지 않을 것으로 생각할 수도 있습니다.</p>

<p>이 모든 경험은 엔지니어가 팀에게 제공할 수 있는 유용한 정보이긴 하지만, 그렇다고 해서 제안이 실패할 것이라는 결론에 즉각적으로 닿지는 않습니다. 과거의 데이터를 알려주면서 거기에 한숨과 부정적인 말투를 더할 필요는 분명 없습니다.</p>

<p>아마 과거 프로젝트에서 레일즈를 사용하면서 한 번의 나쁜 경험이 있을 수 있습니다. 그 한 번의 경험으로 나쁜 도구라고 말할 순 없습니다. 아마도 당시 상황에 맞지 않았거나 팀이 전체적으로 이해하고 있지 못했을 수도 있습니다.</p>

<p>오랜 기간에 걸쳐 많은 프로젝트의 실패를 봐 온 엔지니어라면 실패를 예상하게 될 수도 있고, 이런 엔지니어는 그들이 말하는 모든 것에 일반적인 부정론을 투영하기도 합니다. 당신의 반대로 막다른 골목에서 시간을 낭비하는 팀을 구한 일 하나마다 아마 팀의 의욕을 꺾고 유익한 실험이 되었을 수도 있는 것을 막은 일이 10배는 많을 겁니다.</p>

<p>또한, 새로운 엔지니어가 이런 독한 기운을 뿜는 엔지니어를 경외하기도 한다는 점도 위험합니다. 이런 이유의 배경을 짐작해보면 모든 것에 불평하고 반대하는 것 보면 분명 모든 걸 다 알기 때문이라 생각하는 것으로 보입니다. 이 모든 부분이 악순환입니다.</p>

<p><em>Sidenote: 흥미롭게도 엔지니어도 최소한 한 부분에서는 낙천적인 경향을보이긴 하는데, 바로 작업을 완료하는데 걸리는 시간입니다. 이런 낙관주의적 경향의 주된 결과는 한심할 정도로 현실적이지 못한 일정표입니다.</em></p>

<h3 id="인정적인-대안-2">인정적인 대안</h3>

<p>엔지니어가 실패의 원인에 대해 경계하는 것은 유용합니다. 이런 관점이 버그와 다운타임, 보안 문제를 방지하기 때문입니다. 하지만 성공의 가능성에 대한 믿음과 잠재적으로 부정적 결과를 발견하는 능력에 균형을 찾는 일은 진정 주의해야 합니다.</p>

<p>그 이유는 숀 머피의 <a href="http://www.worldcat.org/title/optimistic-workplace-creating-an-environment-that-energizes-everyone/oclc/913164327">The Optimistic Workplace</a> 설명에서 확인할 수 있습니다.</p>

<blockquote>
  <p>"두뇌는 기쁨과 같은 긍정적 감정에 열리게 될 때, 전체적인 연결성을 명확히 볼 수 있고 문제를 해결할 수 있는 선택지를 찾을 수 있습니다."</p>
</blockquote>

<p>그리고 통계도 인용합니다.</p>

<blockquote>
  <p>"긍정적인 업무 환경에 있는 사람들은 부정적인 분위기에서 일하는 사람보다 10~30% 더 나은 결과를 냈습니다."</p>
</blockquote>

<p>아이디어를 내리깎고 새 프로젝트의 실패를 예측하기 전에 스스로 물어보세요. 정말로 이 아이디어가 실패한다고 확신하나요? 볼테르가 확신에 대해 한 말을 기억하기 바랍니다.</p>

<blockquote>
  <p>"사기꾼만 확신에 차 있습니다. 의심이란 그다지 바람직한 상태가 아닙니다. 하지만 확신이란 얼토당토않은 상태인 것입니다."</p>
</blockquote>

<p>확신하지 않으면서 왜 확신한 것처럼 말할까요? "그거 안 돌아갈 거야"는 정말 당신의 응답으로 필요한 말일까요? 이런 대답 대신에 공손하게 어떤 걱정이 있는지, 과거에 팀에서 어떤 경험을 했는지 공유하는 것은 어떨까요?</p>

<p>만약 정말 그 비운의 아이디어가 실패한다고 <em>확신</em>한다면 먼저 볼테르의 말을 다시 읽어보고, 자신의 의심을 나머지 팀원에게 의욕을 꺾지 않는 방법으로 어떻게 소통할지 고려하기 바랍니다. "<a href="https://www.behance.net/blog/the-yes-and-approach-less-ego-more-openness-more-possibility">맞아요, 그리고...</a>"로 시작하도록 답변할 만한 방법이 있을지도 생각해 봅시다. 예를 들면, "맞아요, 그 아이디어가 뛰어난 이유를 알겠어요. 그리고 이 아이디어는 이런 다른 맥락에서 더 좋은 이유가 있어요." 또는, "맞아요, 흥미로운 아이디어네요. 그리고 아마도 몇 달 후에 Y를 달성하고 나면 그 아이디어가 더 실현할 수 있겠네요." 식으로 답할 수 있을 겁니다.</p>

<p>이번에도 자신에게 인정을 베푸는 것이 도움이 됩니다. 다른 무언가로 인해 괴로워하고 있고 그 고통이 제안된 아이디어에 대해 반대하는 진짜 원인인가요? 그냥 피곤한가요? 먼저 본인에게 있는 개인적인 필요를 돌보고 나서도 그 아이디어를 여전히 반대하는지 살펴보기 바랍니다.</p>

<p>그리고 회사의 사명이나 자기 자신의 사명을 다시 상기하는 일도 도움 됩니다. 당신은 지금 왜 이 프로젝트에 종사하고 있나요? 무엇이 희망을 주나요? 그 희망을 새로운 아이디어에 대한 열린 마음으로 이어갈 수 있나요?</p>

<p>항상 모든 일이 완벽하게 잘 될 것이라고 가장할 필요는 없습니다. 하지만 새 아이디어에 대한 열린 태도는 진정으로 혁신적인 작업을 하기 위한 전제조건입니다. 또한 당신을 더 즐겁고 타인을 지지해줄 수 있는 동료로 만들 것입니다.</p>

<h2 id="미래">미래</h2>

<blockquote>
  <p>"컴퓨터 프로그래밍은 인간 활동입니다. ... 하지만 아직도 ... 많은 사람들―많은 프로그래머들―은 프로그래밍을 인간 활동으로 전혀 고려하고 있지 않습니다."</p>
</blockquote>

<p>제럴드 와인버그는 1971년에 <a href="http://www.worldcat.org/title/psychology-of-computer-programming/oclc/216809">이 글을 썼지만,</a> 여전히 이런 인식이 존재합니다. 저는 엔지니어링에서의 커뮤니케이션이 주목받을 때가 비로소 왔다고 봅니다.</p>

<p>우리가 아름다운 소프트웨어 걸작을 만드는 일을 사랑하는, 뛰어난 엔지니어라면 그와 동시에 친절하고 자비로운 사람들로 서로를 돕고 말과 글로 의사소통을 할 때는 서로의 단어로 영감을 불어넣는 사람도 될 수 있지 않을까요?</p>

<p>저는 기술 업계의 더 밝은 미래를 그립니다. 모든 종류의 독성 말투를 버리고 대신에 긍정적이고, 겸손하며, 희망차고, 명확하며, 초대하는 듯한 말투를 받아들인 미래를 말입니다. 왜 초대하는 듯한 말투냐면, 알다시피 우리가 만들고자 하는 것을 모두 만들려면 더 많은 엔지니어와 기여자가 필요하기 때문입니다.</p>

<p>프란 앨런의 <a href="http://www.worldcat.org/title/coders-at-work-reflections-on-the-craft-of-programming/oclc/535024762">Coders at Work</a> 인터뷰에서 말하는 엔지니어링처럼 말입니다.</p>

<blockquote>
  <p>"엔지니어링은 사회 전체를 위한, 변혁적인 분야입니다. 또한 우리가 하는 일에서 다양한 사람들의 참여 없이는, 우리 사회의 모든 측면에서 매력적이거나 유용하지 않은 결과를 얻게 됩니다."</p>
</blockquote>

<p>그러므로 진보적인 관점에서, 불필요하고 자아 중심적인 부정성 때문에 다른 누군가가 겁먹지 않도록 조심합시다.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:tn-egoless-programming" role="doc-endnote">
      <p>(역자 주) <a href="https://en.wikipedia.org/wiki/Egoless_programming">Egoless programming</a>. 소프트웨어의 품질에 집중하기 위해 개인적인 요인을 최소화하는 프로그래밍 방법론. <a href="#fnref:tn-egoless-programming" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="문화" /><category term="번역" /><category term="임시" /><summary type="html"><![CDATA[이 글은 April Wensel님의 Tech has a Toxic Tone Problem — Let's Fix It!의 edykim님 번역본을 수정한 번역본으로, edykim님의 사정상 원본에 반영되기 전까지 임시로 잇창명 개발 블로그에 게시합니다. edykim님의 블로그에 반영되면 이 글은 삭제한 뒤 원본 링크만 남길 예정입니다.]]></summary></entry><entry><title type="html">플로이드-워셜 알고리즘의 잃어버린 항</title><link href="https://eatchangmyeong.github.io/2023/05/02/the-missing-factor-of-floyd-warshall-algorithm.html" rel="alternate" type="text/html" title="플로이드-워셜 알고리즘의 잃어버린 항" /><published>2023-05-02T00:00:00+09:00</published><updated>2023-05-02T00:00:00+09:00</updated><id>https://eatchangmyeong.github.io/2023/05/02/the-missing-factor-of-floyd-warshall-algorithm</id><content type="html" xml:base="https://eatchangmyeong.github.io/2023/05/02/the-missing-factor-of-floyd-warshall-algorithm.html"><![CDATA[<p><!-- &#40;과 &#41;은 VS Code에서 맞는 괄호 하이라이팅을 부숴먹지 않기 위해(애초에 왜 하이라이팅이 잘 안 되는지는 모르겠는데) 닫는 괄호 대신 사용했습니다. 혹시 EatChangmyeong/EatChangmyeong.github.io에서 오셨다면 이 건으로 이슈나 풀 리퀘를 넣지 말아 주세요. --></p>

<p><strong>이 글에는 백준 <a href="https://www.acmicpc.net/problem/15089">15089번</a>/<a href="https://www.acmicpc.net/problem/16388">16388번 "Is-A? Has-A? Who Knowz-A?"</a>의 풀이 스포일러가 포함되어 있습니다.</strong></p>

<p>2022년 4월 중후반쯤에 백준 <a href="https://www.acmicpc.net/problem/16388">16388번 "Is-A? Has-A? Who Knowz-A?"</a>라는 문제를 찾아서 푼 적이 있습니다. 3달 뒤에는 <a href="https://www.acmicpc.net/problem/15089">15089번</a>이 완전히 같은 문제인 것을 확인하고 16388번 코드를 복사해서 제출했습니다. 현재는 16388번이 레이팅을 주지 않는 것으로 처리되어 있습니다.</p>

<h1 id="문제-옮겨적기-번역문">문제 옮겨적기 (번역문)</h1>

<p>한국어 블로그이기 때문에 독자의 편의를 위해 문제 본문을 번역합니다. 원문은 백준 링크에서 확인해 주세요.</p>

<blockquote>
  <p><strong>문제</strong></p>

  <p>Is-a와 has-a 관계는 객체지향 프로그래밍에서 친숙한 두 가지 개념이다. 두 클래스 A와 B에 대해, A가 B의 서브클래스이면 A is-a B, A의 필드 중 하나가 타입 B를 가지면 A has-a B라고 한다. 예를 들어, 가상의 객체지향 언어(ICPC++라고 하자)에 그림 E.1과 같은 코드를 작성했다고 하면 클래스 <code class="language-plaintext highlighter-rouge">Day</code>는 is-a <code class="language-plaintext highlighter-rouge">Time</code>이고, 클래스 <code class="language-plaintext highlighter-rouge">Appointment</code>는 동시에 is-a <code class="language-plaintext highlighter-rouge">DateBook</code>이자 is-a <code class="language-plaintext highlighter-rouge">Reminder</code>이며, 클래스 <code class="language-plaintext highlighter-rouge">Appointment</code>는 has-a <code class="language-plaintext highlighter-rouge">Day</code>이다.</p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>class Day extends Time        class Appointment extends Datebook, Reminder
{                             {
    ...                           private Day date;
}                                 ...
                              }
</code></pre></div>  </div>

  <p>그림 E.1: ICPC++의 클래스 정의 2개.</p>

  <p>이 두 관계는 추이적이다. 예를 들어 A is-a B이고 B is-a C이면 A is-a C임을 알 수 있다. 이는 앞의 문장에서 is-a를 모두 has-a로 바꾸어도 성립한다. Is-a와 has-a의 조합도 비슷한 성질을 띤다. 위의 예제 코드에서는 <code class="language-plaintext highlighter-rouge">Appointment</code> has-a <code class="language-plaintext highlighter-rouge">Day</code>이고 <code class="language-plaintext highlighter-rouge">Day</code> is-a <code class="language-plaintext highlighter-rouge">Time</code>이므로 <code class="language-plaintext highlighter-rouge">Appointment</code> has-a <code class="language-plaintext highlighter-rouge">Time</code>이 성립한다. 이와 비슷하게, 만약 클래스 <code class="language-plaintext highlighter-rouge">DateBook</code>이 has-a <code class="language-plaintext highlighter-rouge">Year</code>라면 <code class="language-plaintext highlighter-rouge">Appointment</code> is-a <code class="language-plaintext highlighter-rouge">DateBook</code>이므로 <code class="language-plaintext highlighter-rouge">Appointment</code> has-a <code class="language-plaintext highlighter-rouge">Year</code>도 성립한다.</p>

  <p>이 문제에서는 여러 is-a 및 has-a 관계가 주어지고 A is/has-a B 꼴의 질의가 여러 개 주어진다. 각 질의가 참인지 거짓인지 판정하는 코드를 작성하시오.</p>

  <p><strong>입력</strong></p>

  <p>첫째 줄에 두 정수 n과 m(1 ≤ n, m ≤ 10,000)이 주어지며, n은 주어지는 is-a 및 has-a 관계의 개수, m은 질의의 개수이다. 다음 n줄에 걸쳐 c<sub>1</sub> r c<sub>2</sub>의 꼴로 한 줄에 하나씩 관계가 주어지며, c<sub>1</sub>과 c<sub>2</sub>는 한 단어의 클래스 이름, r은 문자열 "is-a" 혹은 "has-a"이다. 그 다음에는 한 줄에 하나씩 m개의 질의가 같은 꼴로 주어진다. 위의 n + m줄에 주어지는 클래스 이름은 최대 500종류이며, 마지막 m줄에 주어지는 모든 클래스 이름은 처음 n줄에 최소한 한 번 주어진다. 주어진 모든 클래스 사이의 is-a 및 has-a 관계는 주어진 n개의 관계로부터 추론할 수 있다. Is-a 관계는 "x is-a x"인 자명한 경우를 제외하면 순환 참조를 이루지 않는다.</p>

  <p><strong>출력</strong></p>

  <p>각 질의에 대해 1부터 시작하는 질의 번호 다음에 그 질의가 참인지 거짓인지를 출력한다.</p>

  <p><strong>예제 입력 1</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>5 5
Day is-a Time
Appointment is-a Datebook
Appointment is-a Reminder
Appointment has-a Day
Datebook has-a Year
Day is-a Time
Time is-a Day
Appointment has-a Time
Appointment has-a Year
Day is-a Day
</code></pre></div>  </div>

  <p><strong>예제 출력 1</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Query 1: true
Query 2: false
Query 3: true
Query 4: true
Query 5: true
</code></pre></div>  </div>
</blockquote>

<h1 id="최단거리가-왜-거기서-나와">최단거리가 왜 거기서 나와</h1>

<p>문제를 잘 읽어보면 is-a와 has-a가 다음 성질을 가짐을 알 수 있습니다. 모든 클래스 A, B, C에 대해...</p>

<ul>
  <li><strong>is-a</strong>
    <ul>
      <li><strong>반사성</strong>: A is-a A</li>
      <li><strong>반대칭성</strong>: A is-a B이고 B is-a A이면 A = B
        <ul>
          <li>is-a가 반대칭 관계가 아닐 경우 문제에서 금지한 순환 is-a 관계가 발생합니다.</li>
          <li>이 글에서는 중요하게 다루지 않습니다.</li>
        </ul>
      </li>
      <li><strong>추이성</strong>: A is-a B이고 B is-a C이면 A is-a C</li>
    </ul>
  </li>
  <li><strong>has-a</strong>
    <ul>
      <li><strong>추이성</strong>: A has-a B이고 B has-a C이면 A has-a C</li>
    </ul>
  </li>
  <li>A is-a B이고 B has-a C이면 A has-a C</li>
  <li>A has-a B이고 B is-a C이면 A has-a C</li>
</ul>

<p>이 문제에 is-a만 나왔다면 최단 경로 알고리즘(특히 플로이드-워셜 알고리즘)을 아주 조금만 응용해서 풀 수 있었겠지만 하필 has-a가 is-a와 상호작용을 하기 때문에 이렇게 풀기는 어렵습니다. 최단 경로 알고리즘 대신 무슨 방법으로 풀 수 있을지 알고리즘 분류를 살펴봅시다.</p>

<ul>
  <li>그래프 이론</li>
  <li>그래프 탐색</li>
  <li>깊이 우선 탐색</li>
  <li><strong>플로이드-워셜</strong> (???)</li>
</ul>

<p>아무래도 플로이드-워셜 알고리즘이 정해가 맞는 것 같습니다. 그런데 구체적으로 플로이드-워셜 알고리즘을 어떻게 이용한다는 걸까요?</p>

<p>저는 문제를 처음 풀 당시에 이 방법을 엄밀한 증명 없이 사용했습니다.</p>

<ul>
  <li>모든 간선은 다음과 같은 네 가지 가중치를 가집니다.
    <ul>
      <li><strong>is-a</strong> (이하 \(i\))</li>
      <li><strong>has-a</strong> (이하 \(h\))</li>
      <li><strong>is-a</strong>이면서 <strong>has-a</strong> (이하 \(ih\))</li>
      <li><strong>is-a</strong>도 <strong>has-a</strong>도 아님 (이하 \(0\))</li>
    </ul>
  </li>
  <li>알고리즘의 재귀 부분인 \(\min (adj[a][b], adj[a][c] + adj[c][b])\) 중...
    <ul>
      <li>
        <p>\(+\) 연산을 "상속" 연산으로 바꿉니다. 이 연산은 다음과 같이 정의합니다.</p>

        <table>
          <thead>
            <tr>
              <th> </th>
              <th>\(0\)</th>
              <th>\(i\)</th>
              <th>\(h\)</th>
              <th>\(ih\)</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>\(0\)</td>
              <td>\(0\)</td>
              <td>\(0\)</td>
              <td>\(0\)</td>
              <td>\(0\)</td>
            </tr>
            <tr>
              <td>\(i\)</td>
              <td>\(0\)</td>
              <td>\(i\)</td>
              <td>\(h\)</td>
              <td>\(ih\)</td>
            </tr>
            <tr>
              <td>\(h\)</td>
              <td>\(0\)</td>
              <td>\(h\)</td>
              <td>\(h\)</td>
              <td>\(h\)</td>
            </tr>
            <tr>
              <td>\(ih\)</td>
              <td>\(0\)</td>
              <td>\(ih\)</td>
              <td>\(h\)</td>
              <td>\(ih\)</td>
            </tr>
          </tbody>
        </table>

        <ul>
          <li>is-a와 has-a를 조합하는 모든 경우의 수를 연산자로 표현한 것입니다.</li>
        </ul>
      </li>
      <li>
        <p>\(\min\) 연산을 다음 정의로 바꿉니다.</p>

        <table>
          <thead>
            <tr>
              <th> </th>
              <th>\(0\)</th>
              <th>\(i\)</th>
              <th>\(h\)</th>
              <th>\(ih\)</th>
            </tr>
          </thead>
          <tbody>
            <tr>
              <td>\(0\)</td>
              <td>\(0\)</td>
              <td>\(i\)</td>
              <td>\(h\)</td>
              <td>\(ih\)</td>
            </tr>
            <tr>
              <td>\(i\)</td>
              <td>\(i\)</td>
              <td>\(i\)</td>
              <td>\(ih\)</td>
              <td>\(ih\)</td>
            </tr>
            <tr>
              <td>\(h\)</td>
              <td>\(h\)</td>
              <td>\(ih\)</td>
              <td>\(h\)</td>
              <td>\(ih\)</td>
            </tr>
            <tr>
              <td>\(ih\)</td>
              <td>\(ih\)</td>
              <td>\(ih\)</td>
              <td>\(ih\)</td>
              <td>\(ih\)</td>
            </tr>
          </tbody>
        </table>

        <ul>
          <li>is-a와 has-a를 독립적으로 OR하는 연산자입니다.</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>\(\min(a, b) \le a\)이고 \(\min(a, b) \le b\)인 것으로 생각합니다. 특히 \(\min(a, b) = a\)이면 \(a \le b\)입니다.
    <ul>
      <li>\(\min(i, h) = ih\)인데, 이 경우에는 \(i\)와 \(h\)를 아예 비교할 수 <strong>없습니다</strong>.</li>
    </ul>
  </li>
  <li>\(adj[a][b]\)를 \(a = b\)이면 \(i\), \(a \ne b\)이면 \(0\)으로 초기화합니다.</li>
  <li>이외에는 플로이드-워셜 알고리즘을 그대로 씁니다.</li>
</ul>

<p>이 방법에는 세 가지 꺼림칙한 점이 있습니다.</p>

<ul>
  <li><strong>간선의 가중치가 실수가 아니다.</strong> 가중치가 음이 아닌 실수면 직관적으로 물리적 최단 거리를 생각할 수 있고 음의 실수가 있어서 최단 거리가 없을 가능성이 있더라도 최소한 "최단 거리"라는 개념 자체는 유지되는데, is-a와 has-a 가중치에는 그런 의미조차도 남아 있지 않습니다.</li>
  <li><strong>비교할 수 없는 가중치의 쌍이 있다.</strong> 실수는 어떤 쌍을 골라도 비교할 수 있지만, is-a와 has-a는 전혀 다른 개념이기 때문에 애초에 비교가 불가능합니다.</li>
  <li><strong>"최단 경로"가 하나로 결정되지 않는다.</strong> 이건 위의 비교 불가능 문제에서 파생되는 문제인데, 예를 들어서 A is-a B, B has-a D, A is-a C, C is-a D라면 A에서 D까지의 "최단 경로"는 \(ih\)인데(A is-a D이고 A has-a D이므로), A-B-D 경로는 \(h\)이고 A-C-D 경로는 \(i\)이기 때문에 어느 하나가 최단 경로를 완전히 결정하지 않습니다.</li>
</ul>

<p>그럼에도 불구하고 저는 이 코드를 그대로 제출했고 <strong>맞았습니다!!</strong>를 받았습니다. 그 뒤로 7달 동안 이 문제를 잊고 살다가 갑자기 다시 생각났고, 이번에는 혼자서 생각하다가 막혀서 <a href="https://cs.stackexchange.com/q/155857/155740">컴퓨터과학 스택 익스체인지의 도움</a>을 받기로 합니다.</p>

<h1 id="대수적-경로-문제">대수적 경로 문제</h1>

<p>알고 보니 실수가 아닌 가중치로 최단 경로를 구하는 문제에는 이미 <strong>algebraic path problem</strong>이라는 이름이 붙어 있었습니다. 이 글에서는 "대수적 경로 문제"라고 번역하겠습니다.</p>

<p>위에 링크한 질문의 답변을 보면 아무 가중치로나 최단 경로를 구할 수는 없고, 특정한 조건, 구체적으로는 <em>덧셈이 멱등이고 클레이니 스타가 정의된 반환 구조</em>를 이루어야 한다고 합니다. 갑자기 낮선 단어를 막 쓰는데 하나씩 짚어보면 단순한 조건들로 이루어져 있고 그 조건들 하나하나가 이유 없이 있는 것이 아니니 너무 겁먹지 않으셔도 됩니다.</p>

<h2 id="대수적-구조-반환">대수적 구조 '반환'</h2>

<p>"덧셈이 멱등이고 클레이니 스타가 정의된 반환"이 구체적으로 무슨 뜻인지 알아봅시다.</p>

<ul>
  <li><strong>"<a href="https://ko.wikipedia.org/wiki/반환_(수학)">반환</a>"</strong>(semiring): 어떤 집합 위에 다음 성질을 가지는 두 연산을 정의할 수 있으면 그 집합과 연산을 통틀어 반환이라고 합니다.
    <ul>
      <li>\(\oplus\)
        <ul>
          <li><strong>교환법칙</strong>: \(a \oplus b = b \oplus a\)</li>
          <li><strong>결합법칙</strong>: \((a \oplus b) \oplus c = a \oplus (b \oplus c)\)</li>
          <li><strong>항등원 \(0\)의 존재</strong>: \(0 \oplus a = a \oplus 0 = a\)</li>
          <li>역원은 존재하지 않아도 됩니다. 역원이 존재하면 <a href="https://ko.wikipedia.org/wiki/환_(수학)">환</a>이 됩니다.</li>
        </ul>
      </li>
      <li>\(\otimes\)
        <ul>
          <li><strong>결합법칙</strong>: \((a \otimes b) \otimes c = a \otimes (b \otimes c)\)</li>
          <li><strong>항등원 \(1\)의 존재</strong>: \(1 \otimes a = a \otimes 1 = a\)</li>
        </ul>
      </li>
      <li><strong>\(\oplus\)에 대한 \(\otimes\)의 분배법칙</strong>
        <ul>
          <li>\(a \otimes (b \oplus c) = (a \otimes b) \oplus (a \otimes c)\)</li>
          <li>\((a \otimes b) \oplus c = (a \otimes c) \oplus (b \otimes c)\)</li>
        </ul>
      </li>
      <li><strong>\(0\)이 \(\otimes\)의 흡수원</strong>: \(0 \otimes a = a \otimes 0 = 0\)</li>
    </ul>
  </li>
  <li><strong>"덧셈이 <a href="https://ko.wikipedia.org/wiki/멱등법칙#이항연산,_멱등원">멱등</a>이고"</strong>: \(a \oplus a = a\)</li>
  <li><strong>"<a href="https://en.wikipedia.org/wiki/Semiring#Complete_star_semirings">클레이니 스타</a>가 정의된"</strong>: \(a^* = 1 \oplus a \oplus a^2 \oplus a^3 \oplus \cdots\)<sup id="fnref:fn-kleene-algebra" role="doc-noteref"><a href="#fn:fn-kleene-algebra" class="footnote" rel="footnote">1</a></sup>
    <ul>
      <li>원래는 이걸 정의하기 전에 무한합에 대한 몇 가지 성질을 먼저 보여야 하는 것 같은데 여기서는 생략하겠습니다.</li>
    </ul>
  </li>
</ul>

<p>우선 반환에 대한 설명부터 해보자면, 대표적인 반환 중 하나로는 <strong>음이 아닌 정수</strong>의 덧셈과 곱셈을 들 수 있습니다. 자연수끼리 덧셈과 곱셈을 할 때 교환법칙, 결합법칙, 분배법칙이 성립하고 항등원이 있는 것은<sup id="fnref:fn-natural-number" role="doc-noteref"><a href="#fn:fn-natural-number" class="footnote" rel="footnote">2</a></sup> 학교에서 배우기 때문에(저는 그렇게 기억하고 있습니다) 증명하는 것이 그렇게 어렵지는 않을 것 같습니다. 최단 거리 알고리즘에서는 \(\infty\)로 초기화를 하는 경우도 흔하기 때문에 음이 아닌 정수에 \(\infty\)를 추가할 수 있고, \(\infty + x = \infty\), \(\infty \times 0 = 0 \times \infty = 0\), 0이 아닌 \(x\)에 대해 \(\infty \times x = x \times \infty = \infty\)로 정의하면 이렇게 정의한 <strong><a href="https://en.wikipedia.org/wiki/Extended_natural_numbers">확장 자연수</a></strong>도 반환을 이룹니다. 그런데 이 정의를 보고 나면 자연스러운 의문이 들 것 같습니다.</p>

<blockquote>
  <p>"최단" 경로를 구한다면서 \(\min\)은 어디 갔나요? 대소 비교는 어떻게 해요?</p>
</blockquote>

<h3 id="열대-반환">열대 반환</h3>

<p>사실 \(\oplus\)가 덧셈이고 \(\otimes\)가 곱셈이라는 고정관념을 버리면 확장 자연수에 대해 이 조건을 만족하는 연산자 쌍을 하나 더 찾을 수 있습니다. 바로 \(a \oplus b = \min(a, b)\), \(a \otimes b = a + b\)입니다.</p>

<ul>
  <li>\(\min\)
    <ul>
      <li><strong>교환법칙</strong>: \(\min(a, b) = \min(b, a)\)</li>
      <li><strong>결합법칙</strong>: \(\min(\min(a, b), c) = \min(a, \min(b, c))\)</li>
      <li><strong>항등원 \(\infty\)의 존재</strong>: \(\min(\infty, a) = \min(a, \infty) = a\)</li>
      <li><strong>멱등법칙</strong>: \(\min(a, a) = a\)
        <ul>
          <li>반환이 되기 위한 조건은 아니지만, 나중에 필요하기 때문에 추가했습니다.</li>
        </ul>
      </li>
    </ul>
  </li>
  <li>덧셈 (수식 생략)
    <ul>
      <li><strong>결합법칙</strong></li>
      <li><strong>항등원 \(0\)의 존재</strong></li>
    </ul>
  </li>
  <li><strong>\(\min\)에 대한 덧셈의 분배법칙</strong>
    <ul>
      <li>\(a + \min(b, c) = \min(a + b, a + c)\)</li>
      <li>\(\min(a, b) + c = \min(a + c, b + c)\)</li>
    </ul>
  </li>
  <li><strong>\(\infty\)가 덧셈의 흡수원으로 작용</strong>: \(\infty + a = a + \infty = \infty\)</li>
</ul>

<p>이것도 나무랄 데 없는 반환이네요. 덧셈과 곱셈 대신 최솟값과 덧셈 연산이 있는 이 반환에는 <strong><a href="https://en.wikipedia.org/wiki/Tropical_semiring">열대 반환</a></strong>(tropical semiring)이라는 이름이 붙어 있습니다.<sup id="fnref:fn-tropical-etymology" role="doc-noteref"><a href="#fn:fn-tropical-etymology" class="footnote" rel="footnote">3</a></sup></p>

<h2 id="min-연산과-대소-비교의-관계">\(\min\) 연산과 대소 비교의 관계</h2>

<p>대소 비교도 \(\min\)으로부터 도출할 수 있는데, 그 과정을 설명하려면 잠깐 삼천포로 빠져야 합니다.</p>

<p>1줄 요약: \(a \le b\)를 \(\min(a, b) = a\)로 정의하면 됩니다.</p>

<h3 id="대수적-구조-반격자">대수적 구조 '반격자'</h3>

<p><strong><a href="https://en.wikipedia.org/wiki/Semilattice">반격자</a></strong>(semilattice)는 덧셈과 곱셈 대신 대소 비교를 일반화한 대수적 구조인데<sup id="fnref:fn-semilattice" role="doc-noteref"><a href="#fn:fn-semilattice" class="footnote" rel="footnote">4</a></sup>, 서로 다른 두 가지 정의가 있습니다.</p>

<h4 id="순서론적-정의">순서론적 정의</h4>

<p>백준 <a href="https://www.acmicpc.net/problem/7568">7568번 "덩치"</a> 문제를 풀어본 적이 있나요? 이 문제에서는 몸무게와 키의 순서쌍으로 덩치를 나타내는데, \((56, 177) &gt; (45, 165)\)처럼 대소 비교가 되는 쌍도 있지만 \((45, 181)\)과 \((55, 173)\)처럼 그렇지 <em>않은</em> 쌍도 있습니다. 이렇게 순서의 개념은 있지만 비교가 가능하지 않은 쌍도 허용하는 것을 <strong><a href="https://ko.wikipedia.org/wiki/부분_순서_집합">부분 순서</a></strong>라고 합니다. 엄밀하게는 다음 성질을 만족하는 이항 관계를 부분 순서라고 합니다.</p>

<ul>
  <li><strong>추이성</strong>: \(a \le b\)이고 \(b \le c\)이면 \(a \le c\)</li>
  <li><strong>반사성</strong>: \(a \le a\)</li>
  <li><strong>반대칭성</strong>: \(a \le b\)이고 \(b \le a\)이면 \(a = b\)</li>
</ul>

<p>부분 순서가 있는 어떤 집합에 다음 성질을 가지는 연산 \(\wedge\)를 정의할 수 있으면 그 집합과 연산을 통틀어 반격자라고 합니다.</p>

<ul>
  <li>\(a \wedge b\)는 \(a\)와 \(b\)의 하한이다.
    <ul>
      <li>\(a \wedge b \le a\)</li>
      <li>\(a \wedge b \le b\)</li>
      <li>\(x \le a\)이고 \(x \le b\)이면 \(x \le a \wedge b\)</li>
    </ul>
  </li>
</ul>

<h4 id="추상대수학적-정의">추상대수학적 정의</h4>

<p>어떤 집합에 다음 성질을 만족하는 연산 \(\wedge\)를 정의할 수 있으면 그 집합과 연산을 통틀어 반격자라고 합니다.</p>

<ul>
  <li><strong>교환법칙</strong>: \(a \wedge b = b \wedge a\)</li>
  <li><strong>결합법칙</strong>: \((a \wedge b) \wedge c = a \wedge (b \wedge c)\)</li>
  <li><strong>멱등법칙</strong>: \(x \wedge x = x\)</li>
</ul>

<h3 id="한-정의에서-다른-정의로">한 정의에서 다른 정의로</h3>

<p>겉보기에는 두 정의가 전혀 달라 보이지만, 사실 두 정의는 <em>동치</em>입니다. 즉, 한 정의에서 다른 정의를 도출할 수 있습니다.</p>

<p>둘 중 이 글에서는 추상대수학적 정의에서 순서론적 정의로 넘어가는 것에 관심이 있습니다. \(a \le b\)를 \(a \wedge b = a\)로 정의하면 \(\wedge\) 연산의 세 성질이 각각 \(\le\)의 성질 하나씩으로 바뀝니다.</p>

<ul>
  <li><strong>결합법칙과 추이성</strong>: \(a \wedge b = a\), \(b \wedge c = b\)라 하면 \((a \wedge b) \wedge c = a \wedge c\)인데 \(a \wedge (b \wedge c) = a \wedge b = a\)이므로 \(a \wedge c = a\)</li>
  <li><strong>멱등법칙과 반사성</strong>: \(a \wedge a = a\)에서 바로 \(a \le a\)를 유추할 수 있습니다.</li>
  <li><strong>교환법칙과 반대칭성</strong>: \(a \wedge b = a\), \(b \wedge a = b\)라 하면 \(a \wedge b = b \wedge a\)이므로 \(a = b\)</li>
</ul>

<p>이렇게 추상대수학적으로 올바르게 정의한 \(\wedge\)가 있으면 항상 올바른 부분 순서 \(\le\)를 얻을 수 있습니다. <strong>연산자를 정의했는데 대소 비교가 따라온 것</strong>입니다.</p>

<p>\(\min\)이 위의 세 성질을 만족함은 이미 <a href="#열대-반환">위에서</a> 보였으니, \(\le\) 연산자도 자연스럽게 사용할 수 있습니다. \(\min\) 연산이 어디 간 게 아니라 \(\oplus\)에 줄곧 숨어 있었던 것입니다.</p>

<h3 id="삼천포의-삼천포">삼천포의 삼천포</h3>

<p>반격자가 왜 "반"격자인지 설명을 안 하고 넘어가면 찜찜하실 분이 있을 것 같아서 언급하는 건데, 똑같은 부분 순서로 하한 연산 \(\wedge\)뿐만 아니라 상한 연산 \(\vee\)도 정의할 수 있으면 그걸 <strong><a href="https://ko.wikipedia.org/wiki/격자_(순서론)">격자</a></strong>(lattice)라고 합니다.</p>

<h2 id="대수적-성질에서-경로-연산으로">대수적 성질에서 경로 연산으로</h2>

<p>기존 플로이드-워셜 알고리즘의 두 연산 중 \(\min\)이 \(\oplus\)라면, 나머지 \(+\)은 자연스럽게 \(\otimes\)에 대응시킬 수 있습니다. 이 점을 염두에 두고 두 연산의 성질이 대수적 경로 문제와 어떻게 관련이 있는지 정리하면 이렇습니다.</p>

<ul>
  <li>\(\oplus\)는 <strong>두 경로의 최솟값</strong>을 일반화합니다.
    <ul>
      <li><strong>교환법칙</strong>: 두 경로를 비교하는 데 순서가 상관이 있으면 어색하기 때문에 필요합니다.</li>
      <li><strong>결합법칙</strong>: 셋 이상의 경로를 비교할 때 순서가 상관이 있으면 어색하기 때문에 필요합니다.</li>
      <li><strong>멱등법칙</strong>: 이 법칙을 만족해야 최솟값 연산이 순서론적인 의미를 가집니다.
        <ul>
          <li>모든 순서는 뒤집어도 올바른 순서가 되기 때문에 대수적 경로 문제의 맥락에서는 \(1 \le 0\)인지도 확인해야 합니다(부등호 방향에 유의).</li>
        </ul>
      </li>
      <li><strong>항등원 \(0\)의 존재</strong>: 항등원이 있어야 "0개의 경로의 최솟값", 즉 아예 연결되지 않은 정점 사이의 거리가 의미를 가집니다.</li>
    </ul>
  </li>
  <li>\(\otimes\)는 두 경로를 순서대로 이용한 거리, 즉 <strong>경유 연산</strong>을 일반화합니다.
    <ul>
      <li><strong>결합법칙</strong>: 이 법칙을 만족해야 셋 이상의 간선을 순서대로 이용하는 것이 의미를 가집니다.</li>
      <li><strong>항등원 \(1\)의 존재</strong>: 항등원이 있어야 "0개의 간선을 경유한 거리", 즉 아무 데도 가지 않고 멈춰 있는 자명한 경로의 거리가 의미를 가집니다.</li>
    </ul>
  </li>
  <li><strong>분배법칙</strong>: 이 법칙을 만족해야 여러 경로를 한 번에 연산하는 것(즉, 동적 계획법)이 의미를 가집니다.</li>
  <li><strong>\(0\)이 \(\otimes\)의 흡수원</strong>: 이 법칙을 만족해야 간선이 없는 정점으로 최단 경로가 "넘치지" 않습니다.</li>
</ul>

<p>이제 대수적 경로 문제와 "거리"의 개념을 엄밀하게 정의할 수 있습니다.</p>

<ul>
  <li>어떤 경로의 거리는 그 경로를 이루는 간선들의 가중치를 모두 \(\otimes\)한 것입니다.</li>
  <li>여러 경로의 최단 거리는 그 경로들의 거리를 모두 \(\oplus\)한 것입니다.</li>
  <li>정점 A와 B 사이의 최단 거리는 A에서 출발해서 B에 도착하는 모든 경로의 최단 거리입니다.</li>
  <li>대수적 경로 문제는 모든 정점 쌍의 최단 거리를 구하는 문제입니다.</li>
</ul>

<p>그런데 사실 이 정의에는 문제가 있습니다. 두 정점 사이에 <strong>사이클이 있는 경로</strong>가 있으면 그 사이클의 거리 \(w\)에 대해 \(1 \oplus w \oplus (w \otimes w) \oplus (w \otimes w \otimes w) \oplus \cdots\)의 무한합을 구해야 합니다. 저희 아직 무한합을 엄밀하게 정의 안 하지 않았나요?</p>

<h2 id="클레이니-스타로-무한합-다루기">클레이니 스타로 무한합 다루기</h2>

<p>사실 "특정한 조건" 중에 빠진 것이 하나 있습니다. 다시 읽어볼까요?</p>

<blockquote>
  <p>특정한 조건, 구체적으로는 덧셈이 멱등이고 <strong>클레이니 스타</strong>가 정의된 반환 구조를 이루어야 한다고 합니다.</p>
</blockquote>

<p>클레이니 스타 \(w^*\)가 바로 위에서 엄밀하게 정의 안 하고 넘어갔던 무한합 \(1 \oplus w \oplus (w \otimes w) \oplus (w \otimes w \otimes w) \oplus \cdots\)입니다. 즉, 클레이니 스타는 <strong>사이클의 최단 거리</strong>를 일반화합니다.</p>

<p>클레이니 스타가 항상 정의되는 것은 아닙니다. 예를 들어 아까 보았던 열대 반환에서 임의의 \(x\)에 대해 \(x^* = \min(0, x, x + x, x + x + x, \cdots) = 0\)이지만, 이를 음의 정수까지(\(-\infty\)를 빼고) 확장하면 음의 정수 \(-x\)에 대해 \((-x)^* = \min(0, -x, -x - x, -x - x - x, \cdots)\)는 정의되지 <strong>않습니다</strong>. 즉, 클레이니 스타가 정의되지 않는 경우는 <strong>음의 사이클</strong>을 일반화합니다.</p>

<p>사실 정수나 실수로 최단 거리를 구할 때는 굳이 사이클이 있는 경로를 확인하지 않는데, 이는 사이클의 거리 \(x\)가 음이 아니면 \(x^* = \min(0, x, x + x, x + x + x, \cdots) = 0\)이 그 사이클을 아예 지나가지 않는 것보다 나을 바가 없고, 음이면 최단 경로를 구하는 것이 의미가 없기 때문입니다. 일반적으로 \(\otimes\)의 항등원 \(1\)과 \(x^*\)가 정의되는 모든 \(x\)에 대해 \(1 \le x^*\)이면 사이클 계산을 생략하는 기존 알고리즘을 써도 문제 없이 잘 돌아갑니다. 역으로, 이 식이 성립하지 않으면 기존의 알고리즘을 사용할 수 <em>없고</em> 더 일반적인 알고리즘을 고안해서 써야 합니다.</p>

<h1 id="객체지향-가중치도-최단-경로를-구할-수-있을까">객체지향 가중치도 최단 경로를 구할 수 있을까?</h1>

<p>이제 제가 제일 궁금해하던 질문에 답해 보겠습니다. <strong>플로이드-워셜 알고리즘에 객체지향 가중치를 넣어도 잘 돌아갈까요?</strong></p>

<p>이 질문에 "예"라고 답할 조건은 다음과 같습니다.</p>

<ul>
  <li>반환 구조
    <ul>
      <li>\(\min\)의 교환법칙</li>
      <li>\(\min\)의 결합법칙</li>
      <li>\(\min\)의 항등원 \(0\)의 존재</li>
      <li>상속 연산의 결합법칙</li>
      <li>상속 연산의 항등원 \(i\)의 존재</li>
      <li>\(\min\)에 대한 상속 연산의 분배법칙</li>
      <li>\(0\)이 상속 연산의 흡수원으로 작용</li>
      <li>위의 7가지 조건은 전부 Haskell로 반례를 찾아서 없는 것을 확인했습니다. 특히 교환법칙과 항등원은 위의 연산자 표를 읽어서 쉽게 확인할 수 있습니다.</li>
    </ul>
  </li>
  <li>\(\min\)이 멱등
    <ul>
      <li>연산자 표의 대각선을 읽어서 쉽게 검증할 수 있습니다.</li>
    </ul>
  </li>
  <li>클레이니 스타가 정의됨
    <ul>
      <li>\(0^* = \min(i, i \; \mathrm{inherit} \; 0 = 0, 0 \; \mathrm{inherit} \; 0 = 0, \cdots) = i\)</li>
      <li>\(i^* = \min(i, i \; \mathrm{inherit} \; i = i, \cdots) = i\)</li>
      <li>\(h^* = \min(i, i \; \mathrm{inherit} \; h = ih, ih \; \mathrm{inherit} \; h = ih, \cdots) = ih\)</li>
      <li>\(ih^* = \min(i, i \; \mathrm{inherit} \; ih = ih, ih \; \mathrm{inherit} \; ih = ih, \cdots) = ih\)</li>
      <li>즉, \(x^* = \min(i, x)\)가 성립합니다.</li>
    </ul>
  </li>
</ul>

<p>이거 말고도 또 있습니다.</p>

<ul>
  <li>\(x^*\)가 정의될 경우 \(x^* \ge i\)
    <ul>
      <li>\(0^* = i \ge i\)</li>
      <li>\(i^* = i \ge i\)</li>
      <li>\(h^* = h \ngeq i\)... 잠깐만?</li>
    </ul>
  </li>
</ul>

<p><img src="/assets/post-images/boj-15089-fireplace.png" alt="solved.ac 디스코드 서버의 포스트 &quot;15089, 16388 Is-A? Has-A? Who Knows-A? - 난이도 및 태그 논의&quot; 스크린샷" /></p>

<p>저는 결국 <strong>플로이드-워셜이 정해가 아니라는</strong> 충격적인 진실을 마주하고야 말았습니다. 사실 문제 제한이 작아서 DFS로도 충분히 풀린다는 걸 지금까지 간과하긴 했는데 이번 글에서는 대수적 경로 문제 얘기만 하기로 했으니 무시하겠습니다.</p>

<h1 id="wfgj-알고리즘">WFGJ 알고리즘</h1>

<p>그렇다고 문제를 아예 못 푸는 건 아니고, 아까 언급했듯이 더 일반적인 알고리즘을 고안해서 쓰면 됩니다. 다행히 S. Rajopadhye의 논문 <a href="https://www.cs.colostate.edu/~cs575dl/Sp2015/Lectures/APP.pdf">The Algebraic Path Problem</a>에서 저자가 "WFGJ 알고리즘"이라고 명명한 알고리즘의 설명을 읽어볼 수 있습니다. 이 논문은 Günter Rote의 <a href="https://page.mi.fu-berlin.de/rote/Papers/pdf/A+systolic+array+algorithm+for+the+algebraic+path+problem+(shortest+paths;+matrix+inversion).pdf">A Systolic Array Algorithm for the Algebraic Path Problem (Shortest Paths; Matrix Inversion)</a>을 요약한 것이고, WFGJ 알고리즘이라는 이름은 Warshall의 추이 폐포 알고리즘, Floyd의 최단 거리 알고리즘(플로이드-워셜 알고리즘), Gauss-Jordan의 역행렬 알고리즘이 모두 이 알고리즘의 특수한 경우라는 의미로 첫 글자를 딴 것입니다.</p>

<h2 id="플로이드-워셜-알고리즘-돌아보기">플로이드-워셜 알고리즘 돌아보기</h2>

<p>WFGJ 알고리즘을 이해하려면 플로이드-워셜 알고리즘을 어떻게 유도했는지를 먼저 이해해야 합니다. 우선 몇 가지 개념과 표기를 정의하겠습니다. 정점에는 1번부터 \(n\)번까지 번호가 붙어 있습니다.</p>

<ul>
  <li>두 경로 \(x\)와 \(y\)에 대해 \(x\)의 끝 정점과 \(y\)의 시작 정점이 같다면 두 경로를 이어붙이는 연산을 (새 기호를 정하기 귀찮으니까) \(x \otimes y\)라고 하겠습니다.
    <ul>
      <li>두 경로의 집합 \(X\)와 \(Y\)에 대해서도 비슷하게 정의합니다. 즉, \(X \otimes Y = \{ z \mid x \in X, y \in Y, z = x \otimes y \}\)로 정의합니다.</li>
    </ul>
  </li>
  <li>경로 \(x\)의 거리를 \(f(x)\)로 적습니다.
    <ul>
      <li>경로의 집합 \(X\)의 거리의 최솟값을 \(f(X)\)로 적습니다.</li>
    </ul>
  </li>
  <li>\(P(i, j, k)\)는 \(i\)번 정점에서 1번부터 \(k\)번까지의 정점만 경유해서 \(j\)번 정점으로 가는 경로의 집합입니다.
    <ul>
      <li>\(P(i, j, 0)\)은 아무 정점도 경유하지 않는 베이스 케이스로, \(i\)번 정점과 \(j\)번 정점을 직접 잇는 간선이 있다면 그 간선만 있는 집합, 없다면 공집합으로 정의합니다.</li>
    </ul>
  </li>
  <li>\(F(i, j, k) = f(P(i, j, k))\)</li>
</ul>

<p>알고리즘 설명에 필요한 성질을 미리 적어 두겠습니다. 증명은 생략합니다.</p>

<ul>
  <li>두 경로 \(x\)와 \(y\)에 대해 \(f(x \otimes y) = f(x) \otimes f(y)\)</li>
  <li>두 집합 \(X\)와 \(Y\)에 대해...
    <ul>
      <li>\(f(X \otimes Y) = f(X) \otimes f(Y)\)</li>
      <li>\(f(X \cup Y) = f(X) \oplus f(Y)\)</li>
    </ul>
  </li>
  <li>\(X \subset X'\)이면...
    <ul>
      <li>\(X \cup Y \subset X' \cup Y\)</li>
      <li>\(X \otimes Y \subset X' \otimes Y\)</li>
      <li>\(Y \otimes X \subset Y \otimes X'\)</li>
    </ul>
  </li>
</ul>

<p>이때 \(P(i, j, k)\)에 속하는 경로는 다음의 두 가지 경우로 나눌 수 있습니다.</p>

<ul>
  <li>\(k\)번 정점을 지나지 않는 경로
    <ul>
      <li>\(P(i, j, k - 1)\)에 해당합니다.</li>
    </ul>
  </li>
  <li>\(k\)번 정점을 지나는 경로
    <ul>
      <li>실수 가중치의 특성상 최단 경로에는 사이클이 없으므로 \(k\)번 정점은 최대 한 번 지나갑니다. 즉, 이 경로는 \(k\)번 정점을 기준으로 앞뒤로 분해할 수 있고 이 두 부분 경로는 \(k\)번 정점을 지나지 않습니다. 즉, 각각 \(P(i, k, k - 1) \otimes P(k, j, k - 1)\)에 해당합니다.
        <ul>
          <li>사실 \(P(i, k, k - 1)\)과 \(P(k, j, k - 1)\)을 합치면 \(k - 1\)번 간선을 2번 이상 지나는 경로도 포함되지만 이런 경로는 어차피 최솟값에 반영되지 않으므로 상관은 없습니다.</li>
        </ul>
      </li>
    </ul>
  </li>
</ul>

<p>즉, \(P(i, j, k)\)는 위의 두 경우의 합집합 \(P(i, j, k - 1) \cup P(i, k, k - 1) \otimes P(k, j, k - 1)\)이고 \(f\)의 성질을 이용해 최솟값의 점화식 \(F(i, j, k) = F(i, j, k - 1) \oplus F(i, k, k - 1) \otimes F(k, j, k - 1)\)으로 바꿀 수 있습니다. 이 점화식을 그대로 계산하는 대신 배열 업데이트로 바꾸면 오늘날 사용하는 플로이드-워셜 알고리즘이 됩니다. 여기서부터는 설명의 편의를 위해 \(F(\cdots)\) 없이 \(P(\cdots)\)만 사용하겠습니다.</p>

<p>여기서 <strong>"최단 경로에는 사이클이 없다"는 가정을 버리고</strong> 알고리즘을 다시 유도해 봅시다.</p>

<h2 id="새로운-알고리즘-만나기">새로운 알고리즘 만나기</h2>

<p>\(P(i, j, k)\)에 속하는 경로는 다음의 두 가지 경우로 나눌 수 있습니다.</p>

<ul>
  <li>\(k\)번 정점을 지나지 않는 경로
    <ul>
      <li>\(P(i, j, k - 1)\)에 해당합니다.</li>
    </ul>
  </li>
  <li>\(k\)번 정점을 지나는 경로
    <ul>
      <li>이 경로는 \(k\)번 정점을 <em>임의의 횟수만큼</em> 지나갑니다. 이전처럼 \(k\)번 정점을 기준으로 분해해서 \(k\)번 정점을 지나지 않는 부분 경로를 만들 수 없으므로 처음 나오는 \(k\)번 정점을 기준으로 분해하든지 마지막으로 나오는 \(k\)번 정점을 기준으로 분해하든지 해야 합니다.</li>
    </ul>
  </li>
</ul>

<p>위의 논문에서는 다음과 같은 방법으로 분해했습니다.</p>

<ul>
  <li>\(j \ne k\)인 경우
    <ul>
      <li>마지막으로 나오는 \(k\)번 정점을 기준으로 분해해 \(P(i, k, k) \otimes P(k, j, k - 1)\)을 얻습니다. 왼쪽 피연산자에 \(k - 1\) 대신 \(k\)가 있음에 유의해 주세요.</li>
    </ul>
  </li>
  <li>\(i \ne k\), \(j = k\)인 경우
    <ul>
      <li>처음으로 나오는 \(k\)번 정점을 기준으로 분해해 \(P(i, k, k - 1) \otimes P(k, k, k)\)를 얻습니다.</li>
    </ul>
  </li>
  <li>\(i = j = k\)인 경우
    <ul>
      <li>이 경우는 순수하게 \(k\)번 정점의 사이클이므로 \(P(k, k, k - 1)^*\)를 얻습니다.</li>
    </ul>
  </li>
</ul>

<p>이때의 의사코드는 다음과 같습니다. 함수 <code class="language-plaintext highlighter-rouge">star</code>가 정의되어 있고 <code class="language-plaintext highlighter-rouge">0</code>, <code class="language-plaintext highlighter-rouge">1</code>, <code class="language-plaintext highlighter-rouge">+</code>, <code class="language-plaintext highlighter-rouge">*</code>이 적당히 오버로딩이 되어 있음을 가정합니다.</p>

<div class="language-plaintext pseudocode highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dist = [[0] * n] * n
for (from, to, len) in edge:
	dist[from][to] += len

for k in [0, n):
	new_dist = [[0] * n] * n
	
	# i = j = k
	for i in [0, n):
		new_dist[i][i] = star(dist[i][i])
	
	# i != k, j = k
	for i in [0, n):
		if i != k:
			new_dist[i][k] = dist[i][k] * new_dist[k][k]
	
	# j != k
	for i in [0, n):
		for j in [0, n):
			if j != k:
				new_dist[i][j] = new_dist[i][k] * dist[k][j]
</code></pre></div></div>

<h2 id="쓰기-편하게-최적화하기">쓰기 편하게 최적화하기</h2>

<p><em>여기부터는 제가 독자 연구한 내용으로, 잘못된 정보가 포함되어 있을 수 있습니다. 건조한 증명이 몇 문단 동안 이어지므로 이 부분은 읽지 않고 넘어가셔도 됩니다.</em></p>

<p>여기까지만 유도해도 올바른 알고리즘은 나오지만, 이대로 놔두면 실 사용이 정말 불편한 알고리즘이 됩니다. 원래 플로이드-워셜 알고리즘만큼 편하게 쓸 수 있게 개선했으면 좋겠네요.</p>

<h3 id="하나의-점화식으로-통일하기">하나의 점화식으로 통일하기</h3>

<p>위의 논문에서 제시한 방법 대신 <strong>모든</strong> \(i\), \(j\), \(k\)의 조합에서 처음으로 나오는 \(k\)번 정점과 마지막으로 나오는 \(k\)번 간선을 기준으로 두 번 분할하면 \(i\)번 정점에서 \(k\)번 정점으로 가는 머리, \(k\)번 정점의 사이클로 이루어진 몸통, \(k\)번 정점에서 \(j\)번 정점으로 가는 꼬리로 나눌 수 있습니다. 단, \(i = k\)이면 머리가, \(j = k\)이면 꼬리가 자명한 경로가 되어 없어집니다.</p>

\[P(i, j, k) = P(i, j, k - 1) \cup P(i, k, k - 1) \otimes P(k, k, k) \otimes P(k, j, k - 1)\]

<p>이때 \(P(k, k, k) = P(k, k, k - 1)^*\)이므로 다음과 같이 바꿀 수 있습니다.</p>

\[P(i, j, k) = P(i, j, k - 1) \cup P(i, k, k - 1) \otimes P(k, k, k - 1)^* \otimes P(k, j, k - 1)\]

<p>이제 점화식이 한 가지 모양으로 고정됐을 뿐만 아니라 \(P(\cdots, \cdots, k - 1)\)에만 의존하는 식이 되었습니다. 점화식에 \(P(\cdots, \cdots, k)\) 항이 포함되었다면 동적 계획법을 사용할 때 순서를 생각하면서 루프를 돌려야 했을 것입니다.</p>

<h3 id="하나의-2차원-배열로-바꾸기">하나의 2차원 배열로 바꾸기</h3>

<p>하지만 아직 2차원 배열을 2개 만들고 루프를 돌 때마다 배열을 저글링하는 불편한 점이 있습니다. 플로이드-워셜 알고리즘처럼 배열 하나에 업데이트만 해도 올바르게 동작할까요?</p>

<p>알고리즘을 이렇게 고칠 수 있으려면 동적 계획법을 돌릴 때 원래보다 경로를 "조금 많이 잡아도" 잘 돌아간다는 것을 증명해야 합니다. 구체적으로는 <em>최적화된</em> 알고리즘의 2차원 배열을 \(P'(i, j)\)라 할 때(값이 계속 바뀌기 때문에 \(k\)를 따로 적지 않는 것에 유의해 주세요), \(k\)번째 루프가 돌아가기 전에 \(P(i, j, k - 1) \subset P'(i, j) \subset P(i, j, n)\)이었으면 \(k\)번째 루프가 돌고 나서는 \(P(i, j, k) \subset P'(i, j) \subset P(i, j, n)\)을 만족하는 것을 보이고 싶습니다. 이렇게 하면 루프가 \(n\)번 돈 뒤에는 \(P(i, j, n) \subset P'(i, j) \subset P(i, j, n)\)이므로 \(P'(i, j) = P(i, j, n)\)이 됩니다.</p>

<p>우선 첫 번째 루프가 돌기 전에는 \(P(i, j, 0) \subset P'(i, j) \subset P(i, j, n)\)을 만족해야 합니다. 이는 \(P'(i, j) \leftarrow P(i, j, 0)\)으로 초기화해서 쉽게 만족할 수 있습니다.</p>

<p>\(k\)번째 루프가 돌 때는 원래의 점화식을 변형해서 \(P'(i, j) \leftarrow P'(i, j) \cup P'(i, k) \otimes P'(k, k)^* \otimes P'(k, j)\) 꼴의 대입을 하는데, 이 시점에서 \(P'(i, j)\)의 값은 루프를 \(k - 1\)번 혹은 \(k\)번 돈 결과이므로 둘 중 하나를 만족합니다.</p>

<ul>
  <li>\(P(i, j, k - 1) \subset P'(i, j) \subset P(i, j, n)\)</li>
  <li>\(P(i, j, k) \subset P'(i, j) \subset P(i, j, n)\)</li>
</ul>

<p>그런데 원래 점화식에 의해 \(P(i, j, k - 1) \subset P(i, j, k)\)이므로 \(P(i, j, k - 1) \subset P'(i, j) \subset P(i, j, n)\)이 성립합니다. 이는 우변의 다른 값에도 똑같이 적용됩니다.</p>

<p>여기에 아까 언급한 여러 가지 포함 관계 성질과 \(P(a, b, n) \otimes P(b, c, n) = P(a, c, n)\)임을 이용하면(\(P(i, j, n)\)은 정의상 \(i\)번 정점에서 \(j\)번 정점으로 가는 모든 경로의 집합이므로) 목표로 했던 \(P(i, j, k) \subset P'(i, j) \cup P'(i, k) \otimes P'(k, k)^* \otimes P'(k, j) \subset P(i, j, n)\)을 얻을 수 있습니다.</p>

<p>글을 처음 쓰기 시작했을 때는 예상하지 못했었는데 <strong>WFGJ 알고리즘도 "그냥" 루프 3개에 배열 1개로 돌릴 수 있네요</strong>!</p>

<h3 id="진정-하나의-점화식으로-통일하기">진정 하나의 점화식으로 통일하기</h3>

<p>사실 <a href="#하나의-점화식으로-통일하기">위의 문단</a>에서는 "\(i = k\)이면 머리가, \(j = k\)이면 꼬리가 자명한 경로가 되어 없어"진다고 해 놓고 바로 밑의 점화식에는 그 사실을 반영하지 않았습니다. 실제로 글을 쓰면서 제가 최적화를 잘 하고 있는지 계속 제출을 해보고 있는데 이 이유로 틀렸습니다를 받았습니다.</p>

<p>문제가 생긴 이유를 한 마디로 정리하자면 \(P(i, i, 0)\)에 "자명한 경로"를 포함하지 않았기 때문입니다. 이 문제는 3중 루프를 돌리기 전에 \(P'(i, i)\)에 그 "자명한 경로"를 직접 추가해서 해결할 수 있습니다. 실제 코드에서는 <code class="language-plaintext highlighter-rouge">dist[i][i] += 1</code>과 같은 모양이 되겠습니다. 이렇게 하고 나면 완성된 점화식은 <em>숨은 조건 없이</em> 이렇게 한 줄이 됩니다.</p>

\[P'(i, j) = P'(i, j) \cup P'(i, k) \otimes P'(k, k)^* \otimes P'(k, j)\]

<p>단, \(i = k\)이거나 \(j = k\)이면 \(P'(i, k)\)나 \(P'(k, j)\)가 "자명한 경로"만 있는 집합이어야 하는데 그렇지 않게 되는 문제가 있습니다. 어차피 이 알고리즘은 <a href="#하나의-2차원-배열로-바꾸기">경로를 "조금 많이 잡아도"</a> 잘 돌아가니 넘어가겠습니다.</p>

<h1 id="결론-플로이드-워셜-알고리즘의-잃어버린-항">결론: 플로이드-워셜 알고리즘의 잃어버린 항</h1>

<p>위의 최적화를 모두 적용하고 난 의사코드는 다음과 같습니다.</p>

<div class="language-plaintext pseudocode highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dist = [[0] * n] * n
for (from, to, len) in edge:
	dist[from][to] += len
for i in [0, n):
	dist[i][i] += 1

for k in [0, n):
	for i in [0, n):
		for j in [0, n):
			dist[i][j] += dist[i][k] * star(dist[k][k]) * dist[k][j]
</code></pre></div></div>

<p>참고로 (대수적 버전의) 플로이드-워셜 알고리즘의 의사코드는 다음과 같습니다.</p>

<div class="language-plaintext pseudocode highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dist = [[0] * n] * n
for (from, to, len) in edge:
	dist[from][to] += len
for i in [0, n):
	dist[i][i] += 1

for k in [0, n):
	for i in [0, n):
		for j in [0, n):
			dist[i][j] += dist[i][k] * dist[k][j]
</code></pre></div></div>

<p>두 알고리즘은 말 그대로 <strong>한 항 차이</strong>입니다. 이 글의 제목에서 언급했던 "잃어버린 항"이 바로 이것을 말하는 것입니다.</p>

<p>이쯤 되면 좀 우려먹는 것 같긴 하지만 <a href="/2022/06/05/how-is-type-derivative-a-thing.html">정규 표현식</a> 가중치도 위에서 언급한 적당한 반환 구조를 이루고, 여기에 WFGJ 알고리즘을 적용하면 갑자기 <strong><a href="https://en.wikipedia.org/wiki/Nondeterministic_finite_automaton#NFA_with_%CE%B5-moves">ε-NFA</a>를 <a href="https://en.wikipedia.org/wiki/Kleene%27s_algorithm">정규 표현식으로 바꾸는 알고리즘</a></strong>을 공짜로 얻을 수 있습니다. 이외에도 이 구조를 이루는 가중치는 얼마든지 생각할 수 있으니 이 한 항이 얼마나 중요한 역할을 하는지 느껴볼 수 있습니다. 다만 이 알고리즘은 모든 쌍의 최단 경로를 구하기 때문에(\(O(n^3)\)) 그럴 필요가 없을 떄는 시간 낭비가 있을 수 있고, 웬만한 최단 경로 문제는 모두 실수 선에서 해결할 수 있어 활용 기회가 좀처럼 없었던 것이 아쉬운데 앞으로는 이런 문제를 더 자주 만나볼 수 있었으면 좋겠습니다.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:fn-kleene-algebra" role="doc-endnote">
      <p>이 글에서 다루는 이 대수적 구조에서 클레이니 스타의 정의를 <a href="https://en.wikipedia.org/wiki/Kleene_algebra#Definition">이 문단의 마지막 네 가지 성질</a>로 바꾼 것을 <a href="https://en.wikipedia.org/wiki/Kleene_algebra">클레이니 대수</a>라고 합니다. <a href="#fnref:fn-kleene-algebra" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:fn-natural-number" role="doc-endnote">
      <p>대한민국의 교육 과정에서는 0이 자연수가 아니지만, 논의의 일관성과 편의를 위해 0을 자연수로 취급하는 경우도 자주 있습니다. 이 글에서도 여기서부터 "자연수"를 "음이 아닌 정수"의 의미로 사용하겠습니다. <a href="#fnref:fn-natural-number" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:fn-tropical-etymology" role="doc-endnote">
      <p>"열대"라는 이름은 이 분야의 연구자인 헝가리계 브라질 컴퓨터과학자 <a href="https://en.wikipedia.org/wiki/Imre_Simon">Imre Simon</a>이 열대 지역에 살았기 때문에 붙여졌다는 것 같습니다. 왜 멀쩡한 사람 이름 대신 기후 이름을 붙였는지는 잘 모르겠습니다. <a href="#fnref:fn-tropical-etymology" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:fn-semilattice" role="doc-endnote">
      <p>구체적으로 반격자에는 상한(join) 반격자와 하한(meet) 반격자가 있는데, 어차피 둘 다 방향만 빼고 같은 개념이니 여기서는 하한 반격자를 "반격자"라고 하겠습니다. <a href="#fnref:fn-semilattice" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="문제해결" /><category term="알고리즘" /><category term="추상대수학" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">급하게 배우는 Haskell (&quot;검증하지 말고 파싱하라&quot; 보충)</title><link href="https://eatchangmyeong.github.io/2022/12/04/haskell-in-a-hurry.html" rel="alternate" type="text/html" title="급하게 배우는 Haskell (&quot;검증하지 말고 파싱하라&quot; 보충)" /><published>2022-12-04T00:00:00+09:00</published><updated>2022-12-04T00:00:00+09:00</updated><id>https://eatchangmyeong.github.io/2022/12/04/haskell-in-a-hurry</id><content type="html" xml:base="https://eatchangmyeong.github.io/2022/12/04/haskell-in-a-hurry.html"><![CDATA[<p><em>이 글은 <a href="/2022/12/04/parse-don-t-validate.html">검증하지 말고 파싱하라</a>의 내용을 보충할 목적으로 작성하는 Haskell 속성 강좌 같은 느낌의 글입니다. 글을 읽는 데 꼭 필요한 분량만 작성했기 때문에 실제로 Haskell을 학습하는 목적으로는 적합하지 않으며, 진지하게 Haskell을 배우신다면 <a href="https://www.haskell.org/documentation/">Haskell 공식 홈페이지</a>에서 안내하는 학습 자료 중 하나를 골라서 읽어보는 것을 권장드립니다.</em></p>

<p><em>Haskell은 <a href="https://www.haskell.org/downloads/">Haskell 공식 홈페이지</a>의 설치 가이드를 따라 설치할 수 있으며, 컴파일러는 <code class="language-plaintext highlighter-rouge">ghc</code>, 대화형 인터프리터는 <code class="language-plaintext highlighter-rouge">ghci</code>로 호출할 수 있습니다. PC에 설치하기 곤란한 경우에는 <a href="https://replit.com/languages/haskell">Replit</a>에서 온라인으로 사용해볼 수 있습니다.</em></p>

<hr />

<h1 id="hello-world"><code class="language-plaintext highlighter-rouge">Hello, world!</code></h1>

<p>다른 언어를 배울 때처럼 헬로월드부터 작성해 봅시다. Haskell로 짠 헬로월드 프로그램은 다음과 같습니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">main</span> <span class="o">::</span> <span class="kt">IO</span> <span class="nb">()</span>
<span class="n">main</span> <span class="o">=</span> <span class="n">putStrLn</span> <span class="s">"Hello, world!"</span>
</code></pre></div></div>

<p>Haskell 프로그램도 C처럼 <code class="language-plaintext highlighter-rouge">main</code>에서 시작합니다. 여기서 <code class="language-plaintext highlighter-rouge">main :: IO ()</code>는 타입 시그니처로 <code class="language-plaintext highlighter-rouge">main</code>의 타입이 <code class="language-plaintext highlighter-rouge">IO ()</code>라는 뜻이고(C의 함수 프로토타입과 비슷합니다. <code class="language-plaintext highlighter-rouge">IO ()</code>가 무슨 타입인지는 나중에 다시 얘기하겠습니다), 다음 줄의 <code class="language-plaintext highlighter-rouge">main = putStrLn "Hello, world!"</code>가 실제 <code class="language-plaintext highlighter-rouge">main</code>의 정의입니다.</p>

<h1 id="모든-것은-함수다">모든 것은 함수다</h1>

<p>Haskell은 순수 함수형 언어이기 때문에 프로그램의 거의 전체가 함수로 이루어집니다. 예를 들어서 아까 <code class="language-plaintext highlighter-rouge">main</code>에 썼던 <code class="language-plaintext highlighter-rouge">putStrLn</code>도 함수입니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">putStrLn</span> <span class="o">::</span> <span class="kt">String</span> <span class="o">-&gt;</span> <span class="kt">IO</span> <span class="nb">()</span>
</code></pre></div></div>

<p>여기서 <code class="language-plaintext highlighter-rouge">String</code>은 문자열 타입이고, <code class="language-plaintext highlighter-rouge">String -&gt; IO ()</code>는 <code class="language-plaintext highlighter-rouge">String</code>을 받아서 <code class="language-plaintext highlighter-rouge">IO ()</code>를 내놓는 함수의 타입입니다.</p>

<p>또 C와 달리 여기서는 함수를 호출할 때 괄호를 쓰지 않고 인자를 바로 연달아서 씁니다. 아까 <code class="language-plaintext highlighter-rouge">main</code>의 정의를 다시 살펴보면 함수 <code class="language-plaintext highlighter-rouge">putStrLn</code>에 인자로 <code class="language-plaintext highlighter-rouge">String</code>인 <code class="language-plaintext highlighter-rouge">"Hello, world!"</code>를 주는 형태이고, <code class="language-plaintext highlighter-rouge">putStrLn</code>은 <code class="language-plaintext highlighter-rouge">String -&gt; IO ()</code> 타입이니 <code class="language-plaintext highlighter-rouge">main</code>이 <code class="language-plaintext highlighter-rouge">IO ()</code>인 것이 자연스럽네요.</p>

<h2 id="함수-만들기">함수 만들기</h2>

<p>여기서는 설명의 편의를 위해 <code class="language-plaintext highlighter-rouge">Bool</code> 타입으로 예시를 들어 보겠습니다. 이 타입의 값은 <code class="language-plaintext highlighter-rouge">True</code>와 <code class="language-plaintext highlighter-rouge">False</code>뿐입니다.</p>

<p>위에서 언급했던 <code class="language-plaintext highlighter-rouge">main</code>처럼 타입 시그니처(생략해도 됨)와 한 줄 이상의 정의를 늘어놓으면 함수를 만들 수 있습니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">not</span> <span class="o">::</span> <span class="kt">Bool</span> <span class="o">-&gt;</span> <span class="kt">Bool</span>
<span class="n">not</span> <span class="kt">False</span> <span class="o">=</span> <span class="kt">True</span> <span class="c1">-- 좌변에 인자를 추가로 입력하면 함수가 됩니다.</span>
<span class="n">not</span> <span class="kt">True</span>  <span class="o">=</span> <span class="kt">False</span>
</code></pre></div></div>

<p>수학에서 함수를 부분부분 정의하듯이 Haskell에서도 인자의 모양을 보고 분기할 수 있습니다. <code class="language-plaintext highlighter-rouge">False</code>나 <code class="language-plaintext highlighter-rouge">True</code>보다 더 복잡한 모양으로도 분기할 수 있는데, 이를 패턴 매칭이라고 합니다.</p>

<h3 id="인자가-많은-함수-만들기">인자가 많은 함수 만들기</h3>

<p>인자가 여러 개인 함수도 만들 수 있습니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">and</span> <span class="o">::</span> <span class="kt">Bool</span> <span class="o">-&gt;</span> <span class="kt">Bool</span> <span class="o">-&gt;</span> <span class="kt">Bool</span> <span class="c1">-- ???</span>
<span class="n">and</span> <span class="kt">False</span> <span class="kr">_</span> <span class="o">=</span> <span class="kt">False</span>
<span class="n">and</span> <span class="kt">True</span> <span class="n">x</span>  <span class="o">=</span> <span class="n">x</span>
</code></pre></div></div>

<p>여기서 <code class="language-plaintext highlighter-rouge">_</code>와 <code class="language-plaintext highlighter-rouge">x</code>도 패턴인데, 후자는(소문자로 시작해야 합니다. 대문자면 안 됩니다) 그 자리에 오는 아무 값을 변수 <code class="language-plaintext highlighter-rouge">x</code>에 대입한다는 의미이고, 전자는 후자와 같지만 그 값에 관심이 없으니 변수에 대입하지 않고 버린다는 의미입니다.</p>

<p>말이 나온 김에 여기서 얘기해야 될 것 같은데 Haskell에서는 <em>문법 수준에서</em> 대소문자를 구분합니다. 즉, 대문자로 시작하는 이름과 소문자로 시작하는 이름은 문법적 의미가 다릅니다.</p>

<p>일단 정의하면 이렇게 쓸 수 있습니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">and</span> <span class="kt">True</span> <span class="kt">True</span> <span class="c1">-- True</span>
<span class="n">and</span> <span class="kt">False</span> <span class="kt">False</span> <span class="c1">-- False</span>
<span class="n">and</span> <span class="kt">True</span> <span class="kt">False</span> <span class="c1">-- False</span>
</code></pre></div></div>

<p>여기서 <code class="language-plaintext highlighter-rouge">Bool -&gt; Bool -&gt; Bool</code>은 <code class="language-plaintext highlighter-rouge">Bool -&gt; (Bool -&gt; Bool)</code>로 묶으며, <code class="language-plaintext highlighter-rouge">Bool</code>을 받아서 <em>함수를</em> 돌려주는 함수라는 뜻입니다. 즉, 이런 것도 할 수 있습니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">foo</span> <span class="o">::</span> <span class="kt">Bool</span> <span class="o">-&gt;</span> <span class="kt">Bool</span>
<span class="n">foo</span> <span class="o">=</span> <span class="n">and</span> <span class="kt">True</span>
<span class="c1">-- foo True = True, foo False = False</span>
</code></pre></div></div>

<p>더 근본적으로는, 모든 Haskell 함수는 <em>인자가 하나인</em> 함수입니다. 다인자 함수를 이렇게 쪼개는 것을 전문 용어로 <a href="https://ko.wikipedia.org/wiki/커링">커링</a>이라고 합니다.</p>

<h3 id="함수를-받는-함수-만들기">함수를 받는 함수 만들기</h3>

<p>함수를 <em>돌려주는</em> 함수가 있으면 함수를 <em>받는</em> 함수도 만들 수 있을까요?</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">applyTrue</span> <span class="o">::</span> <span class="p">(</span><span class="kt">Bool</span> <span class="o">-&gt;</span> <span class="kt">Bool</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="kt">Bool</span>
<span class="n">applyTrue</span> <span class="n">f</span> <span class="o">=</span> <span class="n">f</span> <span class="kt">True</span>
</code></pre></div></div>

<p>이때는 타입 자리의 <code class="language-plaintext highlighter-rouge">-&gt;</code> 연산자가 우결합성이기 때문에 괄호를 꼭 붙여줘야 합니다.</p>

<h2 id="연산자도-함수다">연산자도 함수다</h2>

<p>Haskell은 실 사용을 염두에 두고 개발한 프로그래밍 언어이니 당연히 정수 연산도 지원합니다. <code class="language-plaintext highlighter-rouge">ghci</code>에 다음과 같은 수식을 입력해 보세요. <code class="language-plaintext highlighter-rouge">--</code> 다음부터는 주석입니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="mi">1</span> <span class="o">+</span> <span class="mi">2</span> <span class="c1">-- 3</span>
<span class="mi">3</span> <span class="o">-</span> <span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">)</span> <span class="c1">-- 5. 문법적 한계로 인해 음수는 괄호를 씌워야 합니다.</span>
<span class="mi">6</span> <span class="o">*</span> <span class="mi">3</span> <span class="o">+</span> <span class="mi">2</span> <span class="c1">-- 20</span>
<span class="mi">6</span> <span class="o">+</span> <span class="mi">3</span> <span class="o">*</span> <span class="mi">2</span> <span class="c1">-- 12. 연산자 우선순위도 지원됩니다.</span>
</code></pre></div></div>

<p>그런데 사실 연산자도 특별한 것은 없고 그냥 이름이 특수문자인 함수일 뿐입니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="o">+</span><span class="p">)</span> <span class="mi">1</span> <span class="mi">2</span> <span class="c1">-- 3</span>
<span class="p">(</span><span class="o">-</span><span class="p">)</span> <span class="mi">3</span> <span class="p">(</span><span class="o">-</span><span class="mi">2</span><span class="p">)</span> <span class="c1">-- 5</span>
</code></pre></div></div>

<p>함수형 언어이니만큼 함수 합성을 하는 연산자도 정의되어 있습니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="o">.</span><span class="p">)</span> <span class="o">::</span> <span class="p">(</span><span class="n">b</span> <span class="o">-&gt;</span> <span class="n">c</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="n">a</span> <span class="o">-&gt;</span> <span class="n">b</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="n">a</span> <span class="o">-&gt;</span> <span class="n">c</span><span class="p">)</span>
<span class="p">(</span><span class="o">.</span><span class="p">)</span> <span class="n">g</span> <span class="n">f</span> <span class="n">x</span> <span class="o">=</span> <span class="n">g</span> <span class="p">(</span><span class="n">f</span> <span class="n">x</span><span class="p">)</span>
<span class="c1">-- (not . foo) True = not (foo True) = not True = False</span>
</code></pre></div></div>

<p>지금까지 봤던 <code class="language-plaintext highlighter-rouge">IO</code>나 <code class="language-plaintext highlighter-rouge">String</code>이나 <code class="language-plaintext highlighter-rouge">Bool</code>과는 다르게 타입 이름이 소문자로 시작하는 것을 볼 수 있는데, 이건 <code class="language-plaintext highlighter-rouge">a</code>, <code class="language-plaintext highlighter-rouge">b</code>, <code class="language-plaintext highlighter-rouge">c</code> 자리에 아무 타입이나 올 수 있다는 뜻입니다. 즉, 필요하다면 <code class="language-plaintext highlighter-rouge">a</code> = <code class="language-plaintext highlighter-rouge">String</code>, <code class="language-plaintext highlighter-rouge">b</code> = <code class="language-plaintext highlighter-rouge">Bool</code>, <code class="language-plaintext highlighter-rouge">c</code> = <code class="language-plaintext highlighter-rouge">Bool</code> 등 임의로 타입을 대입해서 그 타입인 것처럼 쓸 수 있습니다.</p>

<p>또 이런 연산자도 정의되어 있습니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">(</span><span class="o">$</span><span class="p">)</span> <span class="o">::</span> <span class="p">(</span><span class="n">a</span> <span class="o">-&gt;</span> <span class="n">b</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="n">a</span> <span class="o">-&gt;</span> <span class="n">b</span><span class="p">)</span>
<span class="n">f</span> <span class="o">$</span> <span class="n">x</span> <span class="o">=</span> <span class="n">f</span> <span class="n">x</span>
</code></pre></div></div>

<p>정의만 보면 굳이 쓸데가 없어 보이는 연산이지만, 이 연산자는 <code class="language-plaintext highlighter-rouge">a b c</code> = <code class="language-plaintext highlighter-rouge">(a b) c</code>와 달리 <code class="language-plaintext highlighter-rouge">a b $ c d</code> = <code class="language-plaintext highlighter-rouge">(a b) (c d)</code>, <code class="language-plaintext highlighter-rouge">a $ b $ c</code> = <code class="language-plaintext highlighter-rouge">a $ (b $ c)</code>이도록 정의되어 있기 때문에 괄호를 쓰기 귀찮을 때 애용할 수 있습니다.</p>

<h1 id="새로운-타입-만들기">새로운 타입 만들기</h1>

<p>Haskell에서는 새로운 타입을 만들 수 있는 방법을 여러 가지 제공합니다. 구체적으로는 대수적 자료형이라고 하는 것을 지원하는데, 이게 뭔지는 <a href="/2022/06/05/how-is-type-derivative-a-thing.html#대수적-자료형">이전 글</a>에서 자세히 다룬 적이 있습니다.</p>

<h2 id="data문"><code class="language-plaintext highlighter-rouge">data</code>문</h2>

<p><code class="language-plaintext highlighter-rouge">data</code>문으로 새로운 타입을 만들고, 그 타입이 가질 수 있는 모양도 정의할 수 있습니다. 여기서 정의한 모양들은 모두 패턴으로 사용할 수 있습니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 아무런 모양도 가질 수 없습니다(바닥 타입).</span>
<span class="kr">data</span> <span class="kt">Void</span>

<span class="c1">-- `()` 타입의 모든 값은 `()` 모양입니다(단위 타입).</span>
<span class="kr">data</span> <span class="nb">()</span> <span class="o">=</span> <span class="nb">()</span>

<span class="c1">-- 위에서 봤던 `Bool` 타입이 이렇게 정의되어 있습니다.</span>
<span class="c1">-- `False` 모양이나 `True` 모양을 가질 수 있다는 의미입니다.</span>
<span class="kr">data</span> <span class="kt">Bool</span> <span class="o">=</span> <span class="kt">False</span> <span class="o">|</span> <span class="kt">True</span>

<span class="c1">-- `MyCoord Integer Integer` 모양을 가집니다.</span>
<span class="c1">-- 즉, `Integer` 타입의 값 2개를 가지고 `MyCoord 3 5`와 같이 `MyCoord`를 만들 수 있습니다.</span>
<span class="c1">-- 이쯤에서 눈치채셨겠지만 모든 모양은 대문자 단어로 시작하고</span>
<span class="c1">-- 이 "태그 단어"를 이용해서 무슨 모양인지 구별합니다.</span>
<span class="c1">-- 이때 `Integer`는 범위가 무한대인 정수 타입입니다.</span>
<span class="kr">data</span> <span class="kt">MyCoord</span> <span class="o">=</span> <span class="kt">MyCoord</span> <span class="kt">Integer</span> <span class="kt">Integer</span>
</code></pre></div></div>

<p>각 모양마다 태그 단어가 필요한 것은 태그 단어가 그 타입의 값을 만드는 <em>생성자 함수</em>의 역할을 하기 때문입니다. <a href="#연산자도-함수다">연산자도 함수</a>인 점을 고려해 태그 단어로 연산자도 사용할 수 있습니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- `Nil` 모양이거나 `Integer` 1개, `IntegerList` 1개를 가지고 `Integer : IntegerList` 모양으로 만들 수 있습니다.</span>
<span class="c1">-- 즉, 이 타입은 `Integer`의 연결 리스트입니다.</span>
<span class="kr">data</span> <span class="kt">IntegerList</span> <span class="o">=</span> <span class="kt">Nil</span> <span class="o">|</span> <span class="kt">Integer</span> <span class="o">:</span> <span class="kt">IntegerList</span>
<span class="c1">-- 참고로 Haskell에서는 연결 리스트 타입 `[a]`를 기본 지원합니다.</span>

<span class="c1">-- 점점 감이 안 올 수도 있으니 `IntegerList`를 쓰는 함수로 예시를 들어 보겠습니다.</span>
<span class="n">foo</span> <span class="o">::</span> <span class="kt">IntegerList</span> <span class="o">-&gt;</span> <span class="kt">Integer</span>
<span class="n">foo</span> <span class="kt">Nil</span>   <span class="o">=</span> <span class="mi">0</span> <span class="c1">-- `Nil` 모양을 매치합니다.</span>
<span class="n">foo</span> <span class="p">(</span><span class="n">x</span><span class="o">:</span><span class="kr">_</span><span class="p">)</span> <span class="o">=</span> <span class="n">x</span> <span class="c1">-- `Integer : IntegerList` 모양을 매치합니다. 우선순위로 인하여 괄호를 달아야 합니다.</span>
</code></pre></div></div>

<h3 id="타입의-함수">타입의 함수</h3>

<p><code class="language-plaintext highlighter-rouge">data</code>문으로 타입의 <em>함수</em>도 만들 수 있고, 문법은 일반 함수를 정의하는 것과 비슷합니다. Haskell의 표준 라이브러리에서 지원하는 몇 가지 유용한 타입 레벨 함수를 소개해 보겠습니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 값이 없을 수도 있는 것을 나타냅니다.</span>
<span class="kr">data</span> <span class="kt">Maybe</span> <span class="n">a</span> <span class="o">=</span> <span class="kt">Nothing</span> <span class="o">|</span> <span class="kt">Just</span> <span class="n">a</span>

<span class="c1">-- `a`를 가지고 있거나 `b`를 가지고 있는 것을 나타냅니다.</span>
<span class="kr">data</span> <span class="kt">Either</span> <span class="n">a</span> <span class="n">b</span> <span class="o">=</span> <span class="kt">Left</span> <span class="n">a</span> <span class="o">|</span> <span class="kt">Right</span> <span class="n">b</span>

<span class="c1">-- 참고로 아까 언급한 `[a]`도 타입 레벨 함수입니다(`[] a`).</span>
<span class="c1">-- 표준 라이브러리 문서에는 문서화되어 있지 않지만 소스 코드에 다음과 같이 정의되어 있습니다.</span>
<span class="kr">data</span> <span class="kt">[]</span> <span class="n">a</span> <span class="o">=</span> <span class="kt">[]</span> <span class="o">|</span> <span class="n">a</span> <span class="o">:</span> <span class="p">[</span><span class="n">a</span><span class="p">]</span>

<span class="c1">-- 두 개 이상의 값을 하나로 묶는 튜플 타입도 있고, GHC 기준으로 최대 62개까지 가능합니다.</span>
<span class="c1">-- `(a,b)`나 `(a,b,c)` 등은 예외적으로 Haskell의 내장 문법을 이용해 정의하고 있습니다.</span>
<span class="kr">data</span> <span class="p">(</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">)</span>
<span class="kr">data</span> <span class="p">(</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">,</span><span class="n">c</span><span class="p">)</span> <span class="o">=</span> <span class="p">(</span><span class="n">a</span><span class="p">,</span><span class="n">b</span><span class="p">,</span><span class="n">c</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="type문"><code class="language-plaintext highlighter-rouge">type</code>문</h2>

<p><code class="language-plaintext highlighter-rouge">type</code>문은 C의 <code class="language-plaintext highlighter-rouge">typedef</code>와 비슷하게 같은 타입을 다른 이름으로 쓸 수 있게 해줍니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- `MaybeInteger`와 `Maybe Integer`는 서로 바꿔 쓸 수 있습니다.</span>
<span class="kr">type</span> <span class="kt">MaybeInteger</span> <span class="o">=</span> <span class="kt">Maybe</span> <span class="kt">Integer</span>

<span class="c1">-- 사실 맨 처음에 봤던 `String`도 타입 동의어입니다. `Char`는 문자 타입입니다.</span>
<span class="kr">type</span> <span class="kt">String</span> <span class="o">=</span> <span class="p">[</span><span class="kt">Char</span><span class="p">]</span>

<span class="c1">-- 표준 라이브러리를 보면 매개변수가 파일의 경로라는 것을 명확히 나타내기 위해</span>
<span class="c1">-- 그냥 `String` 대신 이 타입을 사용하는 경우가 많습니다.</span>
<span class="kr">type</span> <span class="kt">FilePath</span> <span class="o">=</span> <span class="kt">String</span>

<span class="n">foo</span> <span class="o">::</span> <span class="kt">Maybe</span> <span class="kt">Integer</span> <span class="o">-&gt;</span> <span class="kt">Maybe</span> <span class="kt">Integer</span>
<span class="n">foo</span> <span class="n">x</span> <span class="o">=</span> <span class="n">x</span>
<span class="n">bar</span> <span class="o">::</span> <span class="kt">MaybeInteger</span>
<span class="n">bar</span> <span class="o">=</span> <span class="kt">Just</span> <span class="mi">1</span>
<span class="n">foo</span> <span class="n">bar</span> <span class="c1">-- OK</span>
</code></pre></div></div>

<h2 id="newtype문"><code class="language-plaintext highlighter-rouge">newtype</code>문</h2>

<p><code class="language-plaintext highlighter-rouge">newtype</code>문은 <code class="language-plaintext highlighter-rouge">data</code>문과 비슷한데, 정확히 한 가지 모양에 정확히 한 가지 필드밖에 쓸 수 없습니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">newtype</span> <span class="kt">Intish</span> <span class="o">=</span> <span class="kt">Intish</span> <span class="kt">Integer</span>
</code></pre></div></div>

<p>이렇게 보면 의미상 <code class="language-plaintext highlighter-rouge">type</code>문과 다를 바가 없는데, <code class="language-plaintext highlighter-rouge">type</code>문과의 중요한 차이점은 이렇게 새로 정의한 타입과 원래 타입을 바꿔 쓸 수 <em>없다는</em> 것입니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fooInteger</span> <span class="o">::</span> <span class="kt">Integer</span> <span class="o">-&gt;</span> <span class="kt">Integer</span>
<span class="n">fooInteger</span> <span class="n">x</span> <span class="o">=</span> <span class="n">x</span>

<span class="n">fooIntish</span> <span class="o">::</span> <span class="kt">Intish</span> <span class="o">-&gt;</span> <span class="kt">Intish</span>
<span class="n">fooIntish</span> <span class="p">(</span><span class="kt">Intish</span> <span class="n">x</span><span class="p">)</span> <span class="o">=</span> <span class="kt">Intish</span> <span class="n">x</span>

<span class="n">fooInteger</span> <span class="mi">1</span> <span class="c1">-- OK</span>
<span class="n">fooIntish</span> <span class="p">(</span><span class="kt">Intish</span> <span class="mi">1</span><span class="p">)</span> <span class="c1">-- OK</span>
<span class="n">fooInteger</span> <span class="p">(</span><span class="kt">Intish</span> <span class="mi">1</span><span class="p">)</span> <span class="c1">-- 타입 오류</span>
<span class="n">fooIntish</span> <span class="mi">1</span> <span class="c1">-- 타입 오류</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">newtype</code>은 내부 표현은 같지만 의미상 바꿔 쓰면 안 되는 것들을 타입 시스템상에서 구분할 때 유용하게 쓸 수 있습니다.</p>

<h1 id="타입-클래스로-성질-표현하기">타입 클래스로 성질 표현하기</h1>

<p>타입 클래스라는 개념이 있는데, 객체지향 언어의 인터페이스와 비슷하다고 설명하는 게 가장 좋을 것 같고 성질을 표현하는 데 사용합니다<sup id="fnref:fn-typeclass-as-trait" role="doc-noteref"><a href="#fn:fn-typeclass-as-trait" class="footnote" rel="footnote">1</a></sup>. 예를 들어 다음은 동일성 비교가 가능한 성질을 나타내는 타입 클래스입니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">class</span> <span class="kt">Eq</span> <span class="n">a</span> <span class="kr">where</span>
    <span class="c1">-- 타입 클래스의 원소들은 크게 구현해야 하는 것과 제공해주는 것으로 나뉩니다.</span>
    <span class="c1">-- 구현해야 할 것을 아래의 인스턴스를 통해 모두 구현하면 제공해주는 함수를 추가로 쓸 수 있습니다.</span>
    <span class="c1">-- `Eq`를 구현하는 타입이 있으면 그 타입에 대해 `==`나 `/=` 연산자를 쓸 수 있습니다.</span>
    <span class="c1">-- `/=`는 다른 언어에서의 `!=` 연산자와 같습니다.</span>
    <span class="p">(</span><span class="o">==</span><span class="p">),</span> <span class="p">(</span><span class="o">/=</span><span class="p">)</span> <span class="o">::</span> <span class="n">a</span> <span class="o">-&gt;</span> <span class="n">a</span> <span class="o">-&gt;</span> <span class="kt">Bool</span>
    <span class="c1">-- 원칙적으로 `==`와 `/=`는 구현해야 하는 것에 속하지만,</span>
    <span class="c1">-- 하나만 구현해두면 아래의 기본 구현체를 돌려 쓸 수 있습니다.</span>
	<span class="c1">-- 둘 다 생략하면 무한루프에 빠지므로 적어도 하나는 구현해야 합니다.</span>
    <span class="n">x</span> <span class="o">/=</span> <span class="n">y</span> <span class="o">=</span> <span class="n">not</span> <span class="p">(</span><span class="n">x</span> <span class="o">==</span> <span class="n">y</span><span class="p">)</span>
	<span class="n">x</span> <span class="o">==</span> <span class="n">y</span> <span class="o">=</span> <span class="n">not</span> <span class="p">(</span><span class="n">x</span> <span class="o">/=</span> <span class="n">y</span><span class="p">)</span>

<span class="c1">-- 이외에도 코드화할 수 없고 문서로만 명시되어 있는 불변조건이 있을 수 있기 때문에 유의해야 합니다.</span>
<span class="c1">-- `Eq`에 대한 불변조건은 다음과 같습니다.</span>
<span class="c1">-- * `x == x`가 `True`</span>
<span class="c1">-- * `x == y`이면 `y == x`이고 역도 성립함</span>
<span class="c1">-- * `x == y`이고 `y == z`이면 `x == z`</span>
<span class="c1">-- * `Eq`를 구현하는 타입을 반환하는 함수 `f`에 대해 `x == y`이면 `f x == f y`</span>
<span class="c1">-- * `x /= y`와 `not (x == y)`는 동치</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Bool</code>이 동일성 비교가 가능함을 표현할 때는 <code class="language-plaintext highlighter-rouge">instance</code>를 만들어서 <code class="language-plaintext highlighter-rouge">==</code>를 구현하면 됩니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">instance</span> <span class="kt">Eq</span> <span class="kt">Bool</span> <span class="kr">where</span>
    <span class="kt">True</span> <span class="o">==</span> <span class="kt">True</span>   <span class="o">=</span> <span class="kt">True</span>
	<span class="kt">False</span> <span class="o">==</span> <span class="kt">False</span> <span class="o">=</span> <span class="kt">True</span>
	<span class="kr">_</span> <span class="o">==</span> <span class="kr">_</span>         <span class="o">=</span> <span class="kt">False</span> <span class="c1">-- 함수 선택은 위에서 아래로 이루어지므로 위에 나열하지 않은 나머지 경우가 모두 여기에 속합니다.</span>
    <span class="c1">-- `/=`를 구현하지 않았으므로 위의 `class Eq a` 정의에 있는 기본 구현을 그대로 사용합니다.</span>
</code></pre></div></div>

<p>동일성 비교가 가능한 타입만 매개변수로 받는 함수는 타입 자리에 특별한 문법으로 나타낼 수 있습니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">tripleEqual</span> <span class="o">::</span> <span class="kt">Eq</span> <span class="n">a</span> <span class="o">=&gt;</span> <span class="n">a</span> <span class="o">-&gt;</span> <span class="n">a</span> <span class="o">-&gt;</span> <span class="n">a</span> <span class="o">-&gt;</span> <span class="kt">Bool</span> <span class="c1">-- `Eq a =&gt;`는 `a`가 `Eq`를 만족해야 한다는 의미입니다.</span>
<span class="n">tripleEqual</span> <span class="n">a</span> <span class="n">b</span> <span class="n">c</span> <span class="o">=</span> <span class="n">a</span> <span class="o">==</span> <span class="n">b</span> <span class="o">&amp;&amp;</span> <span class="n">b</span> <span class="o">==</span> <span class="n">c</span>
</code></pre></div></div>

<h1 id="이외에-글에서-언급한-문법들">이외에 글에서 언급한 문법들</h1>

<h2 id="caseof식"><code class="language-plaintext highlighter-rouge">case</code>..<code class="language-plaintext highlighter-rouge">of</code>식</h2>

<p><code class="language-plaintext highlighter-rouge">case</code>..<code class="language-plaintext highlighter-rouge">of</code>식은 함수 정의의 우변에서 패턴매칭을 하는 문법입니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">foo</span> <span class="o">::</span> <span class="kt">Maybe</span> <span class="kt">Integer</span> <span class="o">-&gt;</span> <span class="kt">Integer</span>
<span class="n">foo</span> <span class="n">x</span> <span class="o">=</span> <span class="kr">case</span> <span class="n">x</span> <span class="kr">of</span>
    <span class="kt">Just</span> <span class="n">x'</span> <span class="o">-&gt;</span> <span class="n">x'</span>
    <span class="kt">Nothing</span> <span class="o">-&gt;</span> <span class="mi">0</span>
</code></pre></div></div>

<h2 id="do식"><code class="language-plaintext highlighter-rouge">do</code>식</h2>

<p><code class="language-plaintext highlighter-rouge">do</code>식을 설명하기 전에 회수해야 할 떡밥이 있습니다. 헬로 월드 프로그램을 언급하면서 뭐라고 했었죠?</p>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">IO ()</code>가 무슨 타입인지는 나중에 다시 얘기하겠습니다</p>
</blockquote>

<h3 id="io의-의미"><code class="language-plaintext highlighter-rouge">IO</code>의 의미</h3>

<p><strong><code class="language-plaintext highlighter-rouge">IO a</code>는 입출력을 하고 타입 <code class="language-plaintext highlighter-rouge">a</code>의 값을 만드는 연산을 나타냅니다.</strong> 순수 함수형 언어인 Haskell에서는 수학적인 의미의 부작용 없는 함수만 취급하기 때문에 입출력이라는 부작용을 타입 함수 <code class="language-plaintext highlighter-rouge">IO</code>로 격리합니다.</p>

<p>예를 들어 위에서 본 <code class="language-plaintext highlighter-rouge">putStrLn :: String -&gt; IO ()</code>는 <code class="language-plaintext highlighter-rouge">String</code>을 받아 그 <code class="language-plaintext highlighter-rouge">String</code>을 출력하는 연산으로 만듭니다. 연산을 하고 나서 딱히 만들 만한 값이 없기 때문에 정보값이 없는<sup id="fnref:fn-unit-type" role="doc-noteref"><a href="#fn:fn-unit-type" class="footnote" rel="footnote">2</a></sup> <code class="language-plaintext highlighter-rouge">()</code>를 반환합니다.</p>

<p>잘 생각해 보면 <code class="language-plaintext highlighter-rouge">main</code>의 타입도 <code class="language-plaintext highlighter-rouge">IO ()</code>였는데, 즉 <code class="language-plaintext highlighter-rouge">main</code> 자체가 <em>입출력을 하고 종료하는 연산</em>이라는 의미입니다.</p>

<h3 id="do식으로-연산-합성하기"><code class="language-plaintext highlighter-rouge">do</code>식으로 연산 합성하기</h3>

<p><code class="language-plaintext highlighter-rouge">do</code>식 안에 여러 <code class="language-plaintext highlighter-rouge">IO</code> 연산을 절차적 프로그래밍 언어에서 하던 것처럼 입력하면 그 순서대로 여러 연산을 합쳐서 하나로 만들어줍니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">main</span> <span class="o">::</span> <span class="kt">IO</span> <span class="nb">()</span>
<span class="n">main</span> <span class="o">=</span> <span class="kr">do</span>
    <span class="n">putStrLn</span> <span class="s">"Line 1"</span>
    <span class="n">putStrLn</span> <span class="s">"Line 2"</span>
    <span class="n">putStrLn</span> <span class="s">"Line 3"</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">IO a</code>가 만드는 <code class="language-plaintext highlighter-rouge">a</code> 값을 쓰고 싶다면 <code class="language-plaintext highlighter-rouge">&lt;-</code>문을 쓸 수 있습니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 표준 입력에서 한 줄을 입력받는 연산입니다.</span>
<span class="n">getLine</span> <span class="o">::</span> <span class="kt">IO</span> <span class="kt">String</span>

<span class="n">main</span> <span class="o">::</span> <span class="kt">IO</span> <span class="nb">()</span>
<span class="n">main</span> <span class="o">=</span> <span class="kr">do</span>
    <span class="n">input</span> <span class="o">&lt;-</span> <span class="n">getLine</span>
    <span class="n">putStrLn</span> <span class="s">"Input string:"</span>
    <span class="n">putStrLn</span> <span class="n">input</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">IO a</code>가 아닌 일반적인 <code class="language-plaintext highlighter-rouge">a</code>를 변수로 저장해두고 싶다면 <code class="language-plaintext highlighter-rouge">let</code>문을 쓸 수 있습니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">main</span> <span class="o">::</span> <span class="kt">IO</span> <span class="nb">()</span>
<span class="n">main</span> <span class="o">=</span> <span class="kr">do</span>
    <span class="kr">let</span> <span class="n">name</span> <span class="o">=</span> <span class="s">"EatChangmyeong"</span>
    <span class="n">putStrLn</span> <span class="n">name</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">do</code>식도 표현식이기 때문에 <code class="language-plaintext highlighter-rouge">IO a</code> 타입을 가지고, 마지막 줄에 있는 연산의 타입을 그대로 가져옵니다. <code class="language-plaintext highlighter-rouge">a</code>를 반환하고 싶은데 <code class="language-plaintext highlighter-rouge">IO</code>가 없어서 걸리적거린다면 <code class="language-plaintext highlighter-rouge">pure</code>나 <code class="language-plaintext highlighter-rouge">return</code> 함수로 <code class="language-plaintext highlighter-rouge">IO a</code>로 만들 수 있습니다. <code class="language-plaintext highlighter-rouge">return</code><em>문</em>이 아니라 <em>함수</em>임에 유의해 주세요.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">fiveWithoutIO</span> <span class="o">::</span> <span class="kt">IO</span> <span class="kt">Integer</span>
<span class="n">fiveWithoutIO</span> <span class="o">=</span> <span class="kr">do</span>
    <span class="kr">let</span> <span class="n">five</span> <span class="o">=</span> <span class="mi">5</span>
    <span class="n">pure</span> <span class="n">five</span>
</code></pre></div></div>

<p>사실 <code class="language-plaintext highlighter-rouge">do</code>식은 <code class="language-plaintext highlighter-rouge">IO</code>뿐만 아니라 타입 클래스 <code class="language-plaintext highlighter-rouge">Monad</code>를 만족하는 타입이라면 뭐든지 쓸 수 있습니다. <code class="language-plaintext highlighter-rouge">Monad</code>는 설명하기 정말 어려운 개념이기 때문에 이 글에서 구체적으로 설명하지는 않겠지만, <code class="language-plaintext highlighter-rouge">IO</code>처럼 부작용을 격리하는 방법 정도로만 이해하고 있어도 되고 혹시 관심이 있다면 그나마 읽기 좋은 한국어 자료로 <a href="https://overcurried.com/3분%20모나드/">3분 모나드</a>를 추천드립니다. 원 글에서 <code class="language-plaintext highlighter-rouge">m ()</code> 같은 타입을 언급한다면 <code class="language-plaintext highlighter-rouge">m</code>이 모나드라는 뜻이고, 오류 처리 같은 걸 하겠구나라고만 생각해 주세요.</p>

<h1 id="이외에-글에서-언급한-표준-라이브러리-함수들">이외에 글에서 언급한 표준 라이브러리 함수들</h1>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">-- 환경 변수를 불러옵니다.</span>
<span class="n">getEnv</span> <span class="o">::</span> <span class="kt">String</span> <span class="o">-&gt;</span> <span class="kt">IO</span> <span class="kt">String</span>

<span class="c1">-- 문자열을 특정한 문자로 잘라서 문자열의 리스트로 만듭니다.</span>
<span class="n">split</span> <span class="o">::</span> <span class="kt">Char</span> <span class="o">-&gt;</span> <span class="kt">String</span> <span class="o">-&gt;</span> <span class="p">[</span><span class="kt">String</span><span class="p">]</span>

<span class="c1">-- 어떤 조건이 참일 때만 주어진 연산을 실행합니다.</span>
<span class="c1">-- `Applicative`는 `Monad`보다 좀 약한 것이라고만 알고 있으면 됩니다.</span>
<span class="n">when</span> <span class="o">::</span> <span class="kt">Applicative</span> <span class="n">f</span> <span class="o">=&gt;</span> <span class="kt">Bool</span> <span class="o">-&gt;</span> <span class="n">f</span> <span class="nb">()</span> <span class="o">-&gt;</span> <span class="n">f</span> <span class="nb">()</span>

<span class="c1">-- 리스트 (등의 자료구조)가 비어 있는지 확인합니다.</span>
<span class="c1">-- `Foldable`은 값을 하나씩 꺼내서 하나의 값으로 종합할 수 있는 성질을 의미하며, 리스트를 일반화합니다.</span>
<span class="n">null</span> <span class="o">::</span> <span class="kt">Foldable</span> <span class="n">t</span> <span class="o">=&gt;</span> <span class="n">t</span> <span class="n">a</span> <span class="o">-&gt;</span> <span class="kt">Bool</span>

<span class="c1">-- 입출력 오류를 터뜨립니다.</span>
<span class="c1">-- `Exception`은 예외를 의미합니다.</span>
<span class="c1">-- 이 함수는 어차피 반환하기 전에 터지기 때문에 `a`는 자리를 채우기 의한 의미 없는 타입 변수입니다.</span>
<span class="n">throwIO</span> <span class="o">::</span> <span class="kt">Exception</span> <span class="n">e</span> <span class="o">=&gt;</span> <span class="n">e</span> <span class="o">-&gt;</span> <span class="kt">IO</span> <span class="n">a</span>

<span class="c1">-- 주어진 메시지를 가지는 사용자 정의 예외를 만듭니다.</span>
<span class="c1">-- 예외 값을 만들기만 하고, 실제로 오류를 터뜨리지는 않습니다.</span>
<span class="n">userError</span> <span class="o">::</span> <span class="kt">String</span> <span class="o">-&gt;</span> <span class="kt">IOError</span>

<span class="c1">-- `a`를 `b`로 만드는 일반적인 함수를 `f a`를 `f b`로 만드는 함수로 만듭니다.</span>
<span class="c1">-- `f` = `Maybe`일 때는 `Just x`는 원래 함수처럼 연산하고 `Nothing`은 `Nothing`으로 만드는 동작을 합니다.</span>
<span class="c1">-- `Functor`는 `Applicative`보다 더 약한 것이라고만 알고 있으면 됩니다.</span>
<span class="n">fmap</span> <span class="o">::</span> <span class="kt">Functor</span> <span class="n">f</span> <span class="o">=&gt;</span> <span class="p">(</span><span class="n">a</span> <span class="o">-&gt;</span> <span class="n">b</span><span class="p">)</span> <span class="o">-&gt;</span> <span class="p">(</span><span class="n">f</span> <span class="n">a</span> <span class="o">-&gt;</span> <span class="n">f</span> <span class="n">b</span><span class="p">)</span>
</code></pre></div></div>

<p>이 정도면 글을 읽는 데 필요한 사전 지식은 전부 쓴 것 같네요. <a href="/2022/12/04/parse-don-t-validate.html">준비 되셨나요?</a></p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:fn-typeclass-as-trait" role="doc-endnote">
      <p>비슷한 기능을 지원하는 Rust에서는 아예 Trait(특성)이라는 이름을 지어 두었습니다. <a href="#fnref:fn-typeclass-as-trait" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:fn-unit-type" role="doc-endnote">
      <p>바닥 값(<code class="language-plaintext highlighter-rouge">undefined</code> 등 유효한 값으로 평가되지 않는 표현) 등의 예외적인 경우를 제외하면 단위 타입 <code class="language-plaintext highlighter-rouge">()</code>의 값은 무조건 <code class="language-plaintext highlighter-rouge">()</code>로 결정되기 때문에 "정보값이 없다"고 합니다. 실제로 단위 타입을 지원하는 <code class="language-plaintext highlighter-rouge">Rust</code>에서는 단위 타입이 0바이트를 차지합니다. <a href="#fnref:fn-unit-type" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="함수형" /><category term="Haskell" /><category term="보충" /><summary type="html"><![CDATA[이 글은 검증하지 말고 파싱하라의 내용을 보충할 목적으로 작성하는 Haskell 속성 강좌 같은 느낌의 글입니다. 글을 읽는 데 꼭 필요한 분량만 작성했기 때문에 실제로 Haskell을 학습하는 목적으로는 적합하지 않으며, 진지하게 Haskell을 배우신다면 Haskell 공식 홈페이지에서 안내하는 학습 자료 중 하나를 골라서 읽어보는 것을 권장드립니다.]]></summary></entry><entry><title type="html">검증하지 말고 파싱하라</title><link href="https://eatchangmyeong.github.io/2022/12/04/parse-don-t-validate.html" rel="alternate" type="text/html" title="검증하지 말고 파싱하라" /><published>2022-12-04T00:00:00+09:00</published><updated>2022-12-04T00:00:00+09:00</updated><id>https://eatchangmyeong.github.io/2022/12/04/parse-don-t-validate</id><content type="html" xml:base="https://eatchangmyeong.github.io/2022/12/04/parse-don-t-validate.html"><![CDATA[<p><em>이 글은 Alexis King님의 <a href="https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/">Parse, don't validate</a>를 원 작성자님의 동의 하에 한국어로 번역한 글입니다. Haskell에 대한 사전 지식이 필요하며, 보충 글인 <a href="/2022/12/04/haskell-in-a-hurry.html">급하게 배우는 Haskell</a>에 필요한 만큼만 설명해 두었습니다. 오역이 있을 경우 댓글로 알려주세요.</em></p>

<p><em>이어지는 글인 <a href="https://lexi-lambda.github.io/blog/2020/01/19/no-dynamic-type-systems-are-not-inherently-more-open/">No, dynamic type systems are not inherently more open</a>은 <a href="https://donghwi.dev/no-dynamic-type-systems-are-not-inherently-more-open/">서동휘님의 블로그</a>에서 한국어로 읽을 수 있습니다.</em></p>

<p><em>번역 초안을 검수하고 매끄러운 번역문을 제안해주신 카타님과 RanolP님께 감사드립니다.</em></p>

<hr />

<p>그동안 저는 타입 주도 설계를 실천한다는 게 무슨 의미인지 짧고 단순하게 설명하는 데 애를 먹었습니다. 누가 "이런 접근은 어떻게 하신 건가요?"라고 물어볼 때면 만족스러운 답을 내놓지 못할 때가 많았습니다. 뜬금없이 생각난 것은 절대 아니라고 말씀드릴 수 있지만(저는 "올바른" 설계를 주먹구구로 만들어내는 대신 분명한 절차적 방법을 통해 만들어내고 있습니다), 그 과정을 남들에게 이야기하는 건 잘 하지 못했습니다.</p>

<p>그러다 한 달쯤 전에 정적·동적 타입 언어에서 JSON을 파싱할 때 경험한 차이점을 <a href="https://twitter.com/lexi_lambda/status/1182242561655746560">트위터에 회고한 적이 있었는데</a>, 바로 그때 제가 찾던 설명이 무엇인지 드디어 깨달았습니다. 제게 타입 주도 설계가 의미하는 바를 똑 소리 나는 한 문장으로 함축해 보겠습니다. 겨우 3단어밖에 안 됩니다.</p>

<blockquote>
  <p><strong>검증하지 말고 파싱하라.</strong></p>
</blockquote>

<h1 id="타입-주도-설계의-본질">타입 주도 설계의 본질</h1>

<p>솔직히 말씀드리자면, 독자분께서 타입 주도 설계가 뭔지 미리 알고 오신 게 아니면 이렇게 깔쌈한 한 문장으로 정리해도 그다지 와닿지 않을 것 같습니다. 다행히 이 글의 나머지 분량은 전부 이 설명에 할애할 예정입니다. 이제부터 저 문장을 통해 뭘 전달하고 싶었는지 낱낱이 해부해 볼 텐데, 그 전에 한 번 시행착오를 해 보겠습니다.</p>

<h2 id="가능성의-영역">가능성의 영역</h2>

<p>정적 타입 시스템의 좋은 점 중 하나가 "이런 함수를 작성하는 게 가능할까?" 같은 질문을 가능하게, 어떨 때는 심지어 쉽게 만들어 준다는 것입니다. 극단적인 예시로 다음 Haskell 타입 시그니처를 생각해 봅시다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">foo</span> <span class="o">::</span> <span class="kt">Integer</span> <span class="o">-&gt;</span> <span class="kt">Void</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">foo</code>를 구현하는 것이 가능할까요? 정답은 당연히 <em>아니다</em>입니다. <code class="language-plaintext highlighter-rouge">Void</code>는 아무런 값도 없는 타입이고, <em>어떤</em> 함수를 가져와도 <code class="language-plaintext highlighter-rouge">Void</code>의 없는 값을 만들어낼 수는 없죠.<sup id="fnref:fn-void" role="doc-noteref"><a href="#fn:fn-void" class="footnote" rel="footnote">1</a></sup> 이 예시는 별로 재미는 없지만, 다른 현실적인 타입을 들고 오면 더 재미있는 문제가 됩니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">head</span> <span class="o">::</span> <span class="p">[</span><span class="n">a</span><span class="p">]</span> <span class="o">-&gt;</span> <span class="n">a</span>
</code></pre></div></div>

<p>리스트의 첫 원소를 반환하는 함수입니다. 구현할 수 있을까요? 얼핏 보기에 전혀 복잡해 보이지 않지만, 실제로 구현하려고 하면 컴파일러가 만족하지 못합니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">head</span> <span class="o">::</span> <span class="p">[</span><span class="n">a</span><span class="p">]</span> <span class="o">-&gt;</span> <span class="n">a</span>
<span class="n">head</span> <span class="p">(</span><span class="n">x</span><span class="o">:</span><span class="kr">_</span><span class="p">)</span> <span class="o">=</span> <span class="n">x</span>
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>warning: [-Wincomplete-patterns]
    Pattern match(es) are non-exhaustive
    In an equation for ‘head’: Patterns not matched: []
</code></pre></div></div>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>경고: [-Wincomplete-patterns]
    모든 경우의 패턴을 열거하지 않았습니다
    'head'에 대한 등식에서: 매치되지 않은 패턴: []
</code></pre></div></div>

<p><sup id="fnref:tn-error-message" role="doc-noteref"><a href="#fn:tn-error-message" class="footnote" rel="footnote">2</a></sup></p>

<p>메시지를 잘 읽어보면 이 함수가 <em>부분함수</em>라고, 즉 가능한 모든 입력에 대해 정의되지 않았다고 알려주고 있습니다. 특히 이 함수는 입력이 빈 리스트 <code class="language-plaintext highlighter-rouge">[]</code>일 때 정의되지 않습니다. 리스트가 비어 있으면 리스트의 첫 번째 원소도 반환할 수 없으니(반환할 원소가 없으니까요!) 말이 되네요. 여기서 우리는 이 함수 역시 구현할 수 없다는 새로운 사실을 알게 됩니다.</p>

<h2 id="부분함수를-전함수로">부분함수를 전함수로</h2>

<p>동적 타입으로 프로그래밍을 하던 분은 이 상황이 당혹스러울 수도 있겠습니다. 리스트가 있으면 첫 원소가 뭔지는 알아야죠. 물론 Haskell에서도 "리스트의 첫 번째 원소 구하기" 연산이 불가능한 건 아니고, 밑작업이 조금 더 필요할 뿐입니다. 이 <code class="language-plaintext highlighter-rouge">head</code> 함수를 고치는 방법이 2가지 있는데, 쉬운 방법부터 해 보겠습니다.</p>

<h2 id="기대가-없으면-실망도-없다">기대가 없으면 실망도 없다</h2>

<p>위에서 얘기했듯이 <code class="language-plaintext highlighter-rouge">head</code>가 부분함수인 이유는 리스트가 비어 있을 때는 반환할 원소가 없기 때문입니다. 지키지 못할 약속을 한 것입니다. 다행히 이 딜레마를 해결하는 쉬운 방법이 있습니다. 더 약한 약속을 하먼 됩니다. 호출자에게 리스트의 원소를 돌려준다는 보장은 할 수 없으니, 애초에 실망하지 않도록 기대를 덜 하게 하는 것입니다. 이러면 <code class="language-plaintext highlighter-rouge">head</code>가 리스트의 원소를 반환할 수 있도록 최선을 다하겠지만, 아무것도 반환하지 않을 권리도 보장받을 수 있습니다. Haskell에서는 이런 가능성을 <code class="language-plaintext highlighter-rouge">Maybe</code> 타입으로 나타낼 수 있습니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">head</span> <span class="o">::</span> <span class="p">[</span><span class="n">a</span><span class="p">]</span> <span class="o">-&gt;</span> <span class="kt">Maybe</span> <span class="n">a</span>
</code></pre></div></div>

<p>타입 <code class="language-plaintext highlighter-rouge">a</code>의 값을 아예 만들 수 없으면 <code class="language-plaintext highlighter-rouge">Nothing</code>을 반환할 수 있도록 해주는 이 <code class="language-plaintext highlighter-rouge">Maybe</code> 타입 덕에 드디어 <code class="language-plaintext highlighter-rouge">head</code>를 구현할 수 있게 됩니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">head</span> <span class="o">::</span> <span class="p">[</span><span class="n">a</span><span class="p">]</span> <span class="o">-&gt;</span> <span class="kt">Maybe</span> <span class="n">a</span>
<span class="n">head</span> <span class="p">(</span><span class="n">x</span><span class="o">:</span><span class="kr">_</span><span class="p">)</span> <span class="o">=</span> <span class="kt">Just</span> <span class="n">x</span>
<span class="n">head</span> <span class="kt">[]</span>    <span class="o">=</span> <span class="kt">Nothing</span>
</code></pre></div></div>

<p>그럼 다 해결된 거죠? 일단 그렇긴 한데... 이 방법에는 숨어있는 단점이 있습니다.</p>

<p><code class="language-plaintext highlighter-rouge">head</code>를 <em>구현할</em> 때는 <code class="language-plaintext highlighter-rouge">Maybe</code>를 반환하는 것이 편리하다는 건 부정할 수 없습니다. 그런데 이 함수를 실제로 쓸 때가 되면 눈에 띄게 덜 편해집니다! <code class="language-plaintext highlighter-rouge">head</code>는 언제든 <code class="language-plaintext highlighter-rouge">Nothing</code>을 반환할 가능성이 있습니다. 그런 경우를 처리할 책임은 호출자에게 그대로 전가되고, 이렇게 남의 책임을 뒤집어쓰는 게 짜증날 때가 생각보다 많습니다. 감이 안 오신다면 아래 코드를 읽어 봅시다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">getConfigurationDirectories</span> <span class="o">::</span> <span class="kt">IO</span> <span class="p">[</span><span class="kt">FilePath</span><span class="p">]</span>
<span class="n">getConfigurationDirectories</span> <span class="o">=</span> <span class="kr">do</span>
  <span class="n">configDirsString</span> <span class="o">&lt;-</span> <span class="n">getEnv</span> <span class="s">"CONFIG_DIRS"</span>
  <span class="kr">let</span> <span class="n">configDirsList</span> <span class="o">=</span> <span class="n">split</span> <span class="sc">','</span> <span class="n">configDirsString</span>
  <span class="n">when</span> <span class="p">(</span><span class="n">null</span> <span class="n">configDirsList</span><span class="p">)</span> <span class="o">$</span>
    <span class="n">throwIO</span> <span class="o">$</span> <span class="n">userError</span> <span class="s">"CONFIG_DIRS가 비어 있습니다"</span>
  <span class="n">pure</span> <span class="n">configDirsList</span>

<span class="n">main</span> <span class="o">::</span> <span class="kt">IO</span> <span class="nb">()</span>
<span class="n">main</span> <span class="o">=</span> <span class="kr">do</span>
  <span class="n">configDirs</span> <span class="o">&lt;-</span> <span class="n">getConfigurationDirectories</span>
  <span class="kr">case</span> <span class="n">head</span> <span class="n">configDirs</span> <span class="kr">of</span>
    <span class="kt">Just</span> <span class="n">cacheDir</span> <span class="o">-&gt;</span> <span class="n">initializeCache</span> <span class="n">cacheDir</span>
    <span class="kt">Nothing</span> <span class="o">-&gt;</span> <span class="n">error</span> <span class="s">"뜨면 안 되는 오류임. configDirs가 비어있지 않은 걸 이미 확인했음"</span>
</code></pre></div></div>

<p><sup id="fnref:tn-getconfigurationdirectories" role="doc-noteref"><a href="#fn:tn-getconfigurationdirectories" class="footnote" rel="footnote">3</a></sup></p>

<p><code class="language-plaintext highlighter-rouge">getConfigurationDirectories</code>가 환경 변수에서 파일 경로의 리스트를 받아올 때는 그 리스트가 비어 있지 않은지도 미리 확인합니다. 그런데 <code class="language-plaintext highlighter-rouge">main</code> 함수에서 그 리스트의 첫 원소를 구하려고 할 때는 <code class="language-plaintext highlighter-rouge">head</code>가 반환하는 <code class="language-plaintext highlighter-rouge">Maybe FilePath</code>가 <code class="language-plaintext highlighter-rouge">Nothing</code>일 일은 절대 없는 걸 알면서도 그 경우를 확인해서 처리해야 합니다! 이런 상황이 왜 그렇게 끔찍하게 나쁜지 이유를 몇 가지 들어 보겠습니다.</p>

<ol>
  <li>첫째로, 리스트에 뭐가 있는 걸 이미 확인했는데 굳이 확인한 걸 또 확인하는 더러운 코드를 넣어야 된다고요? 너무 짜증나지 않나요?</li>
  <li>둘째로, 성능에 영향을 미칠 여지가 있습니다. 위에서 든 예시에서는 성능 영향이 미미하겠지만, 빡빡한 루프 안에서 돌아가는 등 성능 하락이 극대화될 수 있는 복잡한 시나리오는 얼마든지 생각할 수 있습니다.</li>
  <li>마지막이자 최악의 이유라면, 이 코드 자체가 터지기만 기다리고 있는 버그 덩어리입니다! 의도했든 아니든 <code class="language-plaintext highlighter-rouge">getConfigurationDirectories</code>를 빈 리스트를 확인하지 않도록 수정했다면 무슨 일이 생길까요? 프로그래머가 <code class="language-plaintext highlighter-rouge">main</code>도 같이 수정하는 걸 깜박 잊기라도 한다면 "불가능했던" 버그가 가능만 한 수준을 넘어 자꾸 튀어나오게 됩니다.</li>
</ol>

<p>확인을 중복으로 해야 하는 이 상황 때문에 타입 시스템에 구멍이 뚫려버립니다. 만약에 이 <code class="language-plaintext highlighter-rouge">Nothing</code>의 가능성이 없다는 것을 정적으로 <em>증명</em>만 할 수 있다면, <code class="language-plaintext highlighter-rouge">getConfigurationDirectories</code>이 빈 리스트를 확인하지 않게 바뀔 때 증명이 무효가 되어 컴파일 오류를 낼 것입니다. 하지만 지금으로서는 이미 보았듯이 테스트 코드에 의존하거나 코드를 직접 읽어서 버그를 잡아낼 수밖에 없습니다.</p>

<h2 id="엄선된-인자만-받습니다">엄선된 인자만 받습니다</h2>

<p>저희가 수정한 <code class="language-plaintext highlighter-rouge">head</code>에는 분명 개선할 점이 남아있습니다. 리스트가 비어 있지 않은 것을 확인했으면 이미 불가능한 걸 알고 있는 경우를 처리하지 않아도 무조건 첫 원소를 반환해 주는 똘똘한 녀석이었으면 좋겠는데요. 어떻게 하면 될까요?</p>

<p>이쯤에서 <code class="language-plaintext highlighter-rouge">head</code>의 원래 (부분함수였던 때의) 타입 시그니처를 다시 읽어 봅시다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">head</span> <span class="o">::</span> <span class="p">[</span><span class="n">a</span><span class="p">]</span> <span class="o">-&gt;</span> <span class="n">a</span>
</code></pre></div></div>

<p>위의 문단에서는 반환 타입으로 했던 약속을 약하게 만들어서 부분함수 타입 시그니처를 전함수로 만들 수 있다는 것을 보였습니다. 그런데 그 방법은 또 쓰고 싶지 않으니, 이제 바꿀 수 있는 것은 하나뿐, 바로 인자 타입입니다(여기서는 <code class="language-plaintext highlighter-rouge">[a]</code>). 반환 타입을 약화시키는 대신 인자 타입을 <em>강화</em>시키면 애초에 <code class="language-plaintext highlighter-rouge">head</code>에 빈 리스트가 전달되는 일은 없을 것입니다.</p>

<p>이 방법을 사용하려면 우선 비어 있지 않은 리스트를 나타내는 타입이 필요합니다. 다행히 <code class="language-plaintext highlighter-rouge">Data.List.NonEmpty</code> 모듈의 <code class="language-plaintext highlighter-rouge">NonEmpty</code> 타입이 정확히 그 역할을 합니다. 이 타입의 정의는 다음과 같습니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kr">data</span> <span class="kt">NonEmpty</span> <span class="n">a</span> <span class="o">=</span> <span class="n">a</span> <span class="o">:|</span> <span class="p">[</span><span class="n">a</span><span class="p">]</span>
</code></pre></div></div>

<p>잘 보면 <code class="language-plaintext highlighter-rouge">NonEmpty a</code>는 <code class="language-plaintext highlighter-rouge">a</code> 하나와 보통의 비어 있을 수도 있는 <code class="language-plaintext highlighter-rouge">[a]</code>의 순서쌍에 불과하다는 것을 알 수 있습니다. 리스트의 첫 원소를 나머지 꼬리와 별도로 보관함으로써 비어 있지 않은 리스트를 편리하게 나타낼 수 있게 된 것입니다. <code class="language-plaintext highlighter-rouge">[a]</code> 부분이 <code class="language-plaintext highlighter-rouge">[]</code>이더라도 <code class="language-plaintext highlighter-rouge">a</code> 부분은 항상 있을 수밖에 없습니다. 이제 <code class="language-plaintext highlighter-rouge">head</code> 함수를 구현하는 것은 누워서 떡 먹기입니다.<sup id="fnref:fn-nonempty" role="doc-noteref"><a href="#fn:fn-nonempty" class="footnote" rel="footnote">4</a></sup></p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">head</span> <span class="o">::</span> <span class="kt">NonEmpty</span> <span class="n">a</span> <span class="o">-&gt;</span> <span class="n">a</span>
<span class="n">head</span> <span class="p">(</span><span class="n">x</span><span class="o">:|</span><span class="kr">_</span><span class="p">)</span> <span class="o">=</span> <span class="n">x</span>
</code></pre></div></div>

<p>아까와 달리 이 정의는 GHC가 군말 없이 받아들입니다. 부분함수가 아니라 <em>전함수</em>를 정의했기 때문입니다. 이제 아까 작성한 프로그램에 새로 구현한 함수를 넣어서 고쳐 봅시다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">getConfigurationDirectories</span> <span class="o">::</span> <span class="kt">IO</span> <span class="p">(</span><span class="kt">NonEmpty</span> <span class="kt">FilePath</span><span class="p">)</span>
<span class="n">getConfigurationDirectories</span> <span class="o">=</span> <span class="kr">do</span>
  <span class="n">configDirsString</span> <span class="o">&lt;-</span> <span class="n">getEnv</span> <span class="s">"CONFIG_DIRS"</span>
  <span class="kr">let</span> <span class="n">configDirsList</span> <span class="o">=</span> <span class="n">split</span> <span class="sc">','</span> <span class="n">configDirsString</span>
  <span class="kr">case</span> <span class="n">nonEmpty</span> <span class="n">configDirsList</span> <span class="kr">of</span>
    <span class="kt">Just</span> <span class="n">nonEmptyConfigDirsList</span> <span class="o">-&gt;</span> <span class="n">pure</span> <span class="n">nonEmptyConfigDirsList</span>
    <span class="kt">Nothing</span> <span class="o">-&gt;</span> <span class="n">throwIO</span> <span class="o">$</span> <span class="n">userError</span> <span class="s">"CONFIG_DIRS가 비어 있습니다"</span>

<span class="n">main</span> <span class="o">::</span> <span class="kt">IO</span> <span class="nb">()</span>
<span class="n">main</span> <span class="o">=</span> <span class="kr">do</span>
  <span class="n">configDirs</span> <span class="o">&lt;-</span> <span class="n">getConfigurationDirectories</span>
  <span class="n">initializeCache</span> <span class="p">(</span><span class="n">head</span> <span class="n">configDirs</span><span class="p">)</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">main</code>에 있던 불필요한 확인 코드가 완전히 사라지고 <code class="language-plaintext highlighter-rouge">getConfigurationDirectories</code>에서만 딱 한 번 확인하네요! 이때 <code class="language-plaintext highlighter-rouge">[a]</code>를 <code class="language-plaintext highlighter-rouge">NonEmpty a</code>로 만드는 데는 <code class="language-plaintext highlighter-rouge">Data.List.NonEmpty</code> 모듈의 <code class="language-plaintext highlighter-rouge">nonEmpty</code> 함수를 사용했고, 타입은 이렇습니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">nonEmpty</span> <span class="o">::</span> <span class="p">[</span><span class="n">a</span><span class="p">]</span> <span class="o">-&gt;</span> <span class="kt">Maybe</span> <span class="p">(</span><span class="kt">NonEmpty</span> <span class="n">a</span><span class="p">)</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">Maybe</code>가 아직 남아 있긴 하지만, 여기서는 <code class="language-plaintext highlighter-rouge">Nothing</code>의 경우를 프로그램에서 매우 일찍, 저희가 이미 입력 검증을 하고 있던 바로 그곳에서 처리합니다. 일단 이 확인이 끝나고 나면 <code class="language-plaintext highlighter-rouge">NonEmpty FilePath</code> 값이 남고, 이 값에는 리스트가 비어 있지 않다는 정보가 (타입 시스템 수준에서!) 고스란히 남아 있습니다. 관점을 달리하면 <code class="language-plaintext highlighter-rouge">NonEmpty a</code> 타입의 값은 <code class="language-plaintext highlighter-rouge">[a]</code> 타입의 값에 그 리스트가 비어 있지 않다는 <em>증명</em>이 붙은 것이라고 생각할 수 있습니다.</p>

<p>이전 문단에서 나왔던 문제도 <code class="language-plaintext highlighter-rouge">head</code>의 반환 타입을 약화시키는 대신 인자 타입을 강화시킴으로써 전부 해결되었습니다.</p>

<ul>
  <li>코드에 중복된 확인이 없으니 성능 오버헤드가 생길 여지도 아예 없습니다.</li>
  <li>또한 <code class="language-plaintext highlighter-rouge">getConfigurationDirectories</code>를 리스트가 비어 있는지 확인하지 않도록 변경하면 함수 자체의 반환 타입도 바뀌어야 합니다. 그러면 <code class="language-plaintext highlighter-rouge">main</code>이 타입 검사를 못 통과하니 프로그램이 돌아가기도 전에 문제 상황을 알 수 있습니다!</li>
</ul>

<p>게다가 <code class="language-plaintext highlighter-rouge">nonEmpty</code>와 <code class="language-plaintext highlighter-rouge">head</code>를 합성하면 <code class="language-plaintext highlighter-rouge">head</code>의 원래 동작도 재현할 수 있습니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">head'</span> <span class="o">::</span> <span class="p">[</span><span class="n">a</span><span class="p">]</span> <span class="o">-&gt;</span> <span class="kt">Maybe</span> <span class="n">a</span>
<span class="n">head'</span> <span class="o">=</span> <span class="n">fmap</span> <span class="n">head</span> <span class="o">.</span> <span class="n">nonEmpty</span>
</code></pre></div></div>

<p>역은 성립하지 <em>않습니다</em>. 즉, 위 문단의 <code class="language-plaintext highlighter-rouge">head</code>로 아래 문단의 <code class="language-plaintext highlighter-rouge">head</code>를 만들 수 있는 방법은 없습니다. 종합하자면 두 번째 접근이 모든 면에서 우월하네요.</p>

<h2 id="파싱의-힘">파싱의 힘</h2>

<p>여기까지 읽으셨다면 위에서 든 예시가 이 블로그 글 제목이랑 무슨 상관인지 궁금해하실 것 같습니다. 지금까지 비어 있지 않은 리스트를 검증하는 방법만 2가지 살펴봤는데 대체 파싱이 어디 있다는 거죠? 이렇게 해석하는 것도 잘못된 건 아니지만, 다른 관점도 한번 소개해 보겠습니다. 제가 생각하는 검증과 파싱의 차이는 거의 전적으로 정보가 얼마나 보존되는지에 달려 있습니다. 아래의 두 함수를 읽어보세요.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">validateNonEmpty</span> <span class="o">::</span> <span class="p">[</span><span class="n">a</span><span class="p">]</span> <span class="o">-&gt;</span> <span class="kt">IO</span> <span class="nb">()</span>
<span class="n">validateNonEmpty</span> <span class="p">(</span><span class="kr">_</span><span class="o">:</span><span class="kr">_</span><span class="p">)</span> <span class="o">=</span> <span class="n">pure</span> <span class="nb">()</span>
<span class="n">validateNonEmpty</span> <span class="kt">[]</span> <span class="o">=</span> <span class="n">throwIO</span> <span class="o">$</span> <span class="n">userError</span> <span class="s">"리스트가 비어 있습니다"</span>

<span class="n">parseNonEmpty</span> <span class="o">::</span> <span class="p">[</span><span class="n">a</span><span class="p">]</span> <span class="o">-&gt;</span> <span class="kt">IO</span> <span class="p">(</span><span class="kt">NonEmpty</span> <span class="n">a</span><span class="p">)</span>
<span class="n">parseNonEmpty</span> <span class="p">(</span><span class="n">x</span><span class="o">:</span><span class="n">xs</span><span class="p">)</span> <span class="o">=</span> <span class="n">pure</span> <span class="p">(</span><span class="n">x</span><span class="o">:|</span><span class="n">xs</span><span class="p">)</span>
<span class="n">parseNonEmpty</span> <span class="kt">[]</span> <span class="o">=</span> <span class="n">throwIO</span> <span class="o">$</span> <span class="n">userError</span> <span class="s">"리스트가 비어 있습니다"</span>
</code></pre></div></div>

<p>이 두 함수는 인자로 받은 리스트가 비어 있는지 확인하고, 비었으면 프로그램을 종료하고 오류 메시지를 띄운다는 점에서는 같습니다. 두 함수의 차이는 오직 반환 타입뿐입니다. <code class="language-plaintext highlighter-rouge">validateNonEmpty</code>는 항상 정보값이 없는 <code class="language-plaintext highlighter-rouge">()</code>를 반환하지만, <code class="language-plaintext highlighter-rouge">parseNonEmpty</code>는 획득한 정보를 타입 시스템에 보존하는, 입력받은 타입을 정제한 <code class="language-plaintext highlighter-rouge">NonEmpty a</code>를 반환합니다. 두 함수 모두 같은 것을 확인하지만, <code class="language-plaintext highlighter-rouge">validateNonEmpty</code>가 그냥 버려버리는 실행 중에 얻은 정보를 <code class="language-plaintext highlighter-rouge">parseNonEmpty</code>는 호출자에게 고스란히 전달해 줍니다.</p>

<p>이 두 함수는 정적 타입 시스템에 대한 두 가지 관점을 명쾌하게 나타내 줍니다. <code class="language-plaintext highlighter-rouge">validateNonEmpty</code>도 타입 검사를 적당히 통과하지만, 타입 시스템을 십분 활용하는 것은 <code class="language-plaintext highlighter-rouge">parseNonEmpty</code>뿐입니다. 왜 <code class="language-plaintext highlighter-rouge">parseNonEmpty</code>가 더 나은지 이해하셨다면, 제가 "검증하지 말고 파싱하라"는 구호에 담은 의미는 모두 이해하신 겁니다. 그래도 여전히 <code class="language-plaintext highlighter-rouge">parseNonEmpty</code>라는 이름이 미심쩍을 수도 있겠습니다. 이 함수가 정말 뭔가를 <em>파싱하는</em> 걸까요, 아니면 그냥 입력값을 검증하고 결과를 내놓는 것에 불과할까요? "파싱하다"나 "검증하다"의 의미는 사람마다 조금씩 다르겠지만, 저는 <code class="language-plaintext highlighter-rouge">parseNonEmpty</code>가 (꽤 단순하긴 해도) 진정한 의미의 파서라고 믿고 있습니다.</p>

<p>애당초 파서라는 게 뭔가요? 파서란 덜 구조화된 입력을 받아서 더 구조화된 출력을 만드는 함수 그 이상도 이하도 아닙니다. 파서는 정의상 부분함수이므로(정의역의 어떤 값은 공역의 어떤 값과도 대응되지 않음), 모든 파서는 어떻게든 실패한 경우를 나타낼 수 있어야 합니다. 파서는 웬만하면 텍스트를 입력으로 받지만 반드시 그래야 하는 것은 결코 아니며, 리스트를 비어 있지 않은 리스트로 파싱하고, 프로그램을 종료하고 오류 메시지를 띄움으로써 실패를 나타내는 <code class="language-plaintext highlighter-rouge">parseNonEmpty</code>는 흠잡을 데 없는 엄연한 파서입니다.</p>

<p>이 유연한 정의를 따를 때 파서는 매우 강력한 도구가 됩니다. 프로그램과 바깥 세계 사이를 꽉 막고 서서 올바른 입력만을 걸러내고, 일단 이 거름망을 통과한 입력은 두 번 다시 확인하지 않아도 됩니다! 파싱의 위력을 잘 알고 있는 하스켈러들은 틈만 나면 여러 가지 종류의 파서를 사용합니다.</p>

<ul>
  <li><a href="https://hackage.haskell.org/package/aeson">aeson</a> 라이브러리는 JSON 데이터를 도메인 타입으로 파싱하는 <code class="language-plaintext highlighter-rouge">Parser</code> 타입을 제공합니다.</li>
  <li>이와 비슷하게 <a href="https://hackage.haskell.org/package/optparse-applicative">optparse-applicative</a>는 명령줄 인자를 파싱하는 여러 가지 파서 콤비네이터<sup id="fnref:tn-parser-combinator" role="doc-noteref"><a href="#fn:tn-parser-combinator" class="footnote" rel="footnote">5</a></sup>를 제공합니다.</li>
  <li><a href="https://hackage.haskell.org/package/persistent">persistent</a>나 <a href="https://hackage.haskell.org/package/postgresql-simple">postgresql-simple</a>과 같은 데이터베이스 라이브러리는 외부 데이터 저장소에서 값을 파싱해 오는 메커니즘이 있습니다.</li>
  <li><a href="https://hackage.haskell.org/package/servant">servant</a> 생태계는 경로 컴포넌트, 쿼리 파라미터, HTTP 헤더 등을 Haskell 자료형으로 파싱해 오는 것을 중점으로 이루어져 있습니다.</li>
</ul>

<p>이 라이브러리들 모두 내가 작성한 Haskell 프로그램과 바깥 세계 사이를 이어준다는 공통점이 있습니다. 바깥 세계는 합과 곱 타입 대신 바이트열만이 통하는 세계이니 파싱은 거부할 수 없는 운명인 셈입니다. 이렇게 데이터를 실제로 조작하기 전에 데이터를 받자마자 파싱을 하는 것은 여러 종류의 버그, 심지어는 보안 취약점을 예방하는 데도 효과적입니다.</p>

<p>데이터를 받자마자 파싱해버리는 이 접근의 단점이 있다면 데이터를 실제로 사용하기 한참 전부터 파싱을 해 두어야 하는 경우가 있을 수 있다는 것입니다. 동적 타입 언어에서는 테스트 코드를 치밀하게 짜놓지 않으면 파싱과 처리 로직이 서로 잘 어우러지게 코딩하는 것이 힘들어지고, 테스트 코드 자체도 유지보수하기 정말 귀찮습니다. 그런데 정적 타입 시스템만 있다면 위의 <code class="language-plaintext highlighter-rouge">NonEmpty</code> 예시에서 보았듯이 문제가 놀랍도록 단순해집니다. 파싱과 처리 로직이 조금이라도 어긋났다가는 애초에 컴파일도 안 되니까요.</p>

<h2 id="검증의-위험성">검증의 위험성</h2>

<p>여기까지 읽고 나서 독자 여러분이 파싱이 검증보다 낫다는 의견에 조금이라도 설득됐기를 바라지만, 그래도 떨쳐내지 못하는 의심이 조금 남아 있을 것 같습니다. 어차피 타입 시스템 때문에 필요한 확인은 다 될 것 같은데 그래도 검증이 그렇게 나쁜가요? 오류 메시지 읽기가 좀 어려울 수는 있어도 중복된 확인 코드 좀 있는 게 뭐 어때서요?</p>

<p>아쉽지만 그게 그렇게 단순한 문제가 아닙니다. 애드 혹 검증은 <a href="http://langsec.org/">언어론적 보안</a><sup id="fnref:tn-langsec" role="doc-noteref"><a href="#fn:tn-langsec" class="footnote" rel="footnote">6</a></sup> 판에서 <em>샷건 파싱</em>이라고 하는 현상을 일으킵니다. 2016년 논문인 <a href="http://langsec.org/papers/langsec-cwes-secdev2016.pdf">The Seven Turrets of Babel: A Taxonomy of LangSec Errors and How to Expunge Them</a>에서는 샷건 파싱을 다음과 같이 정의하고 있습니다.</p>

<blockquote>
  <p>샷건 파싱이란 파싱과 입력 검증 코드가 뒤섞여서 데이터를 처리하는 코드에 흩뿌려진 프로그래밍 안티패턴으로, 입력 데이터를 주먹구구로 검증하고 체계적인 정당화 없이 이 정도면 "나쁜" 입력이 전부 걸러지겠지 하고 바라는 것을 의미한다.</p>

  <p>Shotgun parsing is a programming antipattern whereby parsing and input-validating code is mixed with and spread across processing code—throwing a cloud of checks at the input, and hoping, without any systematic justification, that one or another would catch all the “bad” cases.</p>
</blockquote>

<p>해당 논문에서는 이러한 검증 방법에 내재되어 있는 문제점 역시 서술하고 있습니다.</p>

<blockquote>
  <p>샷건 파싱은 필연적으로 프로그램이 잘못된 입력을 처리하지 않고 거부하는 능력을 저해한다. 입력 스트림에서 오류를 뒤늦게 발견할 경우 잘못된 입력의 일부분은 이미 처리된 상태로, 프로그램의 상태를 정확하게 예측하기 어려워지는 결과를 낳는다.</p>

  <p>Shotgun parsing necessarily deprives the program of the ability to reject invalid input instead of processing it. Late-discovered errors in an input stream will result in some portion of invalid input having been processed, with the consequence that program state is difficult to accurately predict.</p>
</blockquote>

<p>다시 설명하자면, 모든 입력을 받자마자 파싱하지 않는 프로그램은 올바른 부분의 입력을 처리하다가 다른 부분이 잘못되었다는 것을 뒤늦게 알고 일관성을 유지하기 위해 이미 수정해버린 것을 허겁지겁 돌려놓아야 하는 리스크를 안게 됩니다. 이것이 가능한 경우도 가끔 있지만(RDBMS에서 트랜잭션을 롤백하는 경우 등), 일반적으로는 불가능합니다.</p>

<p>샷건 파싱이 검증과 무슨 상관인지 바로 감이 오지 않을 수도 있습니다. 애초에 데이터를 바로 검증하면 샷건 파싱의 리스크도 해결되는 거니까요. 그런데 검증에 기반한 접근의 문제점은 모든 데이터가 진짜로 바로 검증되었는지, 아니면 일명 "불가능한" 경우들이 실제로 일어날 수도 있는지 알기 어렵거나 불가능하다는 것입니다. 프로그램의 어디를 실행하고 있든 예외가 생기는 것이 가능만 한 게 아니라 잊을 만하면 나올 것이라고 가정해야 합니다.</p>

<p>파싱에 기반한 접근은 프로그램을 파싱과 실행의 두 단계로 분리하고 잘못된 입력에 의한 오류는 첫 단계에 격리함으로써 문제를 회피합니다. 나머지 실행 단계에서 실패할 경우의 수는 비교적 적고, 필요한 만큼 세심한 관심을 쏟아 처리할 수 있습니다.</p>

<h1 id="검증-말고-파싱-이론과-실제">검증 말고 파싱, 이론과 실제</h1>

<p>지금까지 뭔가 광고 같은 느낌으로 글을 쓰긴 했습니다. "여보! 아버님 댁에 파서 놔드려야겠어요." 제가 글을 잘 썼다면 아마 설득된 분들이 조금은 있을 거라고 생각합니다. 하지만 독자 여러분이 "무엇을"과 "왜"를 이해하셨다고 해도, "어떻게"에 대해서는 아직도 뜬구름 잡는 느낌일 수도 있겠습니다.</p>

<p>저의 조언은... "자료형에 집중하기"가 되겠습니다.</p>

<p>키-값 쌍을 나타내는 튜플의 리스트를 받는 함수를 짜고 있는데, 갑자기 리스트에 중복된 키가 있으면 어떻게 처리할지 난감하다는 사실을 깨달았다고 해 봅시다. 리스트에 중복이 없는지 확인하는 함수를 짜는 것도 방법이겠지만...</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">checkNoDuplicateKeys</span> <span class="o">::</span> <span class="p">(</span><span class="kt">MonadError</span> <span class="kt">AppError</span> <span class="n">m</span><span class="p">,</span> <span class="kt">Eq</span> <span class="n">k</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">[(</span><span class="n">k</span><span class="p">,</span> <span class="n">v</span><span class="p">)]</span> <span class="o">-&gt;</span> <span class="n">m</span> <span class="nb">()</span>
</code></pre></div></div>

<p>이런 확인 사항은 불안합니다. 잊어버리기가 너무 쉬운 것입니다. 이 함수는 반환값을 사용하지 않고 버리기 때문에 항상 생략할 수 있으며, 이 확인이 필요한 코드도 타입 검사를 문제 없이 통과합니다. 더 나은 방법으로는 <code class="language-plaintext highlighter-rouge">Map</code>과 같이 중복 키를 만드는 것을 금지하는 자료구조를 고르는 것이 있습니다. 구현하려고 하는 함수의 타입 시그니처를 튜플의 리스트 대신 <code class="language-plaintext highlighter-rouge">Map</code>을 받도록 고치고, 평소에 하던 대로 구현해 보세요.</p>

<p>구현이 끝나면 방금 구현한 함수를 부르는 곳에서는 아직 튜플의 리스트를 전달하려고 하니 타입 검사가 실패할 겁니다. 호출자가 그 값을 인자나 다른 함수의 결과로 받았다면, 그 길을 따라 호출 체인을 타고 올라가면서 리스트를 <code class="language-plaintext highlighter-rouge">Map</code>으로 계속 바꾸어 보세요. 그러다가 결국에는 처음에 값이 생긴 곳이나, 중복 키를 실제로 허용해야 되는 곳까지 거슬러 올라갈 것입니다. 바로 이때 <code class="language-plaintext highlighter-rouge">checkNoDuplicateKeys</code>를 다음과 같이 조금 수정해서 호출하면 되겠습니다.</p>

<div class="language-haskell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">checkNoDuplicateKeys</span> <span class="o">::</span> <span class="p">(</span><span class="kt">MonadError</span> <span class="kt">AppError</span> <span class="n">m</span><span class="p">,</span> <span class="kt">Eq</span> <span class="n">k</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">[(</span><span class="n">k</span><span class="p">,</span> <span class="n">v</span><span class="p">)]</span> <span class="o">-&gt;</span> <span class="n">m</span> <span class="p">(</span><span class="kt">Map</span> <span class="n">k</span> <span class="n">v</span><span class="p">)</span>
</code></pre></div></div>

<p>이제 이 확인 함수의 결과는 프로그램을 실행하는 데 꼭 필요하기 때문에 함부로 뺄 수 <em>없습니다</em>!</p>

<p>이 가상의 시나리오를 살펴보면 두 가지 간단한 발상에 주목할 수 있습니다.</p>

<ol>
  <li><strong>잘못된 상태를 표현할 수 없는 자료구조를 씁시다.</strong> 합리적인 선 안에서 생각할 수 있는 가장 정확한 자료구조로 데이터를 모델링하세요. 지금 사용하고 있는 부호화 방식으로 특정한 가능성을 배제하는 것이 너무 어렵다면 여러분이 신경써야 할 성질을 더 쉽게 나타낼 수 있는 다른 부호화 방식도 고려해 보세요. 리팩토링을 두려워하지 마세요.</li>
  <li><strong>증명의 책임을 가능한 한 위로 올리되, 지나치게 올리지는 맙시다.</strong> 입력받은 데이터는 가능한 한 빠르게 필요한 만큼 가장 정밀한 표현으로 바꾸세요. 이상적으로는 <em>어떤</em> 데이터도 처리되지 않은 시점에 시스템의 경계에서 바꾸는 것이 좋습니다.<sup id="fnref:fn-authorization" role="doc-noteref"><a href="#fn:fn-authorization" class="footnote" rel="footnote">7</a></sup> <br />
어떤 코드 경로에서 어떤 데이터의 더 정밀한 표현을 요구한다면, 그 경로가 선택되는 즉시 그 데이터를 더 정밀한 표현으로 파싱하세요. 자료형이 제어 흐름을 반영하고 적응하도록 합 타입을 신중하게 사용하세요.</li>
</ol>

<p>다시 말해서, 주어진 자료 표현이 아니라 <em>있었으면 좋겠는</em> 자료 표현에 대한 함수를 작성하세요. 그러면 프로그램의 설계 과정은 여기서 생기는 간극을 메우는 여정이 되고, 보통 양쪽 끝에서 동시에 다리를 지으면서 만날 때까지 좁혀나가게 됩니다. 리팩토링을 하면서 뭔가 새로운 것을 배울 수도 있으니 했던 설계의 부분부분을 반복적으로 수정하는 것도 두려워하지 마세요!</p>

<p>추가로 드릴 만한 조언들을 특별한 순서 없이 더 써 보겠습니다.</p>

<ul>
  <li><strong>코드에 구애받지 않고, 코드를 휘어잡는 자료형을 만듭시다.</strong> 지금 짜고 있는 함수에서 필요할 것 같다는 이유만으로 레코드 어딘가에 <code class="language-plaintext highlighter-rouge">Bool</code>을 넣고 싶은 충동은 참아 주세요. 올바른 자료 표현을 사용하도록 리팩토링하는 것을 두려워하지 마세요. 고쳐야 할 곳을 모두 고쳤는지는 타입 시스템이 알아서 확인해 주고, 이렇게 하는 편이 나중에 머리가 덜 아플 겁니다.</li>
  <li><strong><code class="language-plaintext highlighter-rouge">m ()</code>을 반환하는 함수는 한 번 더 살펴봅시다.</strong> 딱히 의미 있는 결과 없이 부작용을 일으킬 목적의 함수라면 정말 필요하겠지만, 그 부작용이 오류를 띄울 목적으로 있는 거라면 더 좋은 방법이 있을지 생각해 보세요.</li>
  <li><strong>여러 단계에 걸쳐 파싱하는 것을 두려워하지 맙시다.</strong> 샷건 파싱을 하지 말라는 건 입력 데이터의 일부를 보고 나머지 데이터를 파싱하지 말라는 게 아니라 완전히 파싱하지 않은 데이터를 다루지 말라는 뜻입니다. 쓸모 있는 대다수의 파서는 맥락에 의존합니다.</li>
  <li><strong>정규화되지 않은 자료 표현(<em>특히</em> 가변인 경우)은 최대한 피합시다.</strong> 똑같은 데이터를 여러 곳에 저장하면 당연히 그 여러 곳이 어긋나는 잘못된 상태가 생길 수밖에 없습니다. 정보 공급원은 한 곳으로 통일합시다.
    <ul>
      <li><strong>정규화되지 않은 자료 표현은 추상화 경계 안에 숨깁시다.</strong> 어쩔 수 없이 정규화를 할 수 없는 경우라면, 캡슐화를 통해 신뢰할 수 있는 작은 모듈이 올바른 자료 표현을 유지하는 책임을 맡도록 합시다.</li>
    </ul>
  </li>
  <li><strong>추상 자료형을 통해 검증 함수를 파서로 위장합시다.</strong> 특정한 범위 안에 있는 정수와 같이 Haskell에서 제공하는 도구만으로는 잘못된 상태를 표현하지 못하도록 하는 것이 비현실적인 경우가 있습니다. 이때는 추상적인 <code class="language-plaintext highlighter-rouge">newtype</code>문과 스마트 생성자를 병용해서 검증 함수로 "가짜" 파서를 만들 수 있습니다.</li>
</ul>

<p>언제나처럼 독자 여러분의 판단에 맡기는 것이 최선입니다. 어디 한 곳에 있는 <code class="language-plaintext highlighter-rouge">error "불가능함"</code> 하나 없애자고 <a href="https://hackage.haskell.org/package/singletons">singletons</a>를 깔아서 프로그램을 전부 뒤집어 엎을 필요까지는 없겠죠. 대신 이런 경우 보기를 방사성 물질같이 하고, 적절하게 처리해 주세요. 정 안 되겠다면 이 코드를 수정할 다음 사람을 위해 주석으로 지켜야 되는 불변 조건 같은 거라도 달아두세요.</p>

<h1 id="톺아보고-돌아보고-더-볼-만한-글까지">톺아보고, 돌아보고, 더 볼 만한 글까지</h1>

<p>제가 할 얘기는 여기서 끝입니다. 이 블로그 글을 읽고 Haskell 타입 시스템을 200% 활용한다고 박사 학위를 딸 필요도 없고, 갓 나온 따끈따끈한 GHC 언어 확장 같은 걸 쓸 필요도 없다는 게(물론 있으면 좋을 때가 있긴 하죠!) 전달됐으면 좋겠네요. Haskell을 잘 사용하는 데 가장 큰 걸림돌이 무슨 카드를 쓸 수 있는지 모르는 데 있는 경우도 꽤 있고, 아쉽게도 Haskell 커뮤니티가 작아서 생기는 단점 중 하나가 소수만 아는 디자인 패턴과 기술을 서술하는 문서가 비교적 적다는 데 있습니다.</p>

<p>이 글에는 제가 새로 생각해 낸 발상이 하나도 없습니다. 심지어 핵심적인 발상("전함수를 작성하세요")도 개념적으로는 꽤 단순합니다. 그럼에도 불구하고 제가 Haskell을 짜는 실천적인 요령을 남에게 알려주는 것이 꽤나 어렵게 느껴집니다. 많은 시간을 추상적인 개념(이 중에서도 의미 있는 것들이 꽤 많습니다!) 얘기에만 할애하다가 정작 <em>과정</em>에 대해서는 쓸만한 얘기를 하지 못하기 십상입니다. 이번 글이 조금이라도 그 방향으로 나아가는 데 도움이 되었기를 바랍니다.</p>

<p>이 글의 주제에 대해서 읽어볼 만한 자료를 거의 모르는 것은 아쉽지만, 그나마 아는 자료가 하나 있습니다. Matt Parson님의 멋진 블로그 글 <a href="https://www.parsonsmatt.org/2017/10/11/type_safety_back_and_forth.html">Type Safety Back and Forth</a>는 아무리 추천해도 지나치지 않겠습니다. 이러한 발상에 대한 읽기 좋은 다른 관점과 실제로 잘 돌아가는 다른 예제가 궁금하시다면 한번 읽어보시는 것을 강력히 권합니다. 이 분야의 고급 자료도 읽어보고 싶다면 제가 여기에 쓴 것보다 더 복잡한 불변조건을 담아내는 여러 가지 기술을 소개하는 Matt Noonan님의 2018년 논문 <a href="https://kataskeue.com/gdp.pdf">Ghosts of Departed Proofs</a>를 추천드립니다.</p>

<p>글을 마치기 전에, 이 글에서 했던 것과 같이 리팩토링을 하는 것이 항상 쉽지만은 않다는 것을 말씀드리고 싶습니다. 제가 제시한 예제는 단순하지만, 현실은 그것보다 훨씬 복잡한 편이죠. 타입 주도 설계에 정통한 사람들도 어떤 불변조건을 타입 시스템에 담아내는 것을 힘겨워하시기도 하니, 어떤 문제가 원하는 대로 풀리지 않는다고 해서 나는 안 될 거라고 생각하지 말아 주세요! 이 글에 제시한 원리는 반드시 지켜야 할 요구사항이라기보다는 지향해야 할 이상점이라고만 생각해 주세요. 일단 해보는 것이 가장 중요합니다.</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:fn-void" role="doc-endnote">
      <p>엄밀히 말하면 이 설명은 <em>아무</em> 타입이나 될 수 있는 "바닥 값"을 무시하고 있습니다. (다른 언어의 <code class="language-plaintext highlighter-rouge">null</code>과 달리) "진짜" 값이 아니라 무한루프나 예외를 발생시키는 연산 따위이고 하스켈스러운 코드에서는 보통 이런 것들을 피하려고 하기 때문에 바닥 값을 무시하는 논의가 의미가 없어지는 건 아닙니다. 제가 말하는 것만 듣고 넘어가지 마시고 Danielsson et al.이 <a href="https://www.cs.ox.ac.uk/jeremy.gibbons/publications/fast+loose.pdf">Fast and Loose Reasoning is Morally Correct</a>라고 했던 것도 한번 읽어보세요. <a href="#fnref:fn-void" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:tn-error-message" role="doc-endnote">
      <p>(역자 주) 위의 한국어 오류 메시지는 원본 오류 메시지를 임의로 번역한 것으로, 실제 Haskell 컴파일러는 영문 오류 메시지를 출력합니다. <a href="#fnref:tn-error-message" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:tn-getconfigurationdirectories" role="doc-endnote">
      <p>(역자 주) 함수 <code class="language-plaintext highlighter-rouge">getConfigurationDirectories</code>는 환경 변수 <code class="language-plaintext highlighter-rouge">CONFIG_DIRS</code>를 확인해서 비어 있지 않으면 콤마로 나누어서 리스트로 반환하고, 비어 있으면 오류를 내면서 종료하는 함수입니다. <a href="#fnref:tn-getconfigurationdirectories" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:fn-nonempty" role="doc-endnote">
      <p>사실은 <code class="language-plaintext highlighter-rouge">Data.List.NonEmpty</code> 모듈에 이미 이 타입의 <code class="language-plaintext highlighter-rouge">head</code> 함수가 정의되어 있지만, 설명을 위해 다시 구현하기로 하겠습니다. <a href="#fnref:fn-nonempty" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:tn-parser-combinator" role="doc-endnote">
      <p>(역자 주) 상태 기계와 테이블을 이용해 파싱하는 전통적인 방법과 달리, 간단한 파서를 합성해 더욱 복잡한 파서를 정의하는 파싱 방법. <a href="#fnref:tn-parser-combinator" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:tn-langsec" role="doc-endnote">
      <p>(역자 주) 프로그램의 입출력을 형식 언어를 인식하는 과정으로 보고, 형식언어론적 방법을 이용해 보안 취약점을 예방하거나 방어하는 정보 보안의 분과. <a href="#fnref:tn-langsec" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:fn-authorization" role="doc-endnote">
      <p>어떤 경우에는 서비스 거부 공격을 방어하기 위해 사용자 입력을 파싱하기 전에 사용자 인증 같은 절차를 밟아야 할 텐데, 그 정도는 괜찮습니다. 사용자 인증은 비교적 표면적이 작은 작업이고, 시스템의 상태를 크게 바꾸지도 않습니다. <a href="#fnref:fn-authorization" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="함수형" /><category term="Haskell" /><category term="타입이론" /><category term="번역" /><summary type="html"><![CDATA[이 글은 Alexis King님의 Parse, don't validate를 원 작성자님의 동의 하에 한국어로 번역한 글입니다. Haskell에 대한 사전 지식이 필요하며, 보충 글인 급하게 배우는 Haskell에 필요한 만큼만 설명해 두었습니다. 오역이 있을 경우 댓글로 알려주세요.]]></summary></entry><entry><title type="html">TIL: 타입스크립트와 열거 불가능한 속성</title><link href="https://eatchangmyeong.github.io/2022/11/30/til-typescript-and-unenumerable-properties.html" rel="alternate" type="text/html" title="TIL: 타입스크립트와 열거 불가능한 속성" /><published>2022-11-30T00:00:00+09:00</published><updated>2022-11-30T00:00:00+09:00</updated><id>https://eatchangmyeong.github.io/2022/11/30/til-typescript-and-unenumerable-properties</id><content type="html" xml:base="https://eatchangmyeong.github.io/2022/11/30/til-typescript-and-unenumerable-properties.html"><![CDATA[<p><img src="/assets/post-images/x/1597830156647018498.png" alt="잇창명의 2022년 11월 30일 X 게시물: &quot;저는 타입스크립트를 쓰면서 enumerable이 false인 속성을 넣지 않겠습니다. 저는 타입스크립트를 쓰면서 enumerable이 false인 속성을 넣지 않겠습니다. 저는 타입스크립트를 쓰면서 enumerable이 false인 속성을 넣지 않겠습니다.&quot;" /></p>

<p>오늘은 TypeScript를 쓰다가 좀 커다란 시행착오를 했습니다.</p>

<h1 id="문제-상황">문제 상황</h1>

<p>프론트엔드와 백엔드 사이에 이런 데이터를 JSON으로 교환해야 한다고 칩시다. 실제로는 더 복잡한 정보를 교환했는데 설명의 편의상 더 단순한 예시를 만들었습니다.</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 여러 `DataExchange`가 공통으로 가지고 있는 무거운 데이터입니다.</span>
<span class="kd">type</span> <span class="nx">DataExchangeBase</span> <span class="o">=</span> <span class="p">{</span>
	<span class="na">foo</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span>
<span class="p">};</span>

<span class="c1">// 이 데이터를 교환합니다.</span>
<span class="c1">// `bar` 값만으로 `base`를 완전히 복구할 수 있기 때문에</span>
<span class="c1">// 이론상 `base`는 들고 있지 않아도 되지만,</span>
<span class="c1">// 그 복구하는 과정이 귀찮기 때문에 속성으로 넣어 두었습니다.</span>
<span class="kd">type</span> <span class="nx">DataExchange</span> <span class="o">=</span> <span class="p">{</span>
	<span class="na">bar</span><span class="p">:</span> <span class="kr">number</span><span class="p">,</span>
	<span class="na">base</span><span class="p">:</span> <span class="nx">DataExchangeBase</span><span class="p">,</span>
<span class="p">};</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">DataExchange</code>를 교환할 때는 <code class="language-plaintext highlighter-rouge">DataExchangeBase</code>도 같이 따라다니는데, 아무래도 중복되고 무거운 데이터다 보니까 JSON으로 만들 때 같이 보내고 싶지 않습니다. 저는 <a href="https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify#%EC%84%A4%EB%AA%85"><code class="language-plaintext highlighter-rouge">JSON.stringify</code></a> 문서를 보고 이렇게 생각했습니다.</p>

<blockquote>
  <p><code class="language-plaintext highlighter-rouge">base</code>를 <code class="language-plaintext highlighter-rouge">enumerable</code>하지 않게 만들면 <code class="language-plaintext highlighter-rouge">JSON.stringify</code>에서 빠지겠구나!</p>
</blockquote>

<h2 id="enumerable-false-해결책"><code class="language-plaintext highlighter-rouge">enumerable: false</code> 해결책</h2>

<p>이 방법을 바로 실행으로 옮겨봤는데...</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// JSON에서 갓 파싱해 온 `base`가 없는 값</span>
<span class="kd">type</span> <span class="nx">DataExchangeParsed</span> <span class="o">=</span> <span class="p">{</span>
	<span class="na">bar</span><span class="p">:</span> <span class="kr">number</span><span class="p">,</span>
<span class="p">};</span>

<span class="kd">function</span> <span class="nx">inject_base</span><span class="p">(</span><span class="nx">parsed</span><span class="p">:</span> <span class="nx">DataExchangeParsed</span><span class="p">):</span> <span class="nx">DataExchange</span> <span class="p">{</span>
	<span class="nb">Object</span><span class="p">.</span><span class="nx">defineProperty</span><span class="p">(</span><span class="nx">parsed</span><span class="p">,</span> <span class="dl">'</span><span class="s1">base</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span>
		<span class="na">value</span><span class="p">:</span> <span class="p">{</span> <span class="na">foo</span><span class="p">:</span> <span class="dl">'</span><span class="s1">test</span><span class="dl">'</span> <span class="p">},</span> <span class="c1">// 시연용이기 때문에 아무 값이나 넣었습니다.</span>
		<span class="na">enumerable</span><span class="p">:</span> <span class="kc">false</span><span class="p">,</span>
	<span class="p">});</span>
	<span class="k">return</span> <span class="nx">parsed</span> <span class="k">as</span> <span class="nx">DataExchange</span><span class="p">;</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">new_data_exchange</span><span class="p">(</span><span class="nx">bar</span><span class="p">:</span> <span class="kr">number</span><span class="p">):</span> <span class="nx">DataExchange</span> <span class="p">{</span>
	<span class="k">return</span> <span class="nx">inject_base</span><span class="p">({</span> <span class="nx">bar</span> <span class="p">});</span>
<span class="p">}</span>

<span class="kd">const</span> <span class="nx">data_exchange</span><span class="p">:</span> <span class="nx">DataExchange</span> <span class="o">=</span> <span class="nx">new_data_exchange</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="c1">// { bar: 1, base: { foo: "test" } }</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">data_exchange</span><span class="p">))</span>
<span class="c1">// {"bar":1}</span>

<span class="c1">// JSON에서 도로 `DataExchange`로 만들 때는 이렇게 합니다. 올바른 문자열만 들어온다고 가정합니다.</span>

<span class="kd">function</span> <span class="nx">hydrate</span><span class="p">(</span><span class="nx">json</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nx">DataExchange</span> <span class="p">{</span>
	<span class="kd">const</span> <span class="nx">parsed</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">json</span><span class="p">)</span> <span class="k">as</span> <span class="nx">DataExchangeParsed</span><span class="p">;</span>
	<span class="k">return</span> <span class="nx">inject_base</span><span class="p">(</span><span class="nx">parsed</span><span class="p">);</span>
<span class="p">}</span>

<span class="nx">hydrate</span><span class="p">(</span><span class="dl">'</span><span class="s1">{"bar":1}</span><span class="dl">'</span><span class="p">)</span>
<span class="c1">// { bar: 1, base: { foo: "test" } }</span>
</code></pre></div></div>

<p>...예상치 못한 곳에서 문제가 터졌습니다. <code class="language-plaintext highlighter-rouge">DataExchange</code>를 복사할 일이 자주 생기는데...</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 타입 검사를 통과합니다.</span>
<span class="kd">const</span> <span class="nx">cloned</span><span class="p">:</span> <span class="nx">DataExchange</span> <span class="o">=</span> <span class="p">{</span> <span class="p">...</span><span class="nx">data_exchange</span> <span class="p">};</span>

<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">cloned</span><span class="p">.</span><span class="nx">base</span><span class="p">.</span><span class="nx">foo</span><span class="p">);</span>
<span class="c1">// Uncaught TypeError: cloned.base is undefined</span>
</code></pre></div></div>

<p>알고 보니 <code class="language-plaintext highlighter-rouge">enumerable</code>이 <code class="language-plaintext highlighter-rouge">false</code>인 속성은 객체 스프레드 문법(<code class="language-plaintext highlighter-rouge">{ ...object }</code>)에서도 <a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Enumerability_and_ownership_of_properties#traversing_object_properties"><strong>무시합니다</strong></a>. 현재 TypeScript에는 <a href="https://github.com/microsoft/TypeScript/issues/9726">열거 불가능한 속성을 표시하는 방법이 없기 때문에</a> 이런 경우까지 처리해줄 수 없고, 링크한 이슈 페이지를 보면 2016년에 "엣지 케이스처럼 보인다"는 부정적인 입장 이후로 지금까지 딱히 진전된 것이 없어 보입니다.</p>

<p>이 문제를 해결하려면 <code class="language-plaintext highlighter-rouge">DataExchange</code>를 복사할 때마다 <code class="language-plaintext highlighter-rouge">Object.defineProperty</code>를 매번 해 줘야 되는데, 이미 복사를 온갖 곳에서 하고 있고 <code class="language-plaintext highlighter-rouge">base</code>를 주입하는 과정 자체가 너무 귀찮다 보니까 애초에 타입 검사가 잘 되는 다른 방법을 쓰기로 합니다.</p>

<h2 id="tojson-해결책"><code class="language-plaintext highlighter-rouge">toJSON</code> 해결책</h2>

<p><code class="language-plaintext highlighter-rouge">JSON.stringify</code>될 때의 모양을 마음대로 정할 수 있는 <code class="language-plaintext highlighter-rouge">toJSON</code> 메소드를 넣기로 한 것입니다. 타입 정의부터 다시 하자면...</p>

<div class="language-typescript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">type</span> <span class="nx">DataExchangeBase</span> <span class="o">=</span> <span class="p">{</span>
	<span class="na">foo</span><span class="p">:</span> <span class="kr">string</span><span class="p">,</span>
<span class="p">};</span>

<span class="kd">type</span> <span class="nx">DataExchangeParsed</span> <span class="o">=</span> <span class="p">{</span>
	<span class="na">bar</span><span class="p">:</span> <span class="kr">number</span><span class="p">,</span>
<span class="p">};</span>

<span class="c1">// `toJSON`이 새로 생겼습니다.</span>
<span class="kd">type</span> <span class="nx">DataExchange</span> <span class="o">=</span> <span class="p">{</span>
	<span class="na">bar</span><span class="p">:</span> <span class="kr">number</span><span class="p">,</span>
	<span class="na">base</span><span class="p">:</span> <span class="nx">DataExchangeBase</span><span class="p">,</span>
	<span class="na">toJSON</span><span class="p">:</span> <span class="p">(</span><span class="na">this</span><span class="p">:</span> <span class="nx">DataExchange</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">unknown</span><span class="p">,</span>
<span class="p">};</span>

<span class="c1">// 사실 이 메소드는 제네릭하게 타이핑을 하기가 어렵습니다.</span>
<span class="c1">// 제가 짠 실제 코드에는 `DataExchange` 말고도</span>
<span class="c1">// `base`를 빼고 직렬화해야 하는 타입이 많기 때문에 눈물을 머금고 `any`를 썼습니다.</span>
<span class="c1">// 컴파일 오류 없이 타입을 잘 매기신 분이 있다면 댓글로 제보 부탁드립니다. 감사합니다</span>
<span class="kd">function</span> <span class="nx">toJSON</span><span class="p">(</span><span class="k">this</span><span class="p">:</span> <span class="nx">DataExchange</span><span class="p">):</span> <span class="nx">unknown</span> <span class="p">{</span>
	<span class="k">return</span> <span class="p">{</span>
		<span class="p">...</span><span class="k">this</span><span class="p">,</span>
		<span class="na">base</span><span class="p">:</span> <span class="kc">undefined</span><span class="p">,</span>
	<span class="p">};</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">inject_base</span><span class="p">(</span><span class="nx">parsed</span><span class="p">:</span> <span class="nx">DataExchangeParsed</span><span class="p">):</span> <span class="nx">DataExchange</span> <span class="p">{</span>
	<span class="k">return</span> <span class="p">{</span>
		<span class="p">...</span><span class="nx">parsed</span><span class="p">,</span>
		<span class="na">base</span><span class="p">:</span> <span class="p">{</span> <span class="na">foo</span><span class="p">:</span> <span class="dl">'</span><span class="s1">test</span><span class="dl">'</span> <span class="p">},</span>
		<span class="nx">toJSON</span><span class="p">,</span> <span class="c1">// 이 메소드가 있으면 x를 그대로 직렬화하는 대신 x.toJSON()을 직렬화합니다.</span>
	<span class="p">};</span>
<span class="p">}</span>

<span class="kd">function</span> <span class="nx">new_data_exchange</span><span class="p">(</span><span class="nx">bar</span><span class="p">:</span> <span class="kr">number</span><span class="p">):</span> <span class="nx">DataExchange</span> <span class="p">{</span>
	<span class="k">return</span> <span class="nx">inject_base</span><span class="p">({</span> <span class="nx">bar</span> <span class="p">});</span>
<span class="p">}</span>

<span class="kd">const</span> <span class="nx">data_exchange</span><span class="p">:</span> <span class="nx">DataExchange</span> <span class="o">=</span> <span class="nx">new_data_exchange</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
<span class="c1">// { bar: 1, base: { foo: "test" }, toJSON: toJSON() }</span>
<span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">JSON</span><span class="p">.</span><span class="nx">stringify</span><span class="p">(</span><span class="nx">data_exchange</span><span class="p">))</span>
<span class="c1">// {"bar":1}</span>

<span class="kd">function</span> <span class="nx">hydrate</span><span class="p">(</span><span class="nx">json</span><span class="p">:</span> <span class="kr">string</span><span class="p">):</span> <span class="nx">DataExchange</span> <span class="p">{</span>
	<span class="kd">const</span> <span class="nx">parsed</span> <span class="o">=</span> <span class="nx">JSON</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="nx">json</span><span class="p">)</span> <span class="k">as</span> <span class="nx">DataExchangeParsed</span><span class="p">;</span>
	<span class="k">return</span> <span class="nx">inject_base</span><span class="p">(</span><span class="nx">parsed</span><span class="p">);</span>
<span class="p">}</span>

<span class="nx">hydrate</span><span class="p">(</span><span class="dl">'</span><span class="s1">{"bar":1}</span><span class="dl">'</span><span class="p">)</span>
<span class="c1">// { bar: 1, base: { foo: "test" }, toJSON: toJSON() }</span>

<span class="p">({</span> <span class="p">...</span><span class="nx">data_exchange</span> <span class="p">})</span>
<span class="c1">// { bar: 1, base: { foo: "test" }, toJSON: toJSON() }</span>
</code></pre></div></div>

<p>이번에는 복제를 해도 모든 속성이 잘 따라오고, <code class="language-plaintext highlighter-rouge">JSON.stringify</code>를 해도 <code class="language-plaintext highlighter-rouge">base</code>와 <code class="language-plaintext highlighter-rouge">toJSON</code>이 빠진 채로 직렬화됩니다. 게다가 <code class="language-plaintext highlighter-rouge">enumerable: false</code>가 없기 때문에 타입 검사도 잘 되네요!</p>

<p>방금 이 방법으로 리팩토링한 코드를 푸시하고 오는 길인데, 다행히 심각한 버그는 없는 것 같습니다. 다행이네요.</p>]]></content><author><name></name></author><category term="TypeScript" /><category term="TIL" /><summary type="html"><![CDATA[]]></summary></entry><entry><title type="html">백준 14939번 &quot;불 끄기&quot;, 그런데 이제 행렬을 곁들인</title><link href="https://eatchangmyeong.github.io/2022/07/11/boj-14939-lights-out-but-with-matrix.html" rel="alternate" type="text/html" title="백준 14939번 &quot;불 끄기&quot;, 그런데 이제 행렬을 곁들인" /><published>2022-07-11T00:00:00+09:00</published><updated>2022-07-11T00:00:00+09:00</updated><id>https://eatchangmyeong.github.io/2022/07/11/boj-14939-lights-out-but-with-matrix</id><content type="html" xml:base="https://eatchangmyeong.github.io/2022/07/11/boj-14939-lights-out-but-with-matrix.html"><![CDATA[<p>제가 백준 <a href="https://www.acmicpc.net/problem/14939">14939번 "불 끄기"</a>를 무슨 역행렬을 쓰는 이상한 방법으로는 풀 수 있는데 브루트포스로 푸는 방법은 최근까지 전혀 몰랐다고 하면 믿어지시나요? 저도 못 믿겠습니다.</p>

<p>아무튼 이 글은 <a href="/2021/04/29/how-i-made-honeyhouse.html">Honeyhouse</a>와 <a href="/2022/04/05/how-many-stardusts-for-25-stars.html">한별포스</a> 이후 벌써 3번째로 작성하는 선형대수학에 관한 글입니다. 분량은 그렇게까지 길어지지는 않을 것 같네요.</p>

<h1 id="문제-옮겨적기">문제 옮겨적기</h1>

<blockquote>
  <p><strong>문제</strong></p>

  <p>전구 100개가 10×10 정사각형 모양으로 늘어서 있다. 전구에 달린 스위치를 누르면 그 전구와 위, 아래, 왼쪽, 오른쪽에 있는 전구의 상태도 바뀐다. 전구 100개의 상태가 주어지면 모든 전구를 끄기 위해 최소한으로 눌러야 하는 스위치의 개수를 출력하라</p>

  <p><strong>입력</strong></p>

  <p>10줄에 10글자씩 입력이 주어진다. #은 꺼진 전구고 O(대문자 알파벳 o)는 켜진 전구다. #과 O외에는 입력으로 주어지지 않는다.</p>

  <p><strong>출력</strong></p>

  <p>모든 전구를 끄기 위해 최소한으로 눌러야 하는 스위치의 개수를 출력하라. 불가능하면 -1를 출력하라.</p>

  <p><strong>예제 입력 1</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>#O########
OOO#######
#O########
####OO####
###O##O###
####OO####
##########
########O#
#######OOO
########O#
</code></pre></div>  </div>

  <p><strong>예제 출력 1</strong></p>

  <div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>4
</code></pre></div>  </div>
</blockquote>

<h1 id="작은-버전의-문제">작은 버전의 문제</h1>

<p><em>때마침</em> 설명에 쓰기 좋은 게임이 있네요. <a href="https://www.notion.so/Proto-Jam-2f9fd180c0754becb3632fc267a55a72">프로토 게임잼</a><sup id="fnref:fn-proto-gamejam" role="doc-noteref"><a href="#fn:fn-proto-gamejam" class="footnote" rel="footnote">1</a></sup> 2022년 4월 참여작인 <a href="https://protogamejam.itch.io/dobas-candy-drop">도바's 캔디드롭</a>을 소개합니다.</p>

<p><img src="/assets/post-images/dobas-candy-drop.png" alt="도바's 캔디드롭 게임 화면" /></p>

<p>플레이어가 원형의 아레나 위를 돌아다니고, 가운데에 보스가 있습니다(오른쪽 위의 원 모양 그림이 위에서 본 미니맵입니다). 보스의 공격을 적당히 피하면서 아레나에 설치된 버튼 8개를 모두 눌러진(노란색) 상태로 바꾸어야 하는데, 버튼을 누르면 <strong>양옆 버튼의 상태가 같이 바뀝니다</strong>. 원본 문제와는 달리 양옆이 연결되지만 일단은 신경쓰지 말아 봅시다.</p>

<p>이 게임이 올라왔을 당시에 스피드런 이벤트가 같이 열리고 있었는데, 가끔씩 처음 보는 헷갈리는 버튼 조합이 나와서 시간을 다 잡아먹길래 머릿속에서 계산할 수 있는 공략법이 있는지 자연스럽게 생각해보게 되었습니다. 지금도 디스코드에 이 흔적이 남아 있습니다.</p>

<p><img src="/assets/post-images/dobas-candy-drop-walkthrough.png" alt="디스코드 유저 잇창명이 디스코드에 올린 공략 메모. 1번째 버튼만 눌려 있으면 2번째, 5번째, 8번째 버튼을 눌러야 함을 이모지로 나타내었다." /></p>

<h2 id="반응-행렬-만들기">반응 행렬 만들기</h2>

<p>버튼에 1번부터 8번까지 번호를 붙이고 각 버튼이 어떻게 상호작용하는지 반응 행렬(급조한 이름입니다)로 나타내 봅시다. 이 행렬의 원소 \(\mathbf{R}_{ij}\)는 \(i\)번째 버튼을 눌렀을 때 \(j\)번째 버튼이 반응하면 1, 그렇지 않으면 0입니다.</p>

\[\mathbf{R} =
\begin{pmatrix}
	1 &amp; 1 &amp; 0 &amp; 0 &amp; 0 &amp; 0 &amp; 0 &amp; 1 \\
	1 &amp; 1 &amp; 1 &amp; 0 &amp; 0 &amp; 0 &amp; 0 &amp; 0 \\
	0 &amp; 1 &amp; 1 &amp; 1 &amp; 0 &amp; 0 &amp; 0 &amp; 0 \\
	0 &amp; 0 &amp; 1 &amp; 1 &amp; 1 &amp; 0 &amp; 0 &amp; 0 \\
	0 &amp; 0 &amp; 0 &amp; 1 &amp; 1 &amp; 1 &amp; 0 &amp; 0 \\
	0 &amp; 0 &amp; 0 &amp; 0 &amp; 1 &amp; 1 &amp; 1 &amp; 0 \\
	0 &amp; 0 &amp; 0 &amp; 0 &amp; 0 &amp; 1 &amp; 1 &amp; 1 \\
	1 &amp; 0 &amp; 0 &amp; 0 &amp; 0 &amp; 0 &amp; 1 &amp; 1
\end{pmatrix}\]

<p>이 행렬의 오른쪽에 누르려는 버튼을 열벡터로 모아서 곱하면 버튼을 누른 결과를 나타내는 열벡터를 얻을 수 있습니다.</p>

\[\mathbf{R}
\begin{pmatrix}
	0 \\ 1 \\ 0 \\ 0 \\ 1 \\ 0 \\ 0 \\ 1
\end{pmatrix} =
\begin{pmatrix}
	0 \\ 1 \\ 1 \\ 1 \\ 1 \\ 1 \\ 1 \\ 1
\end{pmatrix}\]

<p>그런데 제가 원하는 건 <em>원하는 결과가 주어졌을 때</em> 올바른 버튼의 조합을 구하는 것입니다. 완전히 반대 방향이네요. 역행렬을 구해 봅시다!</p>

<p><img src="/assets/post-images/dobas-candy-drop-inverse-matrix.png" alt="Wolfram|Alpha로 행렬 R의 역행렬을 구한 결과. -1/3과 2/3이 반복되는 행렬이 계산되었다." /></p>

<p>어... 잠시 혼선이 있었습니다. 대신 직접 구한 결과는 이렇습니다.</p>

\[\mathbf{R}^{-1} =
\begin{pmatrix}
	1 &amp; 0 &amp; 1 &amp; 1 &amp; 0 &amp; 1 &amp; 1 &amp; 0 \\
	0 &amp; 1 &amp; 0 &amp; 1 &amp; 1 &amp; 0 &amp; 1 &amp; 1 \\
	1 &amp; 0 &amp; 1 &amp; 0 &amp; 1 &amp; 1 &amp; 0 &amp; 1 \\
	1 &amp; 1 &amp; 0 &amp; 1 &amp; 0 &amp; 1 &amp; 1 &amp; 0 \\
	0 &amp; 1 &amp; 1 &amp; 0 &amp; 1 &amp; 0 &amp; 1 &amp; 1 \\
	1 &amp; 0 &amp; 1 &amp; 1 &amp; 0 &amp; 1 &amp; 0 &amp; 1 \\
	1 &amp; 1 &amp; 0 &amp; 1 &amp; 1 &amp; 0 &amp; 1 &amp; 0 \\
	0 &amp; 1 &amp; 1 &amp; 0 &amp; 1 &amp; 1 &amp; 0 &amp; 1
\end{pmatrix}\]

<p>검산 겸 위에서 \(\mathbf{R}^{-1}\)에 2번, 5번, 8번 버튼을 누른 결과를 다시 곱해봅시다.</p>

\[\mathbf{R}^{-1}
\begin{pmatrix}
	0 \\ 1 \\ 1 \\ 1 \\ 1 \\ 1 \\ 1 \\ 1
\end{pmatrix} =
\begin{pmatrix}
	0 \\ 1 \\ 0 \\ 0 \\ 1 \\ 0 \\ 0 \\ 1
\end{pmatrix}\]

<p>눌러야 되는 버튼 조합을 복구했네요! 위의 \(\mathbf{R}^{-1}\)을 뜷어져라 쳐다보면, 누르고 싶은 버튼이 있으면 그 버튼을 포함해서 좌우 2칸, 3칸씩 떨어진 버튼(총 5개입니다)을 전부 누르면 된다는 공략을 얻을 수 있습니다. 머릿속으로 계산하기 어려운 공략이긴 하네요.</p>

<h2 id="실수-행렬과-0-1-행렬">실수 행렬과 0-1 행렬</h2>

<p>위에서 Wolfram|Alpha로 계산한 결과를 다시 보면 처음에 예상했던 0과 1의 조합이 아니라 \(-\frac{1}{3}\)과 \(\frac{2}{3}\)의 범벅이었습니다. 뭐가 문제였을까요?</p>

<p>사실 "<em>처음에 예상했던</em> 0과 1의 조합"에 제가 말하지 않고 넘어갔던 가정이 숨어 있습니다. 행렬 \(\mathbf{R}\)은 임의의 실수가 아니라 <strong>0과 1만으로 이루어진</strong> 0-1 행렬입니다. 게다가 우리는 누른 버튼을 다시 누르면 원래대로 돌아온다는, 즉 <strong>1+1=0이라는 가정</strong>을 추가로 하고 있었습니다. Wolfram|Alpha는 대신 \(\mathbf{R}\)이 일반적인 실수 행렬이라고 가정하고 풀었기 때문에 "잘못된"<sup id="fnref:fn-mod-two" role="doc-noteref"><a href="#fn:fn-mod-two" class="footnote" rel="footnote">2</a></sup> 결과가 나온 것입니다.</p>

<p>그러고 보니까 창명님, 혹시 아까 \(\mathbf{R}^{-1}\)은 어떻게 구하셨나요?</p>

<blockquote>
  <p>그냥 <a href="https://en.wikipedia.org/wiki/Invertible_matrix#Gaussian_elimination">가우스 소거법</a> 썼는데요?</p>
</blockquote>

<p>엥? 일반적인 실수 행렬도 아닌데 그래도 되나요?</p>

<h2 id="추상-벡터-공간">추상 벡터 공간</h2>

<p>그럴 줄 알고 수학자들이 이미 벡터와 행렬을 일반화해서 <a href="https://en.wikipedia.org/wiki/Vector_space#Definition_and_basic_properties"><em>벡터 공간</em></a>이라는 이름을 붙여 놓았습니다. 다음과 같은 몇 가지 공리만 만족시키면 "벡터 공간"이라고 하고, 선형대수학의 <em>모든</em> 정리와 도구를 그냥 가져와서 쓸 수 있습니다.</p>

<p>설명의 편의상 스칼라는 <em>기울임꼴 소문자</em>, 벡터는 <strong>굵은 소문자</strong>, 행렬은 <strong>굵은 대문자</strong>로 적겠습니다.</p>

<ul>
  <li>스칼라가 체를 이룬다.
    <ul>
      <li>덧셈의 결합법칙: \(a + (b + c) = (a + b) + c\)</li>
      <li>덧셈의 교환법칙: \(a + b = b + a\)</li>
      <li>덧셈의 항등원의 존재: \(a + 0 = a\)</li>
      <li>덧셈의 역원의 존재: \(a + (-a) = 0\)</li>
      <li>곱셈의 결합법칙: \(a \cdot (b \cdot c) = (a \cdot b) \cdot c\)</li>
      <li>곱셈의 교환법칙: \(a \cdot b = b \cdot a\)</li>
      <li>곱셈의 항등원의 존재: \(a \cdot 1 = a\)</li>
      <li>곱셈의 역원의 존재: \(a \cdot a^{-1} = 1\) (단, \(a \ne 0\))</li>
      <li>덧셈에 대한 곱셈의 분배법칙: \(a \cdot (b + c) = a \cdot b + a \cdot c\)</li>
      <li>이건 공리라기보다는 전제 조건에 가까운 느낌으로 들어가 있습니다.</li>
    </ul>
  </li>
  <li>벡터 공간의 8대 공리
    <ul>
      <li>벡터합의 결합법칙: \(\mathbf{u} + (\mathbf{v} + \mathbf{w}) = (\mathbf{u} + \mathbf{v}) + \mathbf{w}\)</li>
      <li>벡터합의 교환법칙: \(\mathbf{u} + \mathbf{v} = \mathbf{v} + \mathbf{u}\)</li>
      <li>벡터합의 항등원의 존재: \(\mathbf{v} + \mathbf{0} = \mathbf{v}\)</li>
      <li>벡터합의 역원의 존재: \(\mathbf{v} + (-\mathbf{v}) = \mathbf{0}\)</li>
      <li>곱셈과 스칼라곱의 호환성: \(a \cdot (b \cdot \mathbf{v}) = (a \cdot b) \cdot \mathbf{v}\)</li>
      <li>스칼라곱의 항등원의 존재: \(1 \cdot \mathbf{v} = \mathbf{v}\)
        <ul>
          <li>여기서의 \(1\)은 위에서 언급한 곱셈의 항등원과 같습니다.</li>
        </ul>
      </li>
      <li>벡터합에 대한 스칼라곱의 분배법칙: \(a \cdot (\mathbf{u} + \mathbf{v}) = a \cdot \mathbf{u} + a \cdot \mathbf{v}\)</li>
      <li>덧셈에 대한 스칼라곱의 분배법칙: \((a \cdot b) \cdot \mathbf{v} = a \cdot \mathbf{v} + b \cdot \mathbf{v}\)</li>
    </ul>
  </li>
</ul>

<p>뭔가 내용이 엄청나게 많아 보이는데 제가 전부 풀어서 써서 그렇지 공리 하나하나는 생각보다 어렵지 않습니다. 여기서 중요한 점은, 관례상 덧셈과 벡터합을 \(+\), 곱셈과 스칼라곱을 \(\cdot\), 덧셈의 항등원을 \(0\), 곱셈의 항등원을 \(1\)로 쓰긴 했지만 이것들이 <strong>진짜로 덧셈, 곱셈, 0, 1이 아니어도 된다</strong>는 것입니다. 애초에 이 문제에서는 1 더하기 1이 0입니다.</p>

<p>제가 이 이상 설명하려고 하면 더 이해하기 어려워질 것 같으니 나머지는 <a href="https://youtu.be/TgKwz5Ikpc8">3Blue1Brown님의 이 유튜브 동영상</a>으로 넘기겠습니다. 원래 이 글에서 0-1 벡터가 왜 벡터 공간을 이루는지도 전부 유도해보려고 했는데, 쓰기도 재미없고 읽기도 재미없을 것 같으니 연습문제로 남기겠습니다. 결론만 정리하자면 다음과 같습니다.</p>

<ul>
  <li>스칼라는 0과 1로만 이루어져 있음</li>
  <li>덧셈은 모듈로 2 덧셈
    <ul>
      <li>덧셈의 항등원은 0</li>
    </ul>
  </li>
  <li>곱셈은 일반 곱셈
    <ul>
      <li>곱셈의 항등원은 1</li>
    </ul>
  </li>
</ul>

<p>아니면 이렇게도 해석할 수 있고, 어느 관점을 취하든 상관은 없습니다.</p>

<ul>
  <li>스칼라는 참과 거짓으로만 이루어져 있음</li>
  <li>덧셈은 배타적 논리합 (XOR)
    <ul>
      <li>덧셈의 항등원은 거짓</li>
    </ul>
  </li>
  <li>곱셈은 논리곱 (AND)
    <ul>
      <li>곱셈의 항등원은 참</li>
    </ul>
  </li>
</ul>

<h1 id="실전-문제-풀기">실전 문제 풀기</h1>

<p>이제 문제를 푸는 방법도 알았고 그 방법이 올바르다는 것도 알았으니 실제로 문제를 풀어볼 시간입니다.</p>

<h2 id="반응-행렬-만들기-1">반응 행렬 만들기</h2>

<p><a href="#작은-버전의-문제">도바's 캔디드롭</a>에서는 버튼이 8개밖에 없었지만, 이 문제에서는 스위치가 무려 100개이고 반응 행렬도 100×100짜리가 됩니다. 이 정도는 컴퓨터로 비교적 빨리 풀 수 있으니 상관 없긴 합니다. 편의상 스위치 번호는 줄글을 읽듯이 붙입시다.</p>

<p>반응 행렬은 적당한 언어에서 100×100짜리 이차원 배열을 만들고 각 줄마다 자신과 상하좌우에 해당하는 원소를 찾아 1로 만들면 됩니다. 저는 C++로 짰습니다.</p>

<h2 id="역행렬-구하기">역행렬 구하기</h2>

<p>...그런데 여기서 잠깐, 반응 행렬이 가역이라고 확신할 수 있나요?</p>

<h2 id="rref-구하기">RREF 구하기</h2>

<p>아까 가우스 소거법을 썼다고 잠깐 언급했는데, 가우스 소거법으로 역행렬 \(\mathbf{R}^{-1}\)을 구하는 방법은 이렇습니다.</p>

<p>우선 \(\mathbf{R}\)과 \(\mathbf{I}\)로 첨가행렬을 만듭니다.</p>

\[\left( \begin{array}{c|c}
	\mathbf{R} &amp; \mathbf{I}
\end{array} \right)\]

<p>이 행렬의 <a href="https://ko.wikipedia.org/wiki/사다리꼴행렬">RREF</a>를 구해서 왼쪽 절반이 \(\mathbf{I}\)가 된다면 나머지 오른쪽이 \(\mathbf{R}\)의 역행렬입니다.</p>

<h3 id="이게-왜-성립하나요">이게 왜 성립하나요?</h3>

<p>RREF나 가우스 소거법이 정확히 뭐고 어떻게 하는 건지는 이 글에서는 생략하지만, 연립방정식의 모양을 바꾸면서 해는 바꾸지 않는 연산을 여러 번 해서 예쁘게 만드는 것이라고만 하겠습니다.</p>

<p>저번 <a href="/2022/04/05/how-many-stardusts-for-25-stars.html#작년에-왔던-선형대수학-죽지도-않고-또-왔네">한별포스</a> 글에서 "행렬로 연립 일차방정식도 풀 수 있다"고 했었는데, 보통 행렬로 연립방정식을 풀 때는 식을 이런 꼴로 세우고...</p>

\[\mathbf{A}\mathbf{v} = \mathbf{b}\]

<p>연립방정식의 관점에서는 이렇게 볼 수 있습니다.</p>

<ul>
  <li>\(\mathbf{A}_{11}\mathbf{v}_1 + \mathbf{A}_{12}\mathbf{v}_2 + \mathbf{A}_{13}\mathbf{v}_3 + \cdots = \mathbf{b}_1\)</li>
  <li>\(\mathbf{A}_{21}\mathbf{v}_1 + \mathbf{A}_{22}\mathbf{v}_2 + \mathbf{A}_{23}\mathbf{v}_3 + \cdots = \mathbf{b}_2\)</li>
  <li>\(\mathbf{A}_{31}\mathbf{v}_1 + \mathbf{A}_{32}\mathbf{v}_2 + \mathbf{A}_{33}\mathbf{v}_3 + \cdots = \mathbf{b}_3\)</li>
  <li>...</li>
</ul>

<p>또 이걸 풀 때는 아까처럼 \(\mathbf{A}\)와 \(\mathbf{b}\)로 첨가행렬을 만들어서 RREF를 구합니다.</p>

\[\left( \begin{array}{c|c}
	\mathbf{A} &amp; \mathbf{b}
\end{array} \right)\]

<p>그런데 첨가행렬을 열벡터 하나 대신 행렬을 통째로 넣어서 만들면 우변에도 문자가 여러 개 생기는 효과를 가집니다. \(\mathbf{u}\)는 모양을 맞추기 위해 새로 만든 열벡터입니다.</p>

\[\mathbf{A}\mathbf{v} = \mathbf{B}\mathbf{u}\]

\[\left( \begin{array}{c|c}
	\mathbf{A} &amp; \mathbf{B}
\end{array} \right)\]

<ul>
  <li>\(\mathbf{A}_{11}\mathbf{v}_1 + \mathbf{A}_{12}\mathbf{v}_2 + \mathbf{A}_{13}\mathbf{v}_3 + \cdots = \mathbf{B}_{11}\mathbf{u}_1 + \mathbf{B}_{12}\mathbf{u}_2 + \mathbf{B}_{13}\mathbf{u}_3 + \cdots\)</li>
  <li>\(\mathbf{A}_{21}\mathbf{v}_1 + \mathbf{A}_{22}\mathbf{v}_2 + \mathbf{A}_{23}\mathbf{v}_3 + \cdots = \mathbf{B}_{21}\mathbf{u}_1 + \mathbf{B}_{22}\mathbf{u}_2 + \mathbf{B}_{23}\mathbf{u}_3 + \cdots\)</li>
  <li>\(\mathbf{A}_{31}\mathbf{v}_1 + \mathbf{A}_{32}\mathbf{v}_2 + \mathbf{A}_{33}\mathbf{v}_3 + \cdots = \mathbf{B}_{31}\mathbf{u}_1 + \mathbf{B}_{32}\mathbf{u}_2 + \mathbf{B}_{33}\mathbf{u}_3 + \cdots\)</li>
  <li>...</li>
</ul>

<p>그런데 \(\mathbf{B}\)의 자리에 대신 \(\mathbf{I}\)를 넣으면...</p>

\[\mathbf{A}\mathbf{v} = \mathbf{I}\mathbf{u} = \mathbf{u}\]

\[\left( \begin{array}{c|c}
	\mathbf{A} &amp; \mathbf{I}
\end{array} \right)\]

<ul>
  <li>\(\mathbf{A}_{11}\mathbf{v}_1 + \mathbf{A}_{12}\mathbf{v}_2 + \mathbf{A}_{13}\mathbf{v}_3 + \cdots = \mathbf{u}_1\)</li>
  <li>\(\mathbf{A}_{21}\mathbf{v}_1 + \mathbf{A}_{22}\mathbf{v}_2 + \mathbf{A}_{23}\mathbf{v}_3 + \cdots = \mathbf{u}_2\)</li>
  <li>\(\mathbf{A}_{31}\mathbf{v}_1 + \mathbf{A}_{32}\mathbf{v}_2 + \mathbf{A}_{33}\mathbf{v}_3 + \cdots = \mathbf{u}_3\)</li>
  <li>...</li>
</ul>

<p>원점으로 돌아왔다고요? 그런데 잘 생각해보면, 우변이 상수 \(\mathbf{b}_i\) 대신 미지수 \(\mathbf{u}_i\)로 달라졌습니다. 다르게 말하면, 이 첨가행렬을 RREF하면 \(\mathbf{A}\)(좌변)를 고정하고 <strong>모든</strong> \(\mathbf{u}\)(우변)에 대해서 연립방정식을 푸는 것과 같습니다. 와!</p>

<h3 id="역행렬이-없다면">역행렬이 없다면?</h3>

<p>그런데 만약에 왼쪽 절반이 \(\mathbf{I}\)가 아니라면 어떻게 될까요? 이럴 때는 "역행렬이 존재하지 않는다"고 하고, 연립방정식 관점에서는 각 줄마다 무슨 모양인지 살펴봐야 하는데, 보통 이 두 가지 패턴을 조심하면 됩니다.</p>

<ul>
  <li>좌변에 항이 2개 이상 있음 (\(\mathbf{v}_1 + \mathbf{v}_2 = \mathbf{u}_1 + \mathbf{u}_3 + \cdots\))
    <ul>
      <li>우변의 여러 항을 좌변에 아무렇게나 나눠서 넣으면 되는데, 가장 첫 항을 잡아서 전부 몰아넣고 나머지는 0으로 놓는 게 제일 편합니다.</li>
    </ul>
  </li>
  <li>좌변이 0임 (\(0 = \mathbf{u}_2 + \mathbf{u}_5 + \cdots\))
    <ul>
      <li>이 식이 성립하지 않으면 연립방정식의 해가 <strong>존재하지 않습니다</strong>.</li>
    </ul>
  </li>
</ul>

<p>역으로 역행렬이 존재한다면 연립방정식의 해가 <em>반드시 하나</em> 존재한다고 말할 수 있습니다.</p>

<p>이 점을 염두에 두면서 위에서 구한 반응 행렬에 RREF를 하고 결과를 확인해 봅시다. 역행렬이 있는지 없는지를 여기서 밝히면 문제를 푸는 재미가 없으니 비밀로 하겠습니다. 저는 결과를 구해서 Base64로 인코딩한 뒤 제출하는 코드에 하드코딩으로 넣었습니다(🤣).</p>

<h2 id="정답-구하기">정답 구하기</h2>

<p>위에서 하드코딩한 행렬을 디코딩하고 입력으로 받은 전구 상태를 방정식 100개에 각각 넣어서 계산하고 나면 위에서 잠깐 보았듯이 세 가지 경우로 나눌 수 있습니다.</p>

<ul>
  <li>좌변에 항이 정확히 1개 있음 (\(\mathbf{v}_1 = 1\))
    <ul>
      <li>우변을 스위치 개수에 그대로 반영하면 됩니다.</li>
    </ul>
  </li>
  <li>좌변에 항이 2개 이상 있음 (\(\mathbf{v}_3 + \mathbf{v}_5 = 0\))
    <ul>
      <li>어차피 하나 빼고 나머지는 0으로 처리하면 되니 똑같이 반영하면 됩니다.</li>
    </ul>
  </li>
  <li>좌변이 0임
    <ul>
      <li>\(0 = 0\)이면 그냥 무시하면 됩니다.</li>
      <li>\(0 = 1\)이면 모든 전구를 끌 수 있는 방법이 없으므로 <code class="language-plaintext highlighter-rouge">-1</code>을 출력하고 종료합니다.</li>
    </ul>
  </li>
</ul>

<p>역행렬이 존재한다면 \(\mathbf{R}^{-1}\)만 하드코딩하고 첫 번째 경우만 고려하면 되니 풀기 훨씬 쉬워집니다.</p>

<p>첫 문단에서 얘기했듯이 이거는 제가 이상한 방법을 생각해서 어거지로 푼 것에 조금 더 가까우니 이 방법 대신 "제대로 된" 풀이법으로 푸는 것을 권장드립니다. 저도 글을 올리고 나서 제대로 풀어볼 예정입니다.</p>

<h1 id="글의-주제와-전혀-무관한-여담">글의 주제와 전혀 무관한 여담</h1>

<p>저는 스피드런 이벤트에서 개발자 제외 2등을 해서 상품을 받았습니다. <a href="https://youtu.be/WzPinxaVLzI">제 기록이 궁금하신가요?</a> (스포일러 주의)</p>

<div class="footnotes" role="doc-endnotes">
  <ol>
    <li id="fn:fn-proto-gamejam" role="doc-endnote">
      <p>제가 참여하고 있는, 1달에 한 번씩 게임잼을 여는 디스코드 그룹입니다. 글이 살짝 홍보에 가까워지는 것 같긴 한데 에라 모르겠네요. <a href="#fnref:fn-proto-gamejam" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
    <li id="fn:fn-mod-two" role="doc-endnote">
      <p>사실 완전히 잘못된 것은 아닌 게, 모든 원소에 \(\mathrm{mod} \; 2\) 연산을 하면 올바른 0-1 행렬인 \(\mathbf{R}^{-1}\)을 얻을 수 있습니다. 자세한 내용은 <a href="/2023/11/28/can-we-just-divide-by-1000000007.html">다른 글</a>에 정리해 두었습니다. <a href="#fnref:fn-mod-two" class="reversefootnote" role="doc-backlink">&#8617;</a></p>
    </li>
  </ol>
</div>]]></content><author><name></name></author><category term="문제해결" /><category term="알고리즘" /><category term="선형대수학" /><category term="추상대수학" /><summary type="html"><![CDATA[제가 백준 14939번 "불 끄기"를 무슨 역행렬을 쓰는 이상한 방법으로는 풀 수 있는데 브루트포스로 푸는 방법은 최근까지 전혀 몰랐다고 하면 믿어지시나요? 저도 못 믿겠습니다.]]></summary></entry></feed>