LYnLab

소개블로그취미로그

[PL] Control: Expressions and Statements

프로그래밍 언어에서의 제어(control)란, 소스 코드를 읽고 무엇을, 언제, 어떤 순서로 실행할 것인가에 대한 의미(semantic)를 부여하는 것이다.

2015-12-06#프로그래밍

💡 이 글은 작성된지 1년 이상 지났습니다. 정보글의 경우 최신 내용이 아닐 수 있음에 유의해주세요.

Control: Expressions and Statements

개요

제어(control)란, 소스 코드를 읽고 무엇을, 언제, 어떤 순서로 실행할 것인가에 대한 의미(semantic)를 다루는 부분이다. 표현을 이르는 두 가지 단어로 expression과 statement의 두 가지가 있다. Expression은 단순히 순수한 값을 수학적으로 연산하여 side effect 없이 결과 값을 반환한다. Statement는 side effect를 가지며 결과값을 반환하지 않는다. 하지만 이렇게 애매한 정의처럼, 이 둘은 명확히 구분되지 않으며 많은 언어에서 expression이 side effect를 가지고 있다.

제어 구조

  • 내적(implicit) 제어 흐름: expression에서 연산의 순서를 결정하고, statement를 순차적으로 시행한다.
  • 외적(explicit) 제어 흐름: 단입단출(single-entry, single-exit)의 블록 구조를 단위로 한다.
  • 선제(preemptive) 제어 메커니즘: 발생한 예외를 처리한다.

Expressions

기초적인 expression은 문자(literal)와 구분자(identifier)로 구성된다. 이러한 기초 단계를 연산과 함수 단계로 재귀적으로 반복하여 더욱 복잡한 expression이 만들어진다. 연산자는 피연산자의 순에 따라 unary, binary, ternary로, 위치에 따라 infix, prefix, postfix로 구분한다.

Expression을 해석하는 방법으로는

  • Applicative order evaluation: 피연산자를 우선적으로 구함.
  • Normal order evaluation: 연산자를 우선적으로 구함.

의 두 가지가 있다. 대부분의 언어에서는 순서 해석 방법이 하나로 정해져있지는 않다.

Side Effects

다음의 C 코드를 살펴보자.

int x = 1;

int f(void) {
    x += 1;
    return x;
}

int p(int a, int b) {
    return a + b;
}

main() {
    printf("%d

", p(x,f())); // HERE! return 0; }

만약 HERE!로 주석 표시된 줄이 좌에서 우로 해석된다면, 즉 f()가 우선적으로 실행된다면 결과값은 3이 될 것이다. 반대로 우에서 좌로 실행된다면 결과값은 4가 된다. 이는 함수 f가 전역 변수 x의 값을 바꾼다는 side effect를 일으키기 때문이다.

x = (y = z)

위와 같은 연산자 또한 side effect를 일으키는 예시 중 하나이다.

Short-Circuit Evaluation (지름길 해석)

아래와 같은 예시처럼, 남은 뒤쪽의 expression을 해석하지 않아도 항상 동일한 결과값이 보장된다면 뒤의 연산을 생략하는 short-circuit 해석이 일어난다.

if (1 != 0 && y%x == 1)

Ada와 같은 일부 언어는 short-circuit을 지원하는 연산자와 지원하지 않는 연산자를 모두 가지고 있다.

Normal Order Evaluation

Side effect를 없애기 위해서, normal order evaluation은 프로그램의 의미를 변질시키지 않는다. 예를 들어 e1 ? e2 : e3 와 같은 삼항 연산자는,

int if_exp(int x, int y, int z){
    if (x) return y;
    else return z;
}

와 동일한 의미를 가지며, y와 z는 해당하는 경우에만 실행된다. 반대로, side effects의 존재는 normal order evaluation이 실질적으로 프로그램의 의미를 변질시킨다.

int get_int() {
    int x;
    scanf("%d", &x);
    return x;
}

위와 같은 함수가 있을 때, normal oreder evaluation이 적용된다면

square(get_int());

의 코드를

get_int() * get_int()

로 확장하여 두 개의 입력을 받게 될 것이다.

Conditional Statements and Guards

Guareded If

많은 언어들이 흔히 if문으로 알려진 조건문을 사용한다.

if B1 -> S1
|  B2 -> S2
       ...
|  Bn -> Sn
fi

각각의 Bi는 가드(guard)로 부르는 Boolean 표현문이며, Si는 statement이다. 하나 이상의 Bi가 참일 경우, 단 하나의 Si가 실행된다.

If-Statement

If문에 대한 자세한 설명은 생략한다.

여러 개의 if문과 else 문이 존재할 때, 이 else문이 어느 if문에 속하는 것인지 모호해지는 경우가 있다. 이러한 문제를 Dangling-Else Problem이라고 하며,

  • 가장 가까운 if문과 매칭 (C, Pascal)
  • 각괄호(bracket)를 통해 묶음 (Ada)

의 두 가지 방식으로 해결한다.

Case and Switch-Statements

Switch-Case문에 대한 자세한 설명은 생략한다. 각각의 언어별로 다음과 같은 특징을 가진다.

  • C에서의 case 값은 컴파일 단계에서 상수로 결정되어야 한다.
  • Ada에서는 case 값들을 리스트나 subrange로 묶는 것이 가능하다.
  • ML에서의 각각의 case 구조는 반환 값을 가져야한다. 또한 case가 패턴 형태이며, 와일드 카드를 통한 패턴을 이용한다.

Loops and Variations on WHILE

Guarded Do

루프에 대한 자세한 설명은 생략한다.

do B1 -> S1
|  B2 -> S2
       ...
|  Bn -> Sn
od

Guarded if와 비슷한 형태를 띄는 이 구문은 guarded do이다. 이는 모든 Bi가 거짓일 때까지 반복되며, 참인 조건의 Bi에 대한 Si가 실행된다.

While Loop

하나의 가드를 가지는 guarded do로 볼 수 있다.

For Loop

For문은 counter controlled repetition을 실행한다. 인덱스 변수에 다음과 같은 일부 제약이 주어진다.

  • i는 loop 내부에서 바뀔 수 없다.
  • i는 loop가 종료되면 메모리상에서 지워진다.
  • i는 제한적인 타입을 가지며, 다른 방식으로는 선언될 수 없다.

또한 다음과 같은 이슈도 존재한다.

  • 루프의 경계는 한 번만 계산되는가? 만약 그렇다면, 경계값은 루프가 실행된 이후 바뀌지 않게 된다.
  • 루프의 하한이 상한보다 클 경우 루프가 실행되는가?
  • exit나 break가 실행된 이후 제어 변수(control variable)은 undefined 되는가?
  • 루프 구조를 실행하기 위하여 번역기는 어떤 확인을 취해야하는가?

한편 OOP 언어에서는 iterator 객체를 사용한다.

The GOTO Controversy and Loop Exits

GOTO는 초기 프로그래밍 언어에서 많이 사용되었으나, 스파게티 코드를 만드는 주범이다.

Exception Handling

지금까지 배운 제어는 모두 외적인(explicit) 것이다. 프로그램을 실행하는 중에 발생하는 오류를 처리하기 위해서 예외 처리(exception handling) 절차가 존재한다.

예외가 발생하면, 이는 raised 혹은 thrown 된다. 예외 핸들러(exception handler)는 예외가 raised될 경우에도 프로그램이 계속해서 동작할 수 있도록 하는 역할을 하는 procedure이며, 흔히 처리(handle) 또는 catch 한다고 이야기한다.

예외 처리는

  • 비동기적 예외: 언제든지 발생 가능한 경우 (하드웨어적 문제)
  • 동기적 예외: 프로그램에 대한 응답 (파일 열기 실패, 0으로 나누기)

의 두 가지로 구분된다.

대부분의 언어들은 자체적으로 예외 처리 메커니즘을 제공한다. 만약 하드웨어나 OS에서 에러를 처리하게 된다면 프로그램은 대부분 종료되거나 충돌을 일으킬 것이다.

Exceptions

예외는 함수형 언어에서는 으로, 구조적 언어나 OOP 언어에서는 변수 또는 객체로 취급된다. 대부분의 경우 추가적인 정보로 에러 메시지와 데이터의 요약(summary)을 포함한다.

Exception Handlers

C++에서는 try-catch 블록을 사용한다.

try {
    // ...
}
catch (Trouble t) {
    // ...
}
catch (...) {
    // 점 3개를 이용하여 catch 되지 않은 예외를 처리
}

Ada는 switch-case의 형태로 exception을, ML은 handle을 사용한다.

Control

내장 예외의 경우 자동적으로 런타임 환경에 raised 되거나 프로그램에 의해 수동적으로 raised 될 수 있다. 유저 정의 예외는 당연히 program에 의해 수동적으로만 raised 될 수 있다. C++에서는 이러한 제어를 위해 throw 예약어를 사용한다. Ada나 ML은 raise를 사용한다.

if (/* wrong */) {
    Trouble t;
    // Write informations about the exception
    throw t;
}

예외가 raised 되면 현재의 연산을 멈춘 뒤, 런타임 시스템이 핸들러를 찾기 시작한다. Ada나 C++의 경우 현재 블록을 탐색한 뒤, 감싼(enclosing) 블록, 그리고 그 바깥의 블록의 순서대로 예외를 전파(propagate)시킨다. 가장 마지막 블록에 도달해도 핸들러를 찾을 수 없다면 예외는 caller로 raised 된다. 이러한 일련의 과정이 메인 프로그램이 종료될 때까지 반복되어, 메인 프로그램에서도 핸들러를 찾지 못하면 기본 핸들러를 수행시킨다. 이러한 과정을 call unwinding이라고 한다.

핸들러가 예외를 잘 처리한 후,

  • Resumption model의 경우 예외가 처음 raised된 부분부터 다시 수행
  • Termination model의 경우 예외 처리가 이루어진 블록부터 이어서 수행

의 두 가지 방식으로 프로그램의 실행이 계속된다.

관련된 글

Rails와 GitHub Actions에 커버리지 레포트를 달아보자

이 블로그의 CMS이기도 한 Shiori를 대폭 리팩토링하면서 테스트가 얼마나 잘 작성되어있는지 궁금해졌습니다.

Rails Global ID로 전역 객체 식별하기

Global ID는 Rails의 모든 객체를 식별할 수 있는 URI(Uniform Resource Identifier)입니다.

Ruby on WebAssembly: 살짝 맛보기

Ruby 3.2에 추가된 WebAssembly 지원을 간단하게 테스트해봅시다.

작성한 댓글은 giscus를 통해 GitHub Discussion에 저장됩니다.

크리에이티브 커먼즈 라이선스크리에이티브 커먼즈 저작자표시크리에이티브 커먼즈 동일조건변경허락

본 사이트의 저작물은 별도의 언급이 없는 한 크리에이티브 커먼즈 저작자표시-동일조건변경허락 4.0 국제 라이선스에 따라 이용할 수 있습니다.

© 2011 - 2024 Hoerin Doh, All rights reserved.

LYnLab 로고About MeGitHubTwitterInstagram