프로그래밍 언어 탐방 - Nim
(본 글은 2020년 9월에 Nim 1.2.6 버전 기준으로 작성했습니다.)
Nim은 System Programming Language를 표방하고 있습니다. System Programming Language의 대표적인 언어는 C이므로, Nim은 "더 나은 C"가 되는 것이 목표인 것으로 보입니다.
C에서는 메모리를 수동으로 관리해줘야합니다. malloc, calloc, realloc 함수로 메모리를 동적할당했으면, free 함수로 메모리를 해제해줘야합니다.
그런데 성능상의 이유로 참조 타입말고 값 타입을 사용하고 싶은 경우가 있는데, 이럴 때는 object 키워드만 사용해서 타입을 정의하면 됩니다.
재미있는 점은 둘을 섞어서 사용할 수 있다는 것입니다.
값 타입(EntityObj)의 변수에 addr 함수를 사용하면 포인터(pointer)를 얻을 수 있는 반면에, 참조(Entity)를 얻을 수는 없습니다. 따라서 값 타입(EntityObj)용 함수와 참조 타입(Entity)용 함수를 따로 만들어줘야합니다. 값 타입과 참조 타입을 섞어서 사용할 수 있는 것처럼 보이지만 실질적으로는 둘은 별개입니다. 따라서 문법적인 측면에서 C#에 비해 큰 장점은 없습니다.
2. 포인터
C에서는 포인터를 잘 사용해야합니다. 함수 호출시 인자를 전달하는 방식이 Call by value이고 배열도 포인터라서 포인터를 쓸 수 밖에 없습니다. Nim에서는 Call by reference 방식도 지원하고(인자의 타입 앞에 var를 붙이면 됩니다.) 배열은 포인터와 다르므로, 포인터를 사용할 일이 별로 없습니다. 그런데 이건 C#도 마찬가지입니다.
3. 헤더 파일
C에서는 선언과 정의의 순서가 중요합니다. S라는 구조체를 선언하기 전에는 S를 사용할 수 없고, 일반적으로 f라는 함수를 선언하기 전에는 f를 사용할 수 없습니다. 이는 Nim에서도 마찬가지입니다.
C에서는 헤더 파일을 관리해줘야합니다. 다른 파일에서 include할만한 부분(타입 정의, 함수 선언 등)은 헤더 파일(*.h)에 넣고 함수의 정의는 소스 파일(*.c)에서 작성합니다.
반면에 Nim에서는 헤더 파일을 별도로 둘 필요가 없고 import를 하면 됩니다.
Java에서는 패키지 단위로 import를 하는데, Nim에서는 파일 단위로 import를 합니다.
이런 문제를 막으려면, C나 C++와 유사하게 타입 정의와 구현부(변수 및 함수 정의)으로 나누거나, 타입과 전역 변수를 하나의 파일에 몰아넣으면 되기는 하지만, 어떤 방식을 취하든 불편함은 감수해야합니다.
4. 객체지향 프로그래밍
C는 절차지향 언어입니다. 객체지향 프로그래밍이 가능하기는 하지만(예를 들어, GObject), 불편합니다. Nim도 절차지향 언어입니다. 객체지향적인 요소는 거의 없습니다. 생성자와 소멸자가 없고, 상속은 가능하지만 필드만 상속받고, 동적 디스패치(dynamic dispatch)가 없습니다. 객체지향 프로그래밍을 하려면 다른 언어를 찾아보는 것이 좋습니다.
5. 함수형 프로그래밍
Nim은 C보다 함수형 프로그래밍을 더 잘 지원합니다. 함수 내에서 함수를 정의할 수 있고, 클로저(Closure)와 익명 함수를 사용할 수 있습니다. 이 부분에서는 C#과 큰 차이가 없습니다.
6. 구조체 필드 기본값
C에서 구조체 필드의 기본값을 지정할 수 없습니다. Nim에서도 마찬가지입니다. 생성자도 없으므로, 생성자의 역할을 대신하는 별도의 함수를 정의해줘야합니다.
7. named loop
C에서 다중 루프(nested loop)를 한번에 빠져나가기 위해서는 goto를 사용해야했습니다. Nim에서는 named loop는 없고, named block으로 다중 루프를 빠져나갈 수 있습니다.
8. 그 밖의 특징
(1) 대소문자와 밑줄 문자 구분 안함(Case/underscore insensitivity)
대소문자를 구분하지도 않고 밑줄 문자(_)도 인식하지 않습니다. 이게 장점인지 단점인지 확신은 없으나, 아직 IDE 지원이 불충분한 상황에서 Refactoring 하기에 불편합니다. 주류의 방식은 대소문자를 구분하는 것이고, 굳이 비주류 방식을 써야하는지는 의문입니다.
(2) 중괄호와 들여쓰기
중괄호 대신에 들여쓰기를 사용해서 코드의 Block을 나타냅니다. (Python방식의 들여쓰기) 이것도 굳이 비주류 방식을 써야했는지 의문입니다.
(3) UFCS(Uniform Function Call Syntax)
함수 호출시 f(a, b) 대신에 a.f(b) 로 쓸 수 있습니다. 편리한 기능인 것은 맞지만, 장점만 있는 것은 아닙니다. 모듈 m에 있는 함수 g를 호출하는 것도 m.g() 로 씁니다. 모듈의 이름도 보통 소문자로 쓰고, 변수의 이름도 보통 소문자로 쓰므로, m.f() 라고 쓰면 m이 모듈인지 변수인지 혼동이 됩니다. Nim에서는 대소문자를 구분하지도 않으므로, 모듈의 경우 m::g() 같은 식으로 다르게 나타냈더라면 더 좋았을 것입니다.
9. 결론
종합적으로 살펴보면, Nim은 "더 나은 C"를 목표로 하지만, Garbage Collector를 사용한다는 점에서 실질적인 경쟁자는 C#나 Java라고 볼 수 있습니다. Nim은 가상머신을 사용하지 않는다는 점에서 C#과 Java와는 다르다고 생각할테지만, 이는 프로그래밍 언어의 차이점은 아니고 실행환경상의 차이점입니다.
C#과 Nim 중에서 어떤 언어가 사용하기 편하냐고 물어보면(라이브러리 같은 외부적인 요소를 제외하고 순수하게 문법적인 측면에서), 저는 C#의 손을 들어주겠습니다.
그러면 남은 장점은 성능적인 측면에서 C#보다 Nim이 더 낫다는 것인데, "Garbage Collector를 사용하며 C#보다 성능은 더 낫고 C보다 사용하기 더 편한 언어"는 이미 D가 있고, 그 D 마저도 Rust에 밀리는 추세입니다.
그리고 "더 나은 C"를 목표로 한다면, 특별한 장점이 없는 한 C의 방식을 유지하는 것이 C의 사용자를 끌어오는데 더 유리하다 보는데, Case/underscore insensitivity, Python방식의 들여쓰기를 도입해서 무엇을 얻고자 하는 것인지 알 수가 없습니다.
계속 단점만 늘어놔서 Nim이 굉장히 안좋은 언어인 것처럼 보일 수 있지만, 그렇지는 않으며, 만약 순환 import 문제(3. 참조)가 없고, 구조체 필드의 기본값을 지정(6. 참조)할 수 있고, 자잘한 문제(8. 참조)가 없다면, 꽤 괜찮은 언어로 평가했을 겁니다.
현재 시점 기준으로는 다소 부족한 부분이 보이지만, 신생 언어라서 향후 어떻게 바뀔지 모르고 나름 장점도 많으므로, 아직은 좀 더 지켜봐야할 것 같습니다.
그렇다면 C에서 불편했던 점이 Nim에서 어떻게 개선됐는지 살펴봅시다.
1. 메모리 관리
C에서는 메모리를 수동으로 관리해줘야합니다. malloc, calloc, realloc 함수로 메모리를 동적할당했으면, free 함수로 메모리를 해제해줘야합니다.
반면에 Nim은 Garbage Collector를 사용하고 있습니다.
이렇게 ref object 키워드를 사용해서 타입을 정의하면 참조 타입이 되고, 참조 타입의 메모리 관리를 자동으로 해줍니다. 여기까지는 Java와 다를 게 없습니다.type Entity* = ref object
x*: int
y*: int
그런데 성능상의 이유로 참조 타입말고 값 타입을 사용하고 싶은 경우가 있는데, 이럴 때는 object 키워드만 사용해서 타입을 정의하면 됩니다.
type EntityObj* = object
x*: int
y*: int
여기까지는 C#에서도 가능합니다. (struct는 값 타입, class는 참조 타입)재미있는 점은 둘을 섞어서 사용할 수 있다는 것입니다.
type EntityObj* = object
x*: int
y*: int
type Entity* = ref EntityObj
위와 같이 하면, EntityObj를 값 타입으로 정의하고 Entity를 참조 타입으로 정의해서 둘 다 사용할 수 있게 됩니다.값 타입(EntityObj)의 변수에 addr 함수를 사용하면 포인터(pointer)를 얻을 수 있는 반면에, 참조(Entity)를 얻을 수는 없습니다. 따라서 값 타입(EntityObj)용 함수와 참조 타입(Entity)용 함수를 따로 만들어줘야합니다. 값 타입과 참조 타입을 섞어서 사용할 수 있는 것처럼 보이지만 실질적으로는 둘은 별개입니다. 따라서 문법적인 측면에서 C#에 비해 큰 장점은 없습니다.
2. 포인터
C에서는 포인터를 잘 사용해야합니다. 함수 호출시 인자를 전달하는 방식이 Call by value이고 배열도 포인터라서 포인터를 쓸 수 밖에 없습니다. Nim에서는 Call by reference 방식도 지원하고(인자의 타입 앞에 var를 붙이면 됩니다.) 배열은 포인터와 다르므로, 포인터를 사용할 일이 별로 없습니다. 그런데 이건 C#도 마찬가지입니다.
3. 헤더 파일
C에서는 선언과 정의의 순서가 중요합니다. S라는 구조체를 선언하기 전에는 S를 사용할 수 없고, 일반적으로 f라는 함수를 선언하기 전에는 f를 사용할 수 없습니다. 이는 Nim에서도 마찬가지입니다.
C에서는 헤더 파일을 관리해줘야합니다. 다른 파일에서 include할만한 부분(타입 정의, 함수 선언 등)은 헤더 파일(*.h)에 넣고 함수의 정의는 소스 파일(*.c)에서 작성합니다.
반면에 Nim에서는 헤더 파일을 별도로 둘 필요가 없고 import를 하면 됩니다.
Java에서는 패키지 단위로 import를 하는데, Nim에서는 파일 단위로 import를 합니다.
# A.java
public class A {
public B b;
}
# B.java
public class B {
public A a;
}
Java에서는 같은 패키지 내에서는 import를 하지 않아도 public이기만 하면 다른 파일에 정의된 클래스를 사용하는데 아무 문제가 없습니다. 반면에 Nim에서는 다른 파일에 정의된 타입을 사용하려면 import가 필수입니다. 그런데 문제는 순환 import는 금지돼있습니다. 예를 들어, a.nim에서 b를 import하고, b.nim에서 a를 import해서는 안됩니다.# a.nim
import b
type A* = B
# b.nim
import a
type B* = int
var b*: A
위의 예제에서는 a.nim의 A라는 타입은 b.nim의 타입 B에 의존하고, b.nim의 변수 b는 a.nim의 타입 A에 의존합니다. 이런 경우 컴파일 오류가 발생합니다.이런 문제를 막으려면, C나 C++와 유사하게 타입 정의와 구현부(변수 및 함수 정의)으로 나누거나, 타입과 전역 변수를 하나의 파일에 몰아넣으면 되기는 하지만, 어떤 방식을 취하든 불편함은 감수해야합니다.
4. 객체지향 프로그래밍
C는 절차지향 언어입니다. 객체지향 프로그래밍이 가능하기는 하지만(예를 들어, GObject), 불편합니다. Nim도 절차지향 언어입니다. 객체지향적인 요소는 거의 없습니다. 생성자와 소멸자가 없고, 상속은 가능하지만 필드만 상속받고, 동적 디스패치(dynamic dispatch)가 없습니다. 객체지향 프로그래밍을 하려면 다른 언어를 찾아보는 것이 좋습니다.
5. 함수형 프로그래밍
Nim은 C보다 함수형 프로그래밍을 더 잘 지원합니다. 함수 내에서 함수를 정의할 수 있고, 클로저(Closure)와 익명 함수를 사용할 수 있습니다. 이 부분에서는 C#과 큰 차이가 없습니다.
6. 구조체 필드 기본값
C에서 구조체 필드의 기본값을 지정할 수 없습니다. Nim에서도 마찬가지입니다. 생성자도 없으므로, 생성자의 역할을 대신하는 별도의 함수를 정의해줘야합니다.
7. named loop
C에서 다중 루프(nested loop)를 한번에 빠져나가기 위해서는 goto를 사용해야했습니다. Nim에서는 named loop는 없고, named block으로 다중 루프를 빠져나갈 수 있습니다.
block myblock:
for i in 1..10:
for j in 1..10:
# ...
if cond:
break myblock
block에 이름을 붙이고 break 키워드로 block을 빠져나갈 수 있습니다. 반면에, 다중 루프에서 continue를 하기 위해서는 다음과 같이 하면 됩니다.for i in 1..10:
block myblock:
for j in 1..10:
# ...
if cond:
break myblock
그런데 이 정도는 C에서 goto로 비슷하게 구현할 수 있고, 가독성 측면에서 C보다 약간 더 나은 수준입니다. C#은 goto를 사용할 수 있고, Java는 named loop를 사용할 수 있습니다.8. 그 밖의 특징
(1) 대소문자와 밑줄 문자 구분 안함(Case/underscore insensitivity)
대소문자를 구분하지도 않고 밑줄 문자(_)도 인식하지 않습니다. 이게 장점인지 단점인지 확신은 없으나, 아직 IDE 지원이 불충분한 상황에서 Refactoring 하기에 불편합니다. 주류의 방식은 대소문자를 구분하는 것이고, 굳이 비주류 방식을 써야하는지는 의문입니다.
(2) 중괄호와 들여쓰기
중괄호 대신에 들여쓰기를 사용해서 코드의 Block을 나타냅니다. (Python방식의 들여쓰기) 이것도 굳이 비주류 방식을 써야했는지 의문입니다.
(3) UFCS(Uniform Function Call Syntax)
함수 호출시 f(a, b) 대신에 a.f(b) 로 쓸 수 있습니다. 편리한 기능인 것은 맞지만, 장점만 있는 것은 아닙니다. 모듈 m에 있는 함수 g를 호출하는 것도 m.g() 로 씁니다. 모듈의 이름도 보통 소문자로 쓰고, 변수의 이름도 보통 소문자로 쓰므로, m.f() 라고 쓰면 m이 모듈인지 변수인지 혼동이 됩니다. Nim에서는 대소문자를 구분하지도 않으므로, 모듈의 경우 m::g() 같은 식으로 다르게 나타냈더라면 더 좋았을 것입니다.
9. 결론
종합적으로 살펴보면, Nim은 "더 나은 C"를 목표로 하지만, Garbage Collector를 사용한다는 점에서 실질적인 경쟁자는 C#나 Java라고 볼 수 있습니다. Nim은 가상머신을 사용하지 않는다는 점에서 C#과 Java와는 다르다고 생각할테지만, 이는 프로그래밍 언어의 차이점은 아니고 실행환경상의 차이점입니다.
C#과 Nim 중에서 어떤 언어가 사용하기 편하냐고 물어보면(라이브러리 같은 외부적인 요소를 제외하고 순수하게 문법적인 측면에서), 저는 C#의 손을 들어주겠습니다.
그러면 남은 장점은 성능적인 측면에서 C#보다 Nim이 더 낫다는 것인데, "Garbage Collector를 사용하며 C#보다 성능은 더 낫고 C보다 사용하기 더 편한 언어"는 이미 D가 있고, 그 D 마저도 Rust에 밀리는 추세입니다.
그리고 "더 나은 C"를 목표로 한다면, 특별한 장점이 없는 한 C의 방식을 유지하는 것이 C의 사용자를 끌어오는데 더 유리하다 보는데, Case/underscore insensitivity, Python방식의 들여쓰기를 도입해서 무엇을 얻고자 하는 것인지 알 수가 없습니다.
계속 단점만 늘어놔서 Nim이 굉장히 안좋은 언어인 것처럼 보일 수 있지만, 그렇지는 않으며, 만약 순환 import 문제(3. 참조)가 없고, 구조체 필드의 기본값을 지정(6. 참조)할 수 있고, 자잘한 문제(8. 참조)가 없다면, 꽤 괜찮은 언어로 평가했을 겁니다.
현재 시점 기준으로는 다소 부족한 부분이 보이지만, 신생 언어라서 향후 어떻게 바뀔지 모르고 나름 장점도 많으므로, 아직은 좀 더 지켜봐야할 것 같습니다.
댓글
댓글 쓰기