Post

[Programming Languages #1] 프로그래밍 언어는 무엇을 배우는 학문인가?

Programming Languages 시리즈 1편 – 프로그래밍 언어를 정의하는 방법: Inductive Definition, Syntax, Semantics, Inference Rules

[Programming Languages #1] 프로그래밍 언어는 무엇을 배우는 학문인가?

프로그래밍 언어를 배운다는 것은 무엇인가

“프로그래밍 언어를 공부한다”고 하면 보통 이렇게 생각한다.

  • 새로운 문법을 익히고
  • 라이브러리를 배우고
  • 코드를 더 잘 짜는 것

하지만 이 시리즈에서 말하는 Programming Languages (PL)
조금 다른 질문에서 출발한다.

“프로그래밍 언어는 어떻게 만들어지고, 어떻게 동작하는가?”


우리가 보통 배우는 것 vs PL에서 배우는 것

일반적인 프로그래밍 학습은 이런 흐름이다.

  • Python, C++, Java 같은 언어를 배우고
  • 기능을 익히고
  • 실제 문제를 푼다

하지만 PL에서는 이렇게 묻는다.

  • 이 언어의 문법은 어떻게 정의되는가?
  • 이 프로그램의 의미는 무엇인가?
  • 이 코드는 왜 이렇게 실행되는가?

즉,

“사용하는 것”이 아니라 “이해하고 정의하는 것”이 목표다


핵심 관점: 프로그램을 ‘수학적 대상’으로 본다

PL에서 가장 중요한 전환은 이것이다.

프로그램을 코드가 아니라 “형식적인 객체”로 본다

예를 들어, 다음과 같은 프로그램이 있다고 하자.

1
2
3
let rec fact n =
  if n = 0 then 1
  else fact (n - 1) * n

이 코드를 단순히 “팩토리얼 함수”로 보는 것이 아니라,
PL에서는 이렇게 본다.

  • 입력 → 프로그램 → 결과
\[\text{Program} \rightarrow \text{Result}\]

즉, 프로그램은 하나의 변환 함수처럼 취급된다.


그래서 무엇을 하게 되는가

이 관점에서 보면 자연스럽게 두 가지 중요한 작업이 등장한다.

1) Interpreter

프로그램을 실행하는 규칙을 정의한다.

\[\text{Program} \rightarrow \text{Interpreter} \rightarrow \text{Result}\]

즉,

“이 프로그램이 실제로 어떻게 계산되는가?”

를 명확하게 설명하는 것이다.


2) Type Checker

프로그램이 안전한지 검사한다.

\[\text{Program} \rightarrow \text{Type Checker} \rightarrow \text{Safe / Unsafe}\]

즉,

“이 프로그램이 실행해도 문제가 없는가?”

를 판단한다.


왜 이게 중요한가

이 접근이 중요한 이유는 명확하다.

우리는 단순히 코드를 작성하는 것을 넘어서
다음과 같은 문제를 다루고 싶기 때문이다.

  • 프로그램이 항상 올바르게 동작하는가
  • 특정 오류가 절대 발생하지 않는가
  • 코드의 의미를 정확하게 정의할 수 있는가

이런 질문은 직관이나 경험으로는 답할 수 없다.
그래서 필요해지는 것이 바로 형식적 사고(Formal reasoning) 다.


한 줄 정리

\[\text{Programming Languages} = \text{프로그램을 정의하고 이해하는 방법}\]

Inductive Definition — 프로그램을 “정의하는” 방법

그렇다면,

“프로그램을 정의한다는 건 정확히 무슨 의미일까?”

이 질문에 답하기 위해 등장하는 핵심 개념이 바로
Inductive Definition (귀납적 정의) 다.


왜 이런 게 필요한가

프로그래밍 언어를 다루다 보면 이런 대상들을 계속 만나게 된다.

  • 프로그램 (expression)
  • 리스트, 트리 같은 자료구조
  • 문법 (syntax)

이들은 모두 크기가 무한할 수 있다

예를 들어 아래와 같은 프로그램을 생각해보자.

1
2
3
4
5
x
x + 1
(x + 1) * (x + 2)
(x + (x + 1)) * ((x + 2) + 3)
...

“이 무한한 집합을 어떻게 정의할까?”


핵심 아이디어: 자기 자신으로 정의한다

Inductive Definition의 핵심은 한 문장이다.

“집합을 자기 자신을 이용해서 정의한다”

즉,

  • 작은 것부터 시작하고
  • 규칙을 이용해 점점 큰 것을 만든다

가장 간단한 예시

자연수 집합을 생각해보자.

다음처럼 정의할 수 있다.

  1. \(0\)은 자연수다
  2. \(n\)이 자연수면 \(n+1\)도 자연수다

이 두 줄만으로 우리는 무한한 집합을 정의했다.

\[\{0, 1, 2, 3, \dots\}\]

프로그래밍 언어에서의 예시

이제 PL 관점에서 더 중요한 예시를 보자.

리스트 정의

리스트는 다음 두 규칙으로 정의할 수 있다.

  1. 빈 리스트 \([]\) 는 리스트다
  2. 어떤 값 \(x\) 와 리스트 \(xs\) 가 있으면,
    \(x :: xs\) 도 리스트다

이 두 규칙으로 모든 리스트가 만들어진다.


여기서 \(x :: xs\) 는 조금 낯설 수 있는데,
이건 다음 의미를 가진다.

“값 \(x\) 를 리스트 \(xs\) 앞에 붙인다”

즉,

  • \(x\): 하나의 값
  • \(xs\): 리스트 (여러 개의 값)

(참고: 이 표기법은 함수형 언어(예: OCaml, Haskell)에서 자주 사용된다)


예를 들어:

1
2
3
1 :: []        = [1]
2 :: [1]       = [2, 1]
3 :: [2, 1]    = [3, 2, 1]

이렇게 “앞에 붙이는 연산”을 반복해서 리스트를 만든다.


따라서

\[[1,2,3] = 1 :: (2 :: (3 :: []))\]

처럼 모든 리스트를 이 규칙으로 표현할 수 있다.


트리 정의

트리도 똑같다.

  1. 빈 트리는 트리다
  2. 두 개의 트리를 자식으로 가지면 트리다

즉, 복잡한 구조도
간단한 규칙 몇 개로 정의할 수 있다.


중요한 감각

여기서 꼭 잡아야 할 감각이 있다.

“만드는 규칙 = 정의”

즉 Inductive Definition은 단순한 설명이 아니라
구성 방법 자체를 정의하는 것이다.


세 가지 방식

귀납적 정의는 세 가지 방식으로 설명할 수 있다.

1) Top-down

  • “이 값이 집합에 속하려면?”을 기준으로 정의

2) Bottom-up

  • 작은 것부터 생성해서 집합을 구성

3) Inference Rules (가장 중요)

  • 규칙 형태로 정의

PL에서는 대부분 이 세 번째 방식을 사용한다.

이 개념이 중요한 이유는 단순하다.

프로그래밍 언어의 모든 것이 이 방식으로 정의되기 때문이다

  • 문법 (syntax)
  • 의미 (semantics)
  • 타입 시스템 (type system)

전부 다 귀납적으로 정의된다.

이 방식이 이후 모든 내용의 핵심이 되기 때문에,
아래 섹션에서 조금 있다가 더 자세히 살펴보겠다.


SyGuS와 연결되는 포인트

여기서 중요한 연결이 하나 나온다.

우리가 앞에서 본 Grammar를 떠올려보자.

\[S \rightarrow x \mid S + S \mid 1\]

이건 사실 무엇인가?

Inductive Definition이다

  • base case: \(x, 1\)
  • recursive case: \(S + S\)

즉,

Grammar = 프로그램의 귀납적 정의


한 줄 정리

\[\text{Inductive Definition} = \text{유한한 규칙으로 무한한 구조를 정의하는 방법}\]

Syntax vs Semantics — 프로그램의 “형태”와 “의미”

Inductive Definition을 통해 우리는
프로그램을 어떻게 정의하는지 알게 되었다.

이제 다음 질문이 자연스럽게 나온다.

“그렇게 정의된 프로그램은 무엇을 의미하는가?”

이 질문에 답하기 위해 등장하는 두 개념이 바로

  • Syntax (문법)
  • Semantics (의미)

이다.


Syntax — 프로그램의 “모양”

Syntax는 프로그램이 어떻게 생겼는지를 다룬다.

즉,

“이게 문법적으로 올바른 프로그램인가?”

를 판단하는 기준이다.


예시

다음 두 식을 보자.

1
x + 1
1
+ x 1

첫 번째 식은 우리가 사용하는 일반적인 문법에서는 올바르다.
두 번째 식은 보통의 언어에서는 문법 오류다.

즉,

  • 첫 번째 식 → valid syntax
  • 두 번째 식 → invalid syntax

Syntax는 어떻게 정의되는가

Syntax는 바로 우리가 앞에서 본 방식으로 정의된다.

\[S \rightarrow x \mid S + S \mid 1\]

이건 단순한 규칙이 아니라,

“어떤 문자열이 프로그램인지”를 정의하는 것

이다.

즉,

Syntax = 프로그램의 집합을 정의하는 규칙


Semantics — 프로그램의 “의미”

이제 더 중요한 질문이다.

1
x + 1

이건 문법적으로 맞다.
그럼 이제 질문은 바뀐다.

“그래서 이건 무엇을 의미하는가?”

이걸 정의하는 것이 바로 Semantics다.


가장 직관적인 의미: 계산 결과

예를 들어 다음 식을 보자.

\[x = 3\]

그럼

\[x + 1 = 4\]

이건 프로그램의 가장 기본적인 의미다.

즉,

Semantics = 프로그램이 무엇으로 계산되는가


더 일반적으로 보면

Semantics는 다음과 같이 표현할 수 있다.

\[\text{Program} \rightarrow \text{Meaning}\]

또는 더 구체적으로는:

\[\text{Program} \rightarrow \text{Value}\]

즉, 프로그램은 어떤 결과로 “해석”된다.


Syntax vs Semantics 한 번에 보기

이 둘의 차이는 매우 중요하다.

구분의미
Syntax프로그램이 어떻게 생겼는가
Semantics프로그램이 무엇을 의미하는가

직관적으로 보면

  • Syntax → 문장이 맞는 문장인가
  • Semantics → 그 문장이 무슨 뜻인가

왜 이 구분이 중요한가

이 구분이 중요한 이유는 단순하다.

문법이 맞다고 해서 의미가 항상 올바른 것은 아니다

예를 들어:

1
1 / 0

이건 Syntax적으로는 완전히 올바르다.
하지만 의미적으로는 문제가 있다.


또 다른 예:

1
x + y

Syntax는 문제 없다.
하지만 \(x, y\) 값이 없다면 의미를 계산할 수 없다.


즉,

  • Syntax → 구조적 올바름
  • Semantics → 실제 의미

이 둘은 완전히 다른 층위의 문제다.


Programming Languages에서의 핵심

PL에서는 이 둘을 명확하게 분리해서 다룬다.

  • Syntax는 Grammar로 정의하고
  • Semantics는 수학적으로 정의한다

이렇게 해야만

  • 프로그램을 정확하게 분석하고
  • 자동으로 검증하고
  • 나중에 합성까지 가능해진다

한 줄 정리

\[\text{Syntax} = \text{형태}, \quad \text{Semantics} = \text{의미}\]

Inference Rules — 규칙으로 정의하는 방법

Inductive Definition에서 우리는
“작은 규칙으로 큰 구조를 만든다”는 아이디어를 봤다.

이걸 더 정확하게 표현하는 방식이 바로 Inference Rule이다.


Inference rule은 다음 형태를 가진다.

\[\frac{A}{B}\]

의미는 단순하다.

A가 참이면 B도 참이다

가정이 없는 경우는 특별하다.

\[\frac{}{B}\]

이건 “항상 참인 기본 사실 (axiom)”이다.


예를 들어 자연수 집합은 다음 두 규칙으로 정의된다.

\[\frac{}{0 \in S} \qquad \frac{n \in S}{n+1 \in S}\]

이 규칙의 의미는 명확하다.

  • 0은 기본적으로 포함된다
  • 포함된 값에서 계속 확장된다

중요한 점은 이것이다.

이 규칙으로 만들어낼 수 있는 것만 집합에 속한다


이제 이 규칙을 실제로 적용하는 과정을 보자.

이 과정을 Derivation (유도) 라고 한다.

예를 들어 3이 자연수임을 보이려면:

\[0 \in S \Rightarrow 1 \in S \Rightarrow 2 \in S \Rightarrow 3 \in S\]

즉,

만들 수 있으면 포함된다


이 과정을 구조로 보면 Derivation Tree가 된다.

1
2
3
4
5
6
7
0 ∈ S
↓
1 ∈ S
↓
2 ∈ S
↓
3 ∈ S
  • 시작: axiom
  • 끝: 우리가 보이려는 결과

즉 이 트리는

“왜 이 값이 포함되는지”를 설명하는 증명 구조


여기서 중요한 연결이 하나 나온다.

이 규칙들은 단순한 수학적 장난이 아니라,

프로그래밍 언어의 의미를 정의하는 데 그대로 사용된다

나중에는 다음과 같은 형태가 등장한다.

\[\rho \vdash e \Rightarrow v\]

이건 이렇게 읽는다.

  • 환경 \(\rho\)에서
  • 프로그램 \(e\)는
  • 값 \(v\)로 평가된다

즉,

Inference Rule = 프로그램의 실행 규칙


한 줄 정리

\[\text{Inference Rules} = \text{규칙으로 정의하고, 유도로 의미를 결정하는 방법}\]

마무리

이번 글에서는 프로그래밍 언어를 바라보는 가장 기본적인 관점을 정리했다.

핵심은 하나다.

프로그램은 단순한 코드가 아니라, 형식적으로 정의되는 대상이다

이 관점에서 우리는 다음을 차례로 보았다.

  • Inductive Definition
    → 유한한 규칙으로 무한한 구조를 정의하는 방법

  • Syntax vs Semantics
    → 프로그램의 형태와 의미를 구분하는 방법

  • Inference Rules
    → 규칙을 통해 프로그램과 의미를 형식적으로 정의하는 방법

이 세 가지는 이후 등장할 모든 개념의 기반이 된다.


조금 더 큰 그림에서 보면, 지금까지 한 일은 이것이다.

\[\text{“프로그램을 어떻게 정의할 것인가”}\]

하지만 아직 중요한 질문이 남아 있다.

“그렇게 정의된 프로그램의 성질은 어떻게 증명할까?”

또는 더 직접적으로 말하면,

“이 프로그램이 항상 올바르게 동작한다는 것을 어떻게 보장할까?”


다음 글에서는 이 질문에 답하기 위해
Structural Induction (구조적 귀납법) 을 살펴본다.

이 개념은 단순한 증명 기법이 아니라,

프로그래밍 언어 이론 전체를 지탱하는 핵심 도구

이기 때문에 반드시 제대로 이해해야 한다.


한 줄 요약

\[\text{Programming Languages #1} = \text{프로그램을 정의하는 방법}\]
This post is licensed under CC BY 4.0 by the author.