[Course] 01-1: 프로그래밍 언어 소개
카이스트 류석영 교수님의 강의를 듣고 정리한 정리본입니다.
✨ Introduction
programming language란 기본적으로는 syntax와 semantics로 구성되어 있다.
그러나 저 두 가지 외에도 더 넓은 의미에서는 standard library와 ecosystem 또한 포함된다.
standard library와 ecosystem은 programming language를 이루는 데 있어 필수는 아니지만, 사람이 입는 옷처럼 다양한 편의를 제공한다.
추가적으로, 프로그래밍 언어는 syntax와 standard libraries에 의해 각기 다르게 보일 수는 있으나, 그 내부적으로 본다면 semantic는 같은 기능을 공유하기 때문에 한 번 다룰 줄 만 알게 된다면 다른 언어 또한 쉽게 다룰 수 있게 된다.
이 강의에서는 Scala를 활용할 예정이다.
✨ Scala
스칼라는 multi-paradigm language로, functional과 object-oriented 스타일 모두 허용한다.
object-oriented 스타일은 Java처럼 객체지향적인 스타일을 의미하는데, 그렇다면 여기서 funcational 스타일이란 무엇일까?
🫧 Functional Programming
Wikipedia에 따르면, Functional Programming이란 다음과 같다.
It is a declarative programming paradigm in which function definitions are trees of expressions that map values to other values, rather than a sequence of imperative statements which update the running state of the program.
즉, 실행 상태를 계속해서 변경하는 명령문 대신 값을 다른 값으로 변환하는 표현식들을 조합하여 함수를 정의하는 것이라고 생각하면 된다.
imperative statements의 경우는 변수의 상태를 계속해서 업데이트하며 일련의 명령들을 나열한다면, 함수형 스타일에는 함수 자체를 값을 다른 값으로 대응시키는 관점으로 본다.
책에서는 Functional Programming을 pure functions로 나타내는데, 이는 함수가 side effect를 일으키지 않는다고 해석할 수 있다.
즉, 함수 외부에 의존하는 것이 아니라 - 외부 상태에 의존하지 않고 - 언제나 같은 input은 같은 output을 반환한다, 정도로 해석할 수 있다.
값을 변환하는 형태로 보는 것이 아닌, 함수를 거쳐 새로운 값을 반환하는 형태로 보는 것이 적절하다.
🫧 Functional Programming의 특징
- first-class functions (일급함수) 사용
- pattern matching 사용
특히, 소스 코드처럼 복잡하고 추상적인 구조의 입력을 받는 프로그램들은 함수형 언어로 작성되는 경우가 많다.
대표적인 예로 컴파일러가 있다. 소스코드 분석 시 트리같은 구조가 생기며, 트리, 재귀적 구조, AST 같은 구조를 처리할 때 함수형 프로그래밍이 대개 사용된다.
🫧 REPL
REPL은 코드를 바로 입력하고 실행하는 대화형 실행환경이다.
완성된 프로그램을 작성하는 곳보다는 짧은 코드를 입력하고 그것이 어떻게 동작하는지 확인하는 데 좋은 환경이라고 볼 수 있다.
한 줄 입력하면 바로 실행하고 결과를 확인해 주는 곳이다.
예를 들면 다음과 같다.
scala> 1+2
val res0: Int = 3
1+2를 치면 Int = 3처럼 결과가 바로 눈에 보일 수 있게 하는 것이다.
💡 calling methods
또, Scala에는 string 관련 메서드도 제공된다.
자바와 같은 메서드를 사용한다.
"hello".length
// 결과로 5가 나온다
"hello".substring(0, 4)
// 결과로 hell이 나온다.
println("hello world!")
// 결과로 hello world!가 나온다.
💡 Unit
스칼라에서 Unit이란 의미 있는 반환 값이 없을 때 사용하는 데이터 타입이다.
Java의 void, Python의 None, Javascript의 undefined와 비슷하다.
또한, Unit의 유일한 값은 ()로 나타낸다.
추가로, REPL에서 만약 결과가 unit이라면 아무것도 보여주지 않는다.
🫧 Variable
val [name]: type = [expression]
예를 들어 다음과 같이 쓸 수 있다.
val x: Int = 1
// 타입 생략의 경우 (컴파일러가 타입 추론)
val x = 1
💡 정적 타입 vs 동적 타입
이 둘은 타입을 언제 검사하는지에 따라 달라진다.
정적 타입 언어는 프로그램을 실행하기 전, 보통 컴파일 시점에 검사하며, 동적 타입 언어는 실행 중 값의 타입을 확인한다.
-> cf) 제네릭 타입은 동적/정적 타입과 관계 없다.
Scala, Java, C, C++, Rust 등은 정적 타입 언어이다.
Python, JavaScript, Ruby 등은 동적 타입 언어이다.
💡 상수와 변수
Scala에서 상수는 val로 표기하고, 변수는 var로 표기한다.
🫧 Function
def name: [type] = [expression]
예시는 다음과 같다.
def add(x: Int, y: Int): Int = x + y
add(3, 7)
// 결과는 다음과 같다.
val res9: Int = 10
// 여기서 res9는 REPL이 자동으로 붙여준 임시 결과 이름이다.
여기서, 함수 타입은 생략 가능하지만, 파라미터 타입은 생략이 불가능하다.
Scala는 따로 return문이 존재하지 않으며, 마지막 줄이 곧 결과이다.
def quadruple(x: Int): Int = {
val y = x + x
y + y // 결과
}
✨ Multiple expressions
Scala에서는 {}를 사용하여 여러 개의 표현식을 나태닐 수 있다.
중괄호 안의 여러 개의 표현식은 하나의 표현식으로 취급되며, 이런 표현식을 순차 표현식(sequenced expression)이라 지칭한다.
val a = {
val x = 1 + 1
x + x
}
// 결과
val a: Int = 4
✨ Conditionals
if ([expression]) [expression] else [expression]
✨ 질문
Q. Scala에서 println 함수는 왜 unit 타입으로 분류되는가?
- Java의 void처럼 의미 있는 반환 값이 없을 때 사용한다면, unit 타입이 아닌 제네릭 형태로 취급해야 하는 게 아닌가?
A. Scala에서 println이 Unit인 이유는 출력이라는 side effect는 수행하지만, 호출 결과로 의미있는 값을 리턴하지 않기 때문이다.
- 헷갈렸는데… java도 println의 리턴값이 void로 표기되어 있다.
Q. 동적 타입 언어와 인터프리터 간 상관관계가 존재하는가?
A. 꼭 그렇지는 않다.. 동적/정적 타입은 타입을 언제 검사하는지에 달렸고, 인터프리터나 컴파일러는 코드를 어떻게 실행하는지에 따라 달라지기 때문이다.
예를 들어 JS는 동적 타입 언어인데, V8 엔진의 경우 인터프리터와 JIT 컴파일도 동시에 수행한다는 예외가 존재한다.
Q. C/Java에서는 if “statement”이고, Scala에서는 if “expression”으로 표현하는 이유가 무엇인가?
A. 값을 가질 수 있냐, 없냐가 가장 큰 차이이다.
- C/Java의 경우 if는 어떤 코드를 실행할 지 결정하는 역할로, 하나의 값으로 평가되지는 않는다. 따라서 다음 코드는 불가능하다.
int x = if (a > 0) 1 else 2 // 오류
- 반면 Scala의 if는 expression으로 취급되어, 실행 결과가 값이 될 수 있다.
val x = {
if (a > 0) 1
else 2
} // 가능
// 따라서 다음 식도 가능하다.
println(
if (x > 0) "positive"
else "negative"
)
// 또는
val result = 10 + (if (x > 0) 1 else 2)
- 어떻게 보면 ?, : 를 사용하는 삼항 연산자와 비슷하다고 할 수 있다.
Q. 인터프리터가 AST를 만드는 경우는 왜 존재하는가?
A. 소스코드를 그대로 한 줄씩 실행하는 것보다 먼저 코드의 구조를 파악하는 것이 편하기 때문이다.
-
예를 들어 1+2*3이 있을 때, *가 우선순위가 높으므로 이러한 문법 구조를 파악해 AST를 만드는 것이 계산이 편리하기 때문이다.
- 현대의 인터프리터는 대부분 파싱 후 AST 같은 구조적 표현을 만든다.
- 그러나 꼭 AST 실행이 보장되는 것은 아니다.