프로그래밍 언어는...

envia의 이미지
envia의 이미지

최근 Python과 Perl을 배우면서 둘의 철학이 다르다는 이야기를 들었습니다.

Perl
There's more then one way to do it. (TMTOWTDI)

Python
There should be one-- and preferably only one --obvious way to do it.

Perl은 사용자들을 믿고 자유를 주는 것 같고, Python은 사용자들이 보기 쉬운 프로그램을 만들도록 제한을 주는 것 같습니다. 언어에 따라 이러한 철학이 다른 것 같습니다. 예를 들어 C같은 언어는 프로그래머가 자신이 무엇을 하려고 하는지 안다고 생각하지요.

flame을 일으킬 수 있는 소재일지도 모르지만, 여기 계신 분들은 어떤 생각을 가지고 있는지 궁금합니다. 여러분은 어느 쪽을 좋아하세요?
(저는 어느 정도 제한을 해 주는 것이 낫다고 생각합니다.)

----

It is essential, if man is not to be compelled to have recourse, as a last resort, to rebellion against tyranny and oppression, that human rights should be protected by the rule of law.
[Universal Declaration of Human Rights]

perky의 이미지

Perl은 많이 해보지는 않아서 잘 모르지만,
제 생각에는 두 슬로건 모두 단지 마케팅적인 광고문구일 뿐 큰 의미는 없다고 봅니다. :)
Perl 개발자들이라고 쓸데없이 같은 문법을 예쁘다고 여러 개 추가하지도 않을 것이고..
Python 개발자들이라고 진짜로 한가지 일을 위해서 한가지 방법으로만 되게 만들지도 않습니다.
약간의 경향은 차이는 있겠지만요..

SKY핸드폰이 It's different. 라고 광고해도, 실은 다른 핸드폰이나 별로 다를게 없죠.. :)

You need Python

alfalf의 이미지

일단은 구조적으로 들여쓰기를 해야만 하게끔 하는 Python의 정책을 아주 좋다고 생각합니다.
프로그램이 한번만 보고 끝나는게 아닌 이상 가독성이 좋다는 것은 큰 장점이 될수 있으니까요.
Perl 프로그램은 잘 만들어진 프로그램이라도 읽기가 좀 힘들더군요.

cjh의 이미지

어떤 언어든 한가지 일을 여러가지 방법으로 할 수 있습니다. 짧게 하는 방법부터 길게 하는 방법, 모듈을 쓰는 방법, OO-style로 하는 방법 등등...

perl은 그게 모토이지만 그렇다고 python이나 ruby가 그렇지 않다는 것은 아닙니다. 다만 perl같으면 oo-style과 non-oo style이 혼재 가능하기 때문에 짜는 사람 관점에 따라서 같은 기능인데도 완전히 다른 프로그램처럼 보이기 십상이죠.

p.s. 전 python이나 perl이나 길면 다 읽기 힘듭니다. :< 기호 없다고 읽기 쉬운게 아니죠. 어떻게 보면 무의미한 알파벳 나열로 보이기 때문에...

--
익스펙토 페트로눔

monpetit의 이미지

cjh wrote:
전 python이나 perl이나 길면 다 읽기 힘듭니다. :< 기호 없다고 읽기 쉬운게 아니죠. 어떻게 보면 무의미한 알파벳 나열로 보이기 때문에...

맞습니다. 길게 짜 놓으면 나중엔 자기 것도 읽다가 화납니다.
maddie의 이미지

글쎄요. 잘은 모르지만, 어떠한 코딩스타일을 추천을 하돼, 방법을 여러가지로 해서 작성자의 스타일이 존중되어야 한다고 생각합니다.

파이선을 안써봐서 함부로 말씀드리기는 힘들지만, 너무 엄격한 문법적용은 작성자의 창조성을 막는 결과가 나올수도 있다고 생각합니다.

단 어떤 기준을 제시하고, 그 기준에 따라줄 것을 권장할 필요성은 있다고 봅니다. 솔직히, perl은 읽기 힘든게 사실입니다. ㅡ.ㅡ

힘없는자의 슬픔

fibonacci의 이미지

저는 수학이란 어떤면에서 "정형화된" 학문을 하는 사람이라 그런지,
정형화된 프로그래밍의 모양이 담긴 언어를 좋아합니다.
C를 배웠다가 C++을 집중적으로 파지 않고, python을 익힌것도 그런 이유에서였고요.. 물론 제가 연구하는 분야의 프로그램이 python으로 만들어져있어서(SnapPea) 그걸 분석하느라 python을 익힌것도 있지만요.
그렇지만 제가 컴퓨터 실무에 있는 사람이라면, 자유도가 높아서 손가는대로 코딩하기가 수월한 그런 언어도 좋아했을것 같습니다.

No Pain, No Gain.

서지훈의 이미지

상황에 따라서 두가지의 경우가 필요한 경우도 있겠지만...
전 기본적으로 "좋지 않은 코드가 생기는 것을 막아야 한다." 라고 생각을 합니다.
이로인해 가독성과 포퍼먼스를 더 높인 코드가 나오지 않을까 생각을 합니다.

제발 코딩에선 바벨탑의 절차를 밝지 않았으면 하는 바람이...-_-ㅋ

<어떠한 역경에도 굴하지 않는 '하양 지훈'>

#include <com.h> <C2H5OH.h> <woman.h>
do { if (com) hacking(); if (money) drinking(); if (women) loving(); } while (1);

차리서의 이미지

먼저 결론부터 말하자면, 이런 언어도 있고 저런 언어도 있어서 항상 다양하게 선택할 수 있으면 좋겠습니다. :)

처음 문제를 제시하신 분께서 말씀하신 정확한 의미가 무엇이었을지 궁금해집니다. 프로그래밍 언어(이하 그냥 '언어')가 좋지 않은 코드가 생기는 것을 막아준다는 의미가, 다른 분들께서 댓글에서 말씀하신 것처럼 언어의 특성에 의해 프로그래밍의 스타일이나 습관이 상대적으로 바람직한 방향으로 유도된다는 의미인지, 아니면 구문 검사나 타입 검사, 심지어는 패턴 검사나 값 검사 등의 장치들을 통해 확실히 올바르다고 검증되는 것 이외에는 자동적으로 차단해주는 시스템을 갖추고 있다는 의미인지 말이죠. 전자에 대해서는 별로 경험도 없고 의견도 없습니다만, 후자에 대해서는 약간 이야기해볼 재미있는 측면이 있습니다.

구문 검사는 기계어 이외의 대부분의 현존 언어들이 갖추고 있는 것으로 알고 있습니다. C언어만 하더라도 컴파일할 때에 syntax error가 나면서 컴파일이 안되고 멈추어버리는 것이 일종의 구문 검사겠죠. 언어 입장에서 프로그래머(이하 그냥 '사람')에게 "네가 말한 명령은 문법이 잘못되어 있어서 뭘 시킨건지 알아먹을 수가 없다. 올바른 문법으로 다시 말해봐라"라고 뗑깡을 부려주는 것입니다. 그나마 이렇게 '뭘 시킨건지 알아먹을 수가 없는' 경우는 양반입니다. 이런 경우에는 설사 언어가 구문 검사를 제대로 해주지 못한다고 해도 결국 실행을 못할테니 원치 않는 오동작으로 낭패를 보는 일은 생기지 않을지도 모릅니다.

구문 검사가 꼭 필요한 더 심각한 경우는 이런 경우입니다: 언어가 사람에게 "네가 말한 명령은 문법이 잘못되어 있어서 시키는 대로 했다가는 뭔 난리가 날지 모른다. 정말 그따위로 말한 명령을 나더러 수행하라는건지 잘 확인해보기 바란다"라고 말하면서 컴파일을 거부해야만 하는 경우죠. 특히 기계어의 경우에는 이런 일이 얼마든지 일어날 수 있습니다. 기계어의 정해진 인스트럭션 셋에 없는 신호열을 말해버리면 (즉, 구문 오류) 이걸 그대로 메모리에 올리고 수행했다가는 정말 뭔 일이 날지 모르죠. 물론 C언어 정도만 되어도 이런 일은 드물겁니다.

C언어에서도 다반사로 나타나는 (그리고 C언어는 미리 검사해서 알려주지 못하는) 경우는 타입이 맞지 않는 경우입니다. C언어에는 타입이 있어보이지만, 어떤 의미에서는 타입이 있으나마나이기도 합니다. 예를 들어, C언어로 다음과 같은 코드를 만들면:

#include <stdlib.h>
#include <stdio.h>

int main(void) {
    char x = 'a';
    char* s;
    printf("%d\n", x + s);
    return EXIT_SUCCESS;
}

C 컴파일러는 별 불만 없이 이 코드를 컴파일해줄 것이며, 심지어 컴파일된 결과를 실행시킬 수도 있습니다. 그 실행 결과가 사람이 원했던 것이 맞는지 아닌지는 차치하고, 일단 실행은 되죠. 문제는, 이렇게 컴파일하고 실행해서 사람이 원했던 결과가 나온다고 해도, 즉, 애초에 사람이 원했던 것이 문자 'a'의 아스키 코드 값과 어떤 메모리 번지수의 합을 출력하는 것이었다고 해도, 이걸 정형적으로 유추해줄 그 무엇도 C언어는 가지고 있지 않다는 점입니다. 물론 gcc 등 많은 현대적인 C 컴파일러들은 위의 코드를 컴파일할 때에 "정말 문자랑 번지수를 더할거야?"라고 Warning을 내주긴 하지만, warning은 warning일 뿐 단호하게 컴파일을 거부해주지는 않습니다.

문제를 제시하신 분께서 말씀하신 '사용자를 믿고 자유를 주는' 대표적인 경우로서, 실제로 C언어에 능통한 사람이 되기 위한 여러가지 요건이라고 일컬어지는 것들 속에는 이런 함정들을 요리조리 잘 피해나갈 뿐만 아니라 거꾸로 이런 함정들을 잘 역이용해서 효율이나 생산성(?)을 높이는 능력도 포함되는 것 같습니다. 즉, 빠지면 함정이요 잘 쓰면 꽁수라는 얘기겠죠. 모두들 그렇게 생각한다는 것이 아니라, 그렇게 생각하는 사람들이 많아보인다는 얘기이며, 따라서 반대로 생각하는 사람들도 있습니다. 이런 함정을 피해나가는 일은 주어진 문제를 해결하려는 사람(프로그래머)의 몫이 아니라 명백히 기계(혹은 언어)의 몫이라고 생각하는 것이죠. 그런 의도의 일환으로 타입 검사를 해주는 언어들이 있습니다.

다양한 수준으로 타입을 검사해주는 언어가 많습니다만, 가장 대표적이자 참 잘 설계된 언어 중 하나가 Haskell이라고 생각합니다. 사실은, 제 능력 상 예를 들어 보일만한 언어가 이것 뿐이라는게 맞겠습니다. 만일 Haskell로 다음과 같은 선언을 하면 (코드의 길이 관계상 Main 모듈 등 전체 stand-alone 프로그램을 다 보이지는 않겠습니다):

data Weekday = Mon | Tue | Wed | Thu | Fri | Sat | Sun
        deriving (Read, Show, Eq, Ord, Enum, Bounded)

data Color = Red | Green | Blue | Violet | Yellow | White | Black | Gray
        deriving (Read, Show, Eq)

next d | d == maxBound    = minBound
       | otherwise        = succ d

oppcolor Black = White
oppcolor White = Black
oppcolor c     = next c

Haskell 컴파일러는 위 선언을 절대로 인정해주지 않습니다.

함수 next의 타입은 (Bounded a, Eq a, Enum a) => a -> a 가 됩니다. minBound와 maxBound가 Bounded 타입클래스에 속하는 타입들에 대해서만 작동하는 메서드(nullary 함수도 함수니까요)로서 정의되어 있고, (==) 함수는 Eq 타입클래스의 인스턴스 타입들에 대해서만 작동하는 메서드며, succ이 Enum 타입클래스에 속하는 인스턴스 타입들에 대해서만 작동하는 메서드로서 정의되어있기 때문입니다. 따라서 next Tue는 Wed이 되고 next Sun은 Mon이 되겠죠.

oppcolor의 타입은 oppcolor Black = White 라는 선언에 의해 Color -> Color로 그 primary type이 유추되며, Color 타입이 Enum 타입클래스와 Bounded 타입클래스에 속하지 않기 때문에 next 함수를 적용할 수 없으므로 oppcolor c = next c 를 허용할 수 없게됩니다. 이런 경우 Haskell은 이런 선언(프로그램)을 컴파일해주지 않습니다. 즉, 언어가 사람에게 이렇게 말하는 것이죠: "네가 말한 명령은 일단 구문은 맞는데, 의미가 영 이상해~. 너 분명히 next는 경계값이 있고 동치를 판단할 수 있으며 나열형인 타입에 대해서 작동한대놓고 이제와서 왜 경계값도 없고 나열형도 아닌 Color 따위의 타입에다가 적용하려들고 지랄이여? 헛소리 말고 다시 명령해 봐~~" 라구요...

또다른 예로, factorial을 다음과 같이 정의하면

factorial n | n <= 1    = 1
            | otherwise = n * factorial (n - 1)

(*) 함수와 (-) 함수는 IntInteger 타입이 아닌 Num 타입클래스에 속하는 모든 타입들에 대해 정의되어 있는 메서드고 (<=) 함수는 Ord 타입클래스에 속하는 모든 타입들에 대해 정의되어 있는 메서드기 때문에, Haskell은 factorial 함수의 타입을 Integer -> Integer 대신 (Num a, Ord a) => a -> a 로 유추해버립니다. 즉, factorial 5.8 이라고 해버려도 타입 오류가 나지 않으며, 533.19168이라는 원치 않는 오작동을 해버리죠. 이 문제를 피하기 위해 명시적으로

factorial :: Integer -> Integer
factorial n | n <= 1    = 1
            | otherwise = n * factorial (n - 1)

라고 선언할 수 있으며, 이렇게 선언된 factorial을 5.8 같은 엉뚱한 타입의 값에 적용하려는 시도는 Haskell 언어 자체가 스스로 거부해버립니다. 즉, 언어가 사람에게 이렇게 말해줍니다: "factorial은 순서가 있는 모든 수치형 타입들에 대해서 작동하는게 아니라 딱 집어서 정수 타입에 대해서만 작동한다며? 네가 분명히 그렇게 말해놓고 왜 이제와서 5.8 따위에다가 적용할려고 용쓰남? 너 변태지?" 라고 말이죠.

실제로 C 처럼 타입 검사를 무시하는 언어의 자유로움(?)에 의해 야기되는 문제가 현대 소프트웨어 문제에서 적지 않은 비중을 차지하기 때문에, 이렇게 타입 검사를 수행해주는 언어도 역시 생각해볼 필요가 있을 듯합니다.

조금 다른 얘기로, 패턴 검사는 OCaml이 잘 해주더군요. 예를 들어 위에서 보았던 factorial 함수 정의를 조금 위험하게 다음과 같이 선언해도:

factorial :: Integer -> Integer
factorial n | n == 1    = 1
            | n >  1    = n * factorial (n - 1)

Haskell은 아무 문제를 제기하지 않으며, 이런 factorial 함수 선언을 포함하는 프로그램을 그냥 컴파일해버립니다. 이후 런타임에 -2 같은 음수에 factorial을 적용하는 사태가 벌어지면 프로그램은 그제서야 factorial이 -2에 대해서 정의되어있지 않음을 알아채고 런타임 오류를 내버립니다.

그런데, OCaml은 신기하게도 이런 선언을 보면 컴파일 타임에 이미 다음과 같이 말해줍니다: "네가 말한 factorial이라는 놈은 자칭 정수로부터 정수로의 함수랍시고 깝쭉대는 주제에, 정의역(정수)의 모든 원소에 대해 정의되어 있는게 아닌데? 네가 말한 패턴들, 즉 n이 1인 경우와 n이 1보다 큰 경우에 모두 포함되지 않는 다른 정수가 있는데 그건 어쩔려구 이러는겨?"라고 말이죠. 문제는, 이렇게 말해주면서도 딱히 '거부'하지는 않더라는 점입니다. 마치 C 언어가 타입 불일치를 보면서 warning을 때려주는 것처럼, OCaml도 패턴 매칭과 함수 정의역의 불일치에 대해 경고에 그치는 듯한 인상을 주었습니다. 아뭏든 재미있고 신기한 측면인 것은 분명하더군요.

마지막으로 값 검사는 (제 얇팍한 지식으로는) 아직 안되는 걸로 알고 있습니다. 예를 들어, div 라는 (Num a) => a -> a -> a 타입의 함수를 정의하되, 나중에 프로그램 실행 도중에 0으로 나누는 경우가 발생할지도 모르는 상황을 컴파일 시점에서 미리 추론해서 차단해주는 방법은... 연구중인 것으로 알고 있습니다.

그리고, 이것을 무슨 검사라고 불러야하는지 모르겠을 뿐더러 ('종료 검사' 정도로 말해야할까요) 불가능하고 알려진 유명한 문제이긴 하지만, 이런 예도 한 번 생각은 해볼 수 있습니다 (유명한 halting problem이므로 불가능하다는 것이 거의 확실하며, 단지 생각해볼 뿐입니다):

factorial :: Integer -> Integer
factorial n = n * factorial (n - 1)

이렇게 정의된 factorial은 당연히 종료되지 못하고 무한 재귀에 빠지게됩니다만, 컴파일 타임에 미리 '이 함수의 재귀 적용은 언젠가 끝난다' 혹은 '영원히 끝나지 못한다'를 파악해서 걸러내주는 언어가 있다면 유용하지 않을까요? 물론 위의 예처럼 문제가 단순하다면 편법으로 '종료 조건이 없다'는 것만 파악해서 무한 재귀의 위험성을 감지할 수도 있겠지만, 다음과 같은 경우라면:

factorial :: Integer -> Integer
factorial n | n <= 1    = 1
            | otherwise = n * factorial (n + 1)

언어가 Integer라는 타입의 정확한 특성과 (+) 함수와의 관계를 충분히 이해하여 1보다 큰 n에 1을 계속 더해서는 영원히 1보다 작거나 같은 경우나 나올 수 없음을 알 수 있어야합니다.

비슷하지만 좀 더 뚜렷하게 halting problem을 피해가야하는 경우를 보여주는 예로서 이련 경우가 있습니다. 아무리 언어가 lazy evaluation을 제공해도, 일반적인 filter의 정의인

filter f []     = []
filter f (x:xs) | f x       = x : filter f xs
                | otherwise = filter f xs

와 일반적인 무한 리스트의 정의인

enumFrom n = n : enumFrom (succ n)

을 감안하면 1로 시작하는 단조 증가 무한 리스트에서 10보다 작은 값만을 얻기위해서는:

filter ((>) 10) [1..]

요따위로 말해서는 원하는 바를 이룰 수 없다는게 문제입니다. 사람은 이 상황에서 filter ((>) 10) [10..] 부터는 생각해볼 필요가 없음을 알지만, 언어는 일반적인 filter로는 이를 판단할 수 없기 때문입니다. (해 봐야 알고, 끝없이 하겠죠. 역시 halting problem이 아닐까 합니다.) Haskell Prelude는 이런 경우에 특화된 장치로서 takeWhile이나 dropWhile 등을 제공하긴 하지만, 이건 어디까지나 특정 상황에 대한 대책일 뿐 근본적인 해법은 아니죠.

물론 succ이라는 Enum 타입클래스의 메서드에 대해서 '단조 증가' 라든가 하는 개념을 아예 언어에 심어버리고, filter 등의 정의를 이런 Enum 계열 타입들의 단조 성질을 끌어안도록 선언하면 '언어가 올바르지 않은 (무한 재귀에 빠지는) 코드를 막아주는 효과'를 볼 수도 있겠지만, succ이 아니라 예를들어 삼각함수가 만드는 진동에 대한 filter라면 얘기는 또 달라집니다. 보다 근본적인 해법이 필요해지는 것이죠.

쓰다보니 글이 상당히 길어졌군요. 여기까지 읽는 분이 계시려나.... -_-; 아뭏든, 장황하게 이 얘기 저 얘기 한 것에 비해 오히려 저는 딱히 '언어는 반드시 이래야만 한다'는 생각을 갖고 있지 않습니다. 글 머리에 말했듯이, 이런 저런 언어가 다양하게 있어서 적절히 선택할 수 있고 제게 그럴 능력까지 있기를 바랄 뿐이죠. 타입 검사나 패턴 검사를 잘 해주는 언어를 가지고 부담 없이 (함정에 빠지지 않으려고 머리털 빠지는 일 없이 편안하게) 발상을 프로그램으로 옮기다가도, 때에 따라서는 역시 기계에게 이렇게 말해줄 수 있는 언어가 필요해질 때가 있거든요:

"내가 지금 너한테 뭘 시키고 있는건지, 그래서 네가 결국 어떻게 움직일지 이미 잘 알면서 시키는거니까, 꼬치꼬치 토 달지 말고 닥치고 시키는 대로 움직이기나 해!" :P

PS: 예로 든 내용중에 말도 안되는 헛소리를 했던 부분이 있어서 약 반나절 후에 수정했습니다. 수정 전에 보셨던 분들께 사과드립니다. ^^;

--
자본주의, 자유민주주의 사회에서는 결국 자유마저 돈으로 사야하나보다.
사줄테니 제발 팔기나 해다오. 아직 내가 "사겠다"고 말하는 동안에 말이다!