Haskell 표준 라이브러리의 몇 가지 동작(예: Int 오버플로, System.Info.os)이 의미적 순수성과 참조 투명성의 원칙에 어긋난다는 문제의식에서 출발해, 타입의 정확한 기의(denotation)와 평가 일관성의 원칙을 제시하고 토론한다.
URL: http://conal.net/blog/posts/notions-of-purity-in-haskell
제목: Haskell에서의 순수성 개념
영감과 실험 — 주로 Haskell의 지시적/함수형 프로그래밍에 관하여
« 논문: 아름다운 미분(Beautiful differentiation)
2009년 3월 30일 오전 11:00
요즘 내가 소중히 여겨온 몇 가지 프로그래밍 원칙이 Haskell 동지들 사이에 널리 공유되지는 않는다는 사실을 배우고 있다. 적어도 내가 들어온 범위에서는. 이 소식에 마음이 좀 상해서, 스스로 정리도 할 겸, 또 내가 찾고 있는 것에 공감하는 이가 누군지 보고 싶어 이 글을 쓰기로 했다.
내가 말하고자 하는 원칙 가운데 하나는 닫힌 표현(자유 변수를 포함하지 않는 표현)의 값이 오롯이 그 표현 자체에만 의존하며 — 실행되는 동적인 조건에 의해 영향을 받지 않는다는 것이다. 나는 이 원칙이 함수형 프로그래밍, 특히 참조 투명성의 혼과도 같다고 느낀다.
수정 사항:
최근 표준 Haskell 라이브러리에서 이 원칙과 조화시키기 어려운 두 가지 사실을 접했다.
Int 연산의 오버플로 상황에서의 의미가 머신 의존적이다. 보통 32비트 머신에서는 32비트를, 64비트 머신에서는 64비트를 쓴다. 구현에 따라 최소 29비트까지 써도 된다. 따라서 “2^32 == (0 ::Int)”라는 표현의 값은 평가되는 동적 조건에 따라 False일 수도, True일 수도 있다.System.Info.os”의 타입이 String인데, 그 값(문자열 시퀀스)은 실행 환경에 따라 달라진다. ( System.Info의 다른 내보내기들도 마찬가지다.) 음. 모듈에 “portable(이식 가능)”이라는 라벨이 붙어있는 걸 방금 봤다. 오타? 농담?나는 1995년쯤부터 주로 Haskell로 프로그래밍해 왔지만, 이런 구현 의존적 의미가 있다는 사실을 몰랐다. 많은 연애 관계가 그렇듯, 나도 Haskell을 있는 그대로가 아니라 내가 이상화한 모습으로 보고 있었던 모양이다.
또 하나의 원칙이 있는데, 위의 것과 밀접하고 내게는 더 근본적이다. 바로 모든 타입은 정확하고 구체적이며 가능하면 단순한 지시 의미(denotation)를 가진다는 것이다. 어떤 표현식 e가 타입 T라면, e의 의미(값)는 T가 지시하는 집합의 원소다. 예를 들어, 나는 String, 즉 [Char]의 의미를 문자들의 시퀀스로 여긴다. 음, 사실 이보다 약간 복잡하다. 부분적으로 정의된 시퀀스도 포함하고, 부분 정보 순서(이 경우 평탄하지 않은)가 있기 때문이다. 이 두 번째 원칙에 따르면, 만약 os :: String이라면 os의 의미는 어떤 문자 시퀀스여야 한다. 그 시퀀스가 유한하고 부분적이지 않다면 리터럴 문자열로 적어둘 수 있고, 그 리터럴을 프로그램의 모든 “os” 위치에 대체해도 프로그램의 의미는 변하지 않아야 한다. 하지만 os는 내 머신에서는 “linux”로, 내 친구 Bob의 머신에서는 “darwin”으로 평가된다. 따라서 “os”에 어떤 리터럴 문자열을 대체하더라도 적어도 둘 중 한 머신에서는 의미가 바뀌게 된다.
지금 내가 이야기하는 것이 표준 Haskell ‘언어’가 아니라 표준 Haskell ‘라이브러리’라는 점은 잘 안다. #haskell 채팅방에서 내 혼란과 실망을 이야기했을 때, 누군가가 이런 의미 차이를 서로 다른 라이브러리, 그리고 그에 따라 서로 다른 프로그램(프로그램을 사용하는 라이브러리를 포함한다고 본다면)으로 설명하자고 제안했다. 라이브러리가 다르면(그로 인해 프로그램이 다르면) 의미도 다를 것이라 기대하는 게 자연스럽다는 것이다.
이 “다른 라이브러리” 관점은 — 문자 그대로라면 — 이해한다. 하지만 여전히 만족스럽지는 않다. 내가 얻은 결론은 표준 라이브러리는 서명(형태)만 표준이고 의미(실질)는 표준이 아니라는 점이다. 의미적 공통성에 대한 보장이 없다면, 표준 라이브러리가 어떻게 유용할 수 있을지 모르겠다.
#haskell에서 또 나온 관점은 내가 찾는 종류의 의미적 일관성은 실패의 가능성 때문에 ‘불가능’하다는 것이었다. 예컨대 어떤 표현을 평가할 때 한 번은 메모리 고갈로 실패하고, 다른 번에는 (아슬아슬하게) 성공할 수 있다. 그 지적을 곰곰이 생각해보고, 내 원칙을 조금 약화하고 싶다. 모든 평가가 ‘같은’ 값을 내라고 요구하는 대신, 모든 평가가 ‘일관된’ 답을 내라고 요구한다. 여기서 “일관된”은 정보량의 관점에서다. 답들이 꼭 같을 필요는 없지만 모순되어서는 안 된다. 메모리 고갈 같은 실패는 ⊥로 모델링한다. 정보 부분순서의 바닥(bottom)이기 때문에 “바텀”이라고 부른다. 아무 정보도 담지 않으므로 어떤 값과도 일관되며, 어떤 값과도 모순되지 않는다. 좀 더 정확히는, 값들이 어떤 공통 상계(정보 상한)를 가지면 ‘일관’하고, 가지지 못하면 ‘비일관’하다. 값 ⊥는 ‘모른다’는 뜻이고, 값 (1,⊥,3)은 (1, 모른다, 3)을 뜻한다. 이 “일관된 값” 원칙은 유한 자원 및 하드웨어 고장으로 인한 실패 가능성은 받아들이되, System.Info.os의 “linux” vs “darwin”, 또는 “2^32 == (0 ::Int)”의 False vs True 같은 일은 거부한다. 또한 System.Info.os :: IO String이라는 타입은 받아들이는데, 이는 내가 기대했던 타입이기도 하다. IO String의 의미는 동적 조건에 대한 의존을 담아낼 만큼 충분히 크기 때문이다.
만약 당신도 내가 위에서 말한 원칙들을 소중히 여긴다면, 당신의 생각을 꼭 듣고 싶다.
요즘 내가 소중히 여겨온 몇 가지 프로그래밍 원칙이 Haskell 동지들 사이에 널리 공유되지는 않는다는 사실을 배우고 있다. 적어도 내가 들어온 범위에서는. 이...
태그: purity, referential transparency, semantics|댓글 (RSS) |트랙백
들리는 여러 문제들 가운데 하나로, Haskell 커뮤니티가 모두가 모나드 얘기를 듣는 만큼이나 ‘코모나드(comonad)’에 대해서도 포교(?)해야 하는 것처럼 들립니다.
또 이렇게 바꿔 말하고 싶네요: “답들이 꼭 같을 필요는 없지만 [모순되어서는] 안 된다.” 제가 늘 틀리기 일쑤인 주제에 이런 잔소리를 하는 건 좀 우습다는 걸 알지만, 그래도 당신이 말하는 바와 당신이 ‘의도’하는 바를 구분하는 데 도움이 되는 작은 차이라고 생각해요.
좋아요, 모두 한숨 돌리고(사람당 한 번, 남의 한숨을 뺏지 마세요. 그럼 소용이 없잖아요…), 단체 포옹합시다. 음, “사람 만지는 건 질색”인 분들은 곰 인형이라도 꼭 안아주세요.
2009년 3월 30일 오후 12:30 2. #### Tracy Harms:
의미적 일관성은 내게 소중하다. 나는 계속해서 applicative/함수 수준 프로그래밍에 끌리곤 하는데, 그 방식이 추상에 집중할 수 있게 해주기 때문이다. 당신이 여기에서 주목하는 바는 바로 그 핵심인 듯하다.
우리는 동등성만으로는 완전히 의존할 수 없게 만드는 여러 사정 때문에, ‘일관성’을 보장함으로써 상황을 개선할 수 있다고 제안했다. 그 일은 다루기 가능하고 효과적일 것이다. 이 조언은 언어, 라이브러리, 애플리케이션 등 여러 층위에 적용할 수 있다.
무지, 부정확, 근사(近似)의 한계는 피할 수 없다. 지식의 이런 양상은 제거되거나 극복되거나 지워질 수 없다. 다만 어느 정도는 ‘관리’될 수 있다. 당신이 여기서 장려하는 것은, 실제 순수성의 “경계(edge)”가 우리가 추론할 수 있도록 드러나게 만드는 방식으로 순수성에 대한 감상을 적용하는 방법이라 생각한다. 대안은 언뜻 더 순수해 보일 수 있다. 눈에 보이는 경계가 없으니까. 하지만 사실상 결과에 영향을 주는 사실들을 숨기고 있기 때문에 순수성은 사라진다.
이렇게 수용하려면 구현을 끌어들이게 된다. 예컨대 정수 그 자체가 아닌 ‘유한 부분집합의 정수’ 표현으로 일한다는 것을 인정하게 되는 것이다. 하지만 결국 어디선가는 양보가 필요하다. 나는 구현되지 않은 계산의 환상을 버리고 의미의 일관성을 지키는 쪽을 택하겠다.
2009년 3월 30일 오후 12:33 3. #### Daniel Yokomizo:
나도 동의한다. Haskell의 최악의 기능 중 하나는 제대로 된 모듈 시스템이 없다는 점이다. 만약 ML의 모듈 시스템 같은 게 있었다면, System.Info를 우리 모듈의 파라미터로 모델링해서, 프로그램의 서로 다른 실행이 “os”에 대해 다른 결과를 갖게 할 수 있었을 것이다. 동일한 실행에서 두 번의 “os” 호출이 서로 다른 결과를 내게 허용하는 “os :: IO String”보다 더 낫다.
심지어 Int를 구현하는 모듈도 실행마다 달라져 다른 결과를 내게 할 수도 있고, 고정되어 일관적일 수도 있다. 어느 쪽을 선호할지 선택할 수 있었을 것이다.
2009년 3월 30일 오후 12:39 4. #### BMeph:
또, 아까 못 했던 말인데, “나도 그 원칙들을 소중히 여긴다.” Haskell에는 편의(exigency)를 위해 그 원칙을 배반하는 것들이 산더미만큼 있다고 믿는다. 그리고 그 배반에 대해 조건부로는 괜찮다고 생각하지만, 어딘가에는 그 배반이 있었고, 왜 그랬는지, 또 왜 가까운 시일 내에 해결책이 나오기 어려운지를 밝히는 성명이 있었으면 한다. 또한, 누군가가 그 편의상의 문제에 대한 해결책을 갖고 있다면 환영한다는 말도. 그 배반이 일어나고 있다는 사실을 모르는 누군가가, 문제가 없다고 믿도록 의도적으로 오도되어, 그 점을 지적하는 사람을 트집잡기 좋아하는 사람쯤으로 치부하는 상황은 정말 싫다.
편의적일 수는 있다. 하지만 무엇보다 정직해야 한다.
2009년 3월 30일 오후 12:48 5. #### conal:
BMeph — 코멘트 고마워요!
내 의도를 더 분명히 드러내준다는 점에서 “disagree(엇갈리다)”보다 “contradict(모순되다)”가 더 구체적이고 적절하다고 생각한다. 나도 처음에는 그 단어를 썼다가, 유혹적으로 그럴싸하지만 더 미묘하고(아마 더 모호하기도 한) 표현을 택하고 말았다.
편의 앞에서의 정직함에 대한 당신의 말이 정말 마음에 든다. 나도 동의한다 — 우리가 아직 그것을 살아내는 법을 모르는 동안에는 우리의 원칙을 ‘정직하게’, ‘명시적으로’ 위배(“배반”)하자. 문서에 “TODO” 주석과 다른 알림을 남겨서, 우리가 말하고 싶은 것과 우리가 말할 줄 아는 것의 차이를 우리도 기억하고 다른 이들도 알아볼 수 있게 하자. 그렇지 않으면, 당신이 지적했듯, 어떤 이들은 우리가 고치고 싶은 실수를 열정적으로 (잘못) 옹호하고 말 것이다.
그리고 그래요 — 단체 포옹(필요시 곰 인형 포함). ![]()
2009년 3월 30일 오후 1:08 6. #### Jake McArthur:
당신도 알다시피, 나도 동의한다.
나는 _|_가 오류에 대한 거의 완벽한 근사라고 본다. 이는 계산이 유용한 값으로 수렴하지 않을 것이라는 뜻인데, 오류란 대체로 그런 것이다. Haskell에는 이미 error가 있는데, 이는 근사하게 이름만 번지르르한 _|_일 뿐이라서, 이 개념은 이미 친숙하다.
하지만 “일관된”은 당신이 원하는 것을 묘사하기에 아주 좋은 단어는 아닌 것 같다. 제한 조건은 당신이 unamb의 의미를 설명하듯이 표현하는 게 낫겠다: “답은 _|_이 아닌 한 일치해야 한다.”
2009년 3월 30일 오후 1:08 7. #### solrize:
나는 오래전부터 Int는 그저 사악하다고 느껴왔고, Haskell은 이를 실수로 접근하지 않도록 특별한 라이브러리로 밀어내야 한다고 생각한다. Int 오버플로는 버퍼 오버플로처럼, 실제 배포된 애플리케이션에서 끔찍한 버그를 야기한다. 그리고 Haskell에서는 형 추론 때문에 이런 버그가 꽤 미묘하게 퍼질 수 있다. 즉, 나는 최근 정수 계산을 하는 Haskell 코드를 작성했는데 잘 동작했다(Haskell의 기본 수치 타입은 Integer다). 그런 다음 코드를 약간 바꿨는데 겉보기에는 계속 잘 작동했지만 사실은 틀린 답을 내고 있었다. 무슨 일이냐면, 내 변경이 어떤 표현에서 (xs !! n)을 사용했는데, 이는 n이 Integer가 아니라 Int여야 함을 의미했고, 그로 인해 다른 많은 표현이 형 추론을 통해 Int가 되었고, 고정밀 Integer가 필요한 코드의 먼 부분을 망가뜨렸다. 물론 더 많은 함수에 타입 주석을 썼다면 더 빨리 잡았을 테고, 그렇게 하려 노력 중이지만, 사실 자동 추론이야말로 Haskell이 Java를 이기는 이유 중 하나 아닌가.
Scheme, Common Lisp, Python 같은 다른 언어들은 기본으로 큰 정수(bigint)를 쓰고 있고, Haskell의 후속 언어는 그들과 보조를 맞춰야 한다고 본다.
2009년 3월 30일 오후 1:21 8. #### conal:
Daniel — IO는 과한 면이 있다고 나도 본다. (사실 IO는 ‘잡동사니(“sin bin”)’ 역할을 하는 만큼, ‘모든’ 용도에서 과할지 모른다.)
한편, 실행 중에 OS가 바뀌지 않는다는 것이 정말 ‘안전한’ 가정인지 질문하고 싶다. 머신 의존적 의미를 널리 받아들이는 풍토가 나를 불편하게 하는 이유 중 하나는, 해로울 수도 있고 아마 무의식적일 수도 있는 기저 가정이 있다고 의심하기 때문이다. 즉, 프로그램의 실행은 단일 머신에서, 단일 아키텍처와 단일 OS(비트 폭 포함)에서 일어난다는 가정이다. 많은 프로그래머가 이미 단일 실행에 둘 이상의 스레드, 둘 이상의 CPU 코어를 허용하는 것이 중요하다는 점을 알고 있다. 그리고 우리 Haskeller들은 함수형 프로그래밍 패러다임이 다중 스레드와 다중 코어를 품을 만큼 충분히 아름답다는 점을 자랑스러워한다. 이제 나는 한 걸음 더 나아가, 단일 함수형 프로그램의 실행이 여러 CPU, 여러 아키텍처와 여러 운영체제에 걸쳐 일어나는 상황을 예상하자고 제안한다. 물론 명령형 패러다임은 이런 시나리오에서 질식하겠지만, 함수형 패러다임은 — 우리가 스스로 불필요한 걸림돌을 만들지만 않는다면 — 우아하게 나아갈 수 있다.
2009년 3월 30일 오후 1:27 9. #### Patai Gergely:
나는 Int를 어떤 UnsafeInteger로 생각하는 데 아무 문제 없다. 결국 unsafePerformIO에도 정당한 용도가 있지 않은가. 이쯤 되면 계약의 문제 아닌가?
System.Info 묶음에 대해서는, IO로 감싸지 않을 만한 좋은 이유를 떠올릴 수가 없다. 런타임에서의 유일한 합리적 용도는 뭔가를 표시하는 것인데, 어차피 IO 영역에 있어야 하니까.
2009년 3월 30일 오후 1:28 10. #### BMeph:
@conal: 이걸 생각하다 보니, Int의 표현 — 그리고 그 연산 의미 — 가 64비트 머신에서 바뀐다면, Integer의 표현도 바뀐다는 뜻인가요?
2009년 3월 30일 오후 1:36 11. #### Matt Hellige:
나도 그 원칙들을 소중히 여긴다. 왜 그 원칙들이 어떤 경우에 타협되었는지도 이해한다. 하지만 게으름(따라서 순수성)에 대한 집착이 Haskell의 멋진 혁신 상당수를 동기(정확히는 ‘필요’)를 부여했듯, 의미적 순수성에 좀 더 엄격히 매달린다면 또 다른 혁신의 묶음이 발견될 수도 있다. 필요는, 그들이 말하듯, 발명의 어머니… 하지만 한 언어에서 너무 많은 걸 바라지는 말아야 할지도.
어쨌든, 당신의 예시들을 ‘필요악’이라 하더라도 악으로 보지 않는 사람은 매우 의심스럽다.
2009년 3월 30일 오후 1:37 12. #### conal:
내 마지막 댓글을 다른 식으로 표현하자면: 선언적 프로그램의 분산 실행은 Y2K만큼이나 분명히 다가오고 있다. 그러니 함수형 프로그래머들이 불행한(그리고 피할 수 있는) 가정 위에 프로그램을 짓는 부류에 들지 않기를 바란다.
2009년 3월 30일 오후 1:48 13. #### Raoul Duke:
이 문제에 대한 당신과 당신의 생각에 동의한다고 믿습니다. 이렇게 드러내 주셔서 고맙습니다.
2009년 3월 30일 오후 1:51 14. #### John Dorsey:
몇 달 전 cafe에서 이 문제에 대해 논의가 있었다. IEEE 타입[1], Int, System.Info 및 유사한 문제아들을 IO 모나드로 추방하는 것은 분명히 가능하다. 내 생각에, 그 경우 흔한 플랫폼에서 빠르고 네이티브이며 “순수한” float와 머신 크기 Int 연산을 더는 할 수 없게 되는, 실용성의 높은 비용이 따른다.
부끄럽고 더럽고 타협적인 대안 — 망설이며 선호하는 쪽 — 은 순수 세계에서 좀 더 복잡한 의미론을 인정하는 것이다.
구체적으로, 내 관점은 순수성과 참조 투명성의 보장은 제한된 범위 안에서 적용되며, 그 한계는 바로 이런 아키텍처 차이에서 생겨난다는 것이다. System.Info.os는 — 그 아키텍처에서는 — 항상 “darwin” 값을 갖는다. 이런 관점 아래에서는 System.Info를 “portable(이식 가능)”이라 부르는 데 아무 문제가 없다.
그나저나, 당신은 사례를 너무 약하게 말한 것 같다. Int 오버플로는 핵심 언어 차원의 문제 아닌가?
어쨌든, 이것은 ‘모든 비용을 무릅쓰고서라도 성공을 피하라’는 Haskell-연구-언어와, ‘현실 세계의 공학 도구’인 Haskell 사이의 자연스러운 긴장의 좋은 예다.
[1] Float는 아마도 줄여진 보장조차 깨뜨릴 것이다. Float에는 일종의 묵시적 “unsafe” 경고가 딸려온다.
2009년 3월 30일 오후 2:24 15. #### Robert Harper:
그렇고 말고요!
2009년 3월 30일 오후 2:36 16. #### Daniel Yokomizo:
@Conal: “os”의 의미론이 무엇인가 하는 질문은 아주 흥미롭다. 라이브러리는 완전히 미래를 대비할 수 없고, 우리의 표현에는 언제나 어떤 가정을 해야 한다. 나는 “osName :: OSInfo -> String”과 “os :: IO OSInfo”를 갖고 싶다. 매개변수화된 모듈을 사용하면 모듈 파라미터로 “osi :: OSInfo”를 둘 수 있고(즉, 모듈이 인스턴스화될 때의 정보를 고정하고 싶다면), 또는 프로그램의 적절한 위치에서 “os :: IO OSInfo”를 사용할 수 있다. 비슷한 문제가(예: 코어 수) 떠오르는데, 순수성에 대한 헌신이 있다면 잘못될 일이 없다. 즉 “os :: IO String”을 갖거나, IO 호출의 결과로부터 “os :: String”을 정의할 수 있는 어떤 바인딩이 있어야 한다.
2009년 3월 30일 오후 2:55 17. #### conal:
BMeph가 썼다:
@conal: Int의 표현 — 따라서 그 연산 의미 — 가 64비트 머신에서 바뀐다면, Integer의 표현도 바뀐다는 뜻인가요?
나는 Integer가 가능한 한(정확하게) 효율적으로 표현되기를 바란다 — 그러니 그렇다, 32비트 머신에서는 Int32를, 64비트 머신에서는 Int64를 기반으로 Integer 표현을 만들 것이라 기대한다. 여기서 중요한 점은 Integer의 ‘의미’가 동일하다는 것이다. 따라서 프로그램은 모든 플랫폼에서(서로 간에도, 정수 의미와도) 일관되게, 그리고 효율적으로 실행될 것이다. 서로 다른 플랫폼에 분산된 경우에도.
그래서 이 예시는 내가 말하는 ‘의미론적으로 표준인 라이브러리’를 보여준다. 구현은 달라도 의미는 같다. 반면, 현재 Int 상황은 구현도 다르고 의미도 다르다. 형태(인터페이스)만 표준이고 실질(의미)은 표준이 아니다.
2009년 3월 30일 오후 3:01 18. #### conal:
구체적으로, 내 관점은 순수성과 참조 투명성의 보장은 제한된 범위 안에서 적용되며, 그 한계는 바로 이런 아키텍처 차이에서 생겨난다는 것이다.
관점이라기보다 선택 아닐까. 다시 말해, 자기실현적 예언일 수 있다. 이 관점을 취한다면, 의미론적으로 지저분한 시스템을 만들 가능성이 높아진다. 실행 플랫폼의 다양성이 참조 투명성 상실을 ‘필연’으로 만든다고 보는가? 지금까지 내가 본 것은 사람들이 순수성(그리고 그 실용적 혜택)을 기꺼이 희생하고, 마치 필수인 양 정당화하는 태도뿐이다.
System.Info.os는 — 그 아키텍처에서는 — 항상 “darwin” 값을 갖는다. 이런 관점 아래에서는 System.Info를 “portable(이식 가능)”이라 부르는 데 아무 문제가 없다.
만약 프로그램이 실행 플랫폼마다 ‘일관되게 비일관적으로’ 행동한다면, 그것이 “이식 가능”인가? 정말 이해가 안 된다. 나는 “portable”을 “플랫폼 간에 일관되게 동작하는”으로 쓴다. 당신은 어떻게 쓰는가?
그나저나, 당신은 사례를 너무 약하게 말한 것 같다. Int 오버플로는 핵심 언어 차원의 문제 아닌가?
내 생각에 Int는 라이브러리에서 온다.
2009년 3월 30일 오후 3:18 19. #### Luke Palmer:
나는 최근 이 원칙들을 매우 진지하게 받아들이고 있다. 그래서 흥미로운 몇 가지 포인트를 적어본다.
최근 나는 여러 조합자 계산법에서 선(禪)을 맛봤다. 그곳에서는 모든 표현이 닫혀 있기 때문이다. 아직 조합자로 유창히 프로그래밍하지는 못하고, 그러고 싶지도 않지만, 람다는 문맥 민감성을 도입해 추론을 더 어렵게 만든다. 그래서 Dana의 핵심 계산은 이런 이유로 조합자 기반이다.
프로그램은 자원(메모리 + CPU)을 ‘지능적으로’ 관리하면서도 어디서나 의미적 일관성을 보장할 수 없다. 어떤 실패 처리기는 정보량에 대해 단조(monotone)가 아닐 것이기 때문이다. 예컨대 대화형 인터프리터를 구현한다고 하자. 사용자에게 임의의 표현을 평가할 수 있게 하되, 다중 사용자 환경 같은 이유로 메모리 상한을 두고 싶다. 적절한 검사를 삽입할 수 있도록 코드를 해석(interpret)해야 한다. 이는 많은 오버헤드처럼 보인다. (맥락에서 벗어난 이들을 위해, IO는 답이 아니다.) 이런 자원의 합성적 관리에 크게 관심이 있지만 아직 결론을 얻지 못했다.
마지막으로, 모든 타입/닫힌 값에 대한 의미를 주는 일은 실제로 파고들수록 까다롭다. 나는 의미를 플라톤주의적으로 생각한다. 예를 들어, Integer 타입의 표현은 ‘실제’ 정수를 지시한다. 하지만 플라톤주의는 도움이 되는 신화일 뿐이고, 실제로는 형식주의만 있을 뿐이다. 의미는 반드시 어떤 다른 체계에 상대적으로 정의되어야 한다. 사실, 다른 체계에서의 의미 정의는 상대적 일관성에 대한 증명이다. 그래서 어떤 의미에서는, 표현은 그것을 부호화할 만큼 강력하고 일관적인 각 체계마다 많은 의미를 갖는다. 괴델과 친구들 덕분에, 의미를 정의할 수 있는 궁극의 체계는 없다…
두서없어 미안하다. 아직 정제 중인 생각들이지만, 반쯤 익은 상태로라도 공유하지 않기엔 너무 흥분되는 주제였다.
2009년 3월 30일 오후 4:13 20. #### newsham:
실용적 고려는 어떤가? 어떤 플랫폼에서만 사용 가능한 Haskell 패키지가 있다. 그런 기능을 감추는 호환성 라이브러리를 만들고 싶다면, 차이를 적절히 패치하기 위해 내가 쓰는 시스템이 무엇인지 알아야 한다. 이 정보를 가져오기 위해 IO String을 쓰라고 하겠는가? 그리고 나서? 그것은 그 시점의 단일 실행 환경의 스냅샷을 줄 뿐이다. 그 정보에 기반해 이후 수행하는 실행은 다른 시점에서 이루어진다. 실행 환경이 동일하게 유지된다는 보장이 없다면 심각한 문제가 된다. 즉, OS 타입을 String으로 만드는 것보다 더 깊은 문제가 있다. 한 걸음 물러서서 보면, 당신이 정말로 요구하는 것은 이질적 분산 컴퓨팅을 위한 플랫폼이다. 이는 darwin, linux나 다른 어떤 것과도 다른, 그 자체로 고유한 플랫폼이다. 이 플랫폼의 구성 요소들은 코드를 서로 다른 시스템 사이로 이동시킬 때 생기는 효과를 감춰야 한다. 나는 바로 ‘그’ 계층이 OS 이름을 다뤄야 한다고 주장하겠다. 일단 플랫폼이 고정되면, OS 이름 같은 매개변수도 고정되고, 그것들은 그 호출에서 참된 상수일 수 있다(하지만 당신이 추구하는 것처럼 모든 호출에 대한 상수는 아니다).
2009년 3월 30일 오후 6:02 21. #### claus:
실행 중에 OS가 바뀌지 않는다는 것이 정말 안전한 가정인지 질문한다.
아, Haskell이 모바일이 되기를 얼마나 오래 기다렸는지. 유망한 출발이 있었지만 아직은 이루어지지 않았다(늘 그렇듯이, Clean이 이 점에서는 조금 앞서 있다..). 다음 자원과 메일링 리스트에도 관심이 있을 것이다:
Research in Theory for Mobile Processes http://www.it.uu.se/research/group/mobility
하지만 이런 초점을 두면, 당신의 탐구는 더 흥미로워진다. 더 이상 개별 머신에서 ‘[| |] :: Syntax -> Semantics’가 사실은 ‘[| |] :: Syntax -> Machine -> Semantics’라는 점을 걱정하는 것이 아니라, 단일 프로그램이 여러 플랫폼을 포괄할 때, 지배적인 ‘port :: Platform -> Syntax -> Semantics’(포팅 결과로 전통적 ‘[| |] :: Syntax -> Semantics’의 머신 특화 인스턴스가 생긴다)를 일관된 형태로 사용할 수 없다는 점을 걱정한다.
하지만 이 경우, os가 String인지 다른 열거형인지가 크게 중요하지 않다. 코드를 Platform -> Maybe Program으로 구조화할 수 있지만, 이는 우리의 코드가 특정 플랫폼에서 ‘돌아가느냐 마느냐’만 포착할 뿐, 당신이 처음 제기했던 의미의 차이는 포착하지 못한다. 그에 대해, 프로그래머의 어깨에 플랫폼 경계에 대한 인식을 지우고, x와 y 사이에서 데이터나 코드가 교환될 때마다 platform-x-world-view와 platform-y-world-view 사이의 변환 부담을 지게 하거나; 아니면 모든 x, y, ..에 대해 세계에 대한 공통 관점을 보장하려 시도할 수 있다.
이것이 우리를 다음으로 이끈다:
이제 나는 단일 함수형 프로그램의 실행이 여러 CPU, 여러 아키텍처와 여러 운영체제에 걸쳐 일어나는 상황을 예상하자고 제안한다. 물론 명령형 패러다임은 이런 시나리오에서 질식하겠지만, 함수형 패러다임은 — 우리가 스스로 불필요한 걸림돌을 만들지만 않는다면 — 우아하게 나아갈 수 있다.
Squeak을 살펴보라, http://www.squeak.org/About/
“Squeak의 부분집합으로 작성된 빠른 가상 머신. 지원 플랫폼에서 동일한 동작을 보장하기 쉽다.”
이는 당신이 찾는 종류의 기반처럼 보인다. 그리고 이는 Croquet을 가능케 했다, http://www.opencroquet.org/index.php/Overview
“Croquet은 여러 운영체제와 장치에서 깊이 있는 협업형 다중 사용자 온라인 애플리케이션을 만들고 배포하기 위한 강력한 오픈 소스 소프트웨어 개발 환경이다. Squeak에서 파생되었으며, 다중 사용자/다중 장치 간 통신, 협업, 자원 공유, 동기 계산을 지원하는 피어 기반 네트워크 아키텍처를 제공한다. Croquet을 사용하면 개발자가 강력하고 매우 협업적인 크로스 플랫폼 다중 사용자 2D/3D 애플리케이션과 시뮬레이션을 만들고 연결할 수 있다 — 매우 대규모이면서도 풍부한 기능을 가진 연계된 가상 환경의 분산 배포를 가능케 한다.”
이는 당신이 제안한 종류의 분산 프로그래밍 환경처럼 보인다.
우리는 함수형 프로그래머로서 따라잡아야 할 것이 좀 있는 듯하다..
2009년 3월 31일 오전 1:43 22. #### Jason Dusek:
@conal:
얼마 전, 바로 그 값 System.Info.os에 대해 리스트에 이메일을 쓴 적이 있다. 나도 이 사안이 좀 당혹스러웠기 때문이다. 같은 표현은 같은 값을 내야 하지 않는가? 하지만 정말 같은 표현이 아니다. 같은 텍스트 문자열일 뿐이다. 한 컴퓨터에서는 하나의 표현을, 다른 컴퓨터에서는 다른 표현을 이름한다. 요컨대, 내 Haskell 컴파일러는 다른 Haskell 컴파일러가 무엇을 하는지 모르고, 그 사실 덕분에 복된 순수의 상태를 유지한다.
이는 언어 의미론보다는 신뢰할 수 있는 플랫폼의 문제다. 언어 의미론은(현재로서는) 개별 컴퓨터의 개별 컴파일러에 의해 강제된다. 동일한(같은) 컴파일러 모두에게 같은 데이터를 먹여야만 같은 방식으로 컴파일하고 있음을 확신할 수 있다. 이는 신사 협정이나 일종의 인증 체계로 귀결된다. 나는 이런 장치들에 신뢰를 둘 수 없다.
댓글에서, 당신은 “이제 나는 단일 함수형 프로그램의 실행이 여러 CPU, 여러 아키텍처와 여러 운영체제에 걸쳐 일어나는 상황을 예상하자고 제안한다. 물론 명령형 패러다임은 이런 시나리오에서 질식하겠지만, 함수형 패러다임은 — 우리가 스스로 불필요한 걸림돌을 만들지만 않는다면 — 우아하게 나아갈 수 있다.”라고 썼다. 내가 이 시나리오를 상상하기 전에, 나는 그 모든 컴퓨터를 신뢰하는 상황을 먼저 상상해야 한다. 명령형 패러다임이 그 점에서 ‘질식’한다는 주장에 대해서는, 음 — 바로 이런 시스템을 다루기 위해 메시지 전달 시스템이 설계되었다. 분산 환경에서 우리를 넘어뜨리는 것은 공유 상태이지, 명령형 프로그래밍 그 자체가 아니다.
2009년 3월 31일 오전 2:24 23. #### Simon Marlow:
어느 쪽에 서야 할지 잘 모르겠지만, “문제”가 즉각 눈에 보이는 것보다 더 넓게 퍼져 있음을 지적하고 싶다. 이미 언급된 Int와 System.Info.os 외에 내가 찾은 예시는 다음과 같다:
* Float와 Double 타입, 그리고 따라서 floatRadix, floatDigits 등
* System.FilePath 전체: Windows와 Unix에서 서로 다르게 동작하는 비-IO 함수들로 가득
* Foreign.sizeof, Foreign.alignment, 그리고 Foreign.C.Types의 모든 것
시간이 다 되었지만, GHC와 함께 제공되는 라이브러리를 빠르게 훑다가 찾은 것이 이 정도다.
2009년 3월 31일 오전 7:59 24. #### conal:
@Jake,
“일관된”은 당신이 원하는 것을 묘사하기에 아주 좋은 단어는 아닌 것 같다. 제한 조건은 당신이 unamb의 의미를 설명하듯이 표현하는 게 낫겠다: “답은 _⊥_이 아닌 한 일치해야 한다.”
나는 lub의 의미(평탄 영역에서 unamb와 동치인 더 일반적 변종)를 원한다. 이는 내가 보기엔 정확히 “일관성(consistency)”, 즉 공통 상위 정보 경계를 갖는 것을 뜻한다. 예컨대 (3,⊥)와 (⊥,4)는 일관적이다.
2009년 3월 31일 오전 8:19 25. #### John Dorsey:
@conal: 선택/관점에 관하여. 나는 ‘물론’ 지저분한 의미론을 받아들일 용의가 있다. 나는 소프트웨어 엔지니어니까! 내가 가진 최고의 도구를 위한 최고의 서술적 의미론을 선호하는 것 말고 무엇을 하겠는가? 더 나은 의미론을 보이는 사용 가능한 시스템을 애타게 기다릴 뿐이다. 내가 제안한 “범위 제한 순수성(scoped purity)”에 치명적 결함이 있는가?
이식성에 관해, 나는 절대적 용어로 정의하려는 경향에 공감한다. 행위/의미/무엇이든 변화가 없도록. 그리고 당신이 어디에서 오는지 정말로(그리고 이미) 이해한다. 하지만 그것은 이식성에 대한 보편적 이해를 반영하지 않는다.
이식 가능한 코드는 호스트 환경 사이를 쉽게, 그리고 유용하게 옮길 수 있는 코드다. 당신의 “portable” 정의는 당신의 “pure” 정의와 비슷해 보인다. IO 코드를 비이식적이라고 보는가? 그것은 분명 서로 다른 플랫폼에서 일관되게 행동한다고 말할 수 없다. 덧붙여, System.Info를 IO로 밀어넣는 것은 내게 불편하지 않다. Int를 IO로 밀어넣는 생각에는 불편하다. 내 모델에서는 많은 코드가 유용하고 ‘순수’하지만, 당신의 모델에서는 그렇지 않을 것이다. 그것이 나의 실용적 정당화다.
Int에 관해, 오버플로 동작은 Haskell Report Part I(언어)의 섹션 6.4(사전 정의 타입과 클래스)에 정의되어 있다. 그 의미에서 핵심 언어의 문제다.
2009년 3월 31일 오전 8:22 26. #### conal:
@Jason:
같은 표현은 같은 값을 내야 하지 않는가? 하지만 정말 같은 표현이 아니다. 같은 텍스트 문자열일 뿐이다. 한 컴퓨터에서는 하나의 표현을, 다른 컴퓨터에서는 다른 표현을 이름한다.
당신에게 “표현(expression)”이 무엇을 뜻하는지 잘 모르겠다. 내게 파싱은 ‘함수’다. 같은 문자열로 시작하면 같은 표현을 얻는다. 다른 언어, 예컨대 Haskell과 Standard ML처럼 서로 다른 언어를 말하는 게 아니라면.
언어 의미론은(현재로서는) 개별 컴퓨터의 개별 컴파일러에 의해 강제된다. 동일한(같은) 컴파일러 모두에게 같은 데이터를 먹여야만 같은 방식으로 컴파일하고 있음을 확신할 수 있다.
아마 내가 고치자고 제안하는 바를 공정하게 묘사한 것 같다. 우리는 Haskell의 의미를 정확하고 구현과 무관하게 정의하지 않았기 때문에, 구현들이 서로 불일치할 수 있고 실제로 그렇다. 만약 Standard ML처럼 언어가 무엇을 의미하는지(즉, 형식뿐 아니라 실질로서의 “표준”)를 실제로 정의하고, “표준” 라이브러리들에 대해서도 같은 일을 한다면, 구현 불일치가 발견될 때 버그 리포트를 제출하고 구현이 고쳐질 것이다.
2009년 3월 31일 오전 8:30 27. #### conal:
@John:
물론 나는 지저분한 의미론을 받아들일 용의가 있다. 나는 소프트웨어 엔지니어니까! 내가 가진 최고의 도구를 위한 최고의 서술적 의미론을 선호하는 것 말고 무엇을 하겠는가? 더 나은 의미론을 보이는 사용 가능한 시스템을 애타게 기다릴 뿐이다.
알겠다. 우리는 지금 다른 역할을 하고 있다. 당신은 주어진 도구에 적응하고 있고, 나는 우리의 실천을 이론에 더 맞춰 바꾸자고 제안하는 중이다.
내가 제안한 “범위 제한 순수성(scoped purity)”에 치명적 결함이 있는가?
당신의 현재 역할에는 없다. 내 역할에는 있다.
이식 가능한 코드는 호스트 환경 사이를 쉽게, 그리고 유용하게 옮길 수 있는 코드다. 당신의 “portable” 정의는 당신의 “pure” 정의와 비슷해 보인다. IO 코드를 비이식적이라고 보는가? 그것은 분명 서로 다른 플랫폼에서 일관되게 행동한다고 말할 수 없다.
나는 ‘지시 의미(denotation)’를 말하고 있다. IO는 일관될 수 있다. IO ‘그 자체’의 지시 의미에는 이미 “실행되는 동적 조건”에 대한 접근이 포함되어 있기 때문이다. 반대로, String과 Bool 같은 타입들의 의미론은 훨씬 단순하길 바란다. 차이를 느끼는가?
나는 ‘순수’ 타입들의 지시적 단순함이야말로 함수형 프로그래밍이 유용한 수학적 성질을 갖고 잘 합성되며(“등식적 추론에 좋다”)하는 이유의 핵심이라고 믿는다. 그리고 명령형 프로그래밍(Haskell의 IO 등)의 상대적 지시적 복잡성은 명령형 프로그래밍이 이런 성질을 결여하는 바로 그 이유다. Backus가 튜링상 강연에서 논한 그대로.
그리고 당신이 어디에서 오는지 정말로(그리고 이미) 이해한다. 하지만 그것은 이식성에 대한 보편적 이해를 반영하지 않는다.
Haskeller들이 자신의 모듈에 “portable” 도장을 찍을 때, 단지 “코드가 호스트 환경 간에 쉽게, 유용하게 옮겨진다” 이상을 뜻하길 바란다. 방금 #haskell에서 빠르게 설문해봤는데, 여러 해석이 있었다. 어떤 이는 의미적 일관성을 뜻하고, 어떤 이는 특정 맥락에서 라이브러리가 ‘작동하느냐’를 뜻했다. 어떤 이는 컴파일러 간, 어떤 이는 실행 플랫폼 간을 뜻했다. 아마 “portable” 라벨에 대한 더 많은 해석과 의도가 들려오길 기대한다.
2009년 3월 31일 오전 9:03 28. #### Duncan:
모듈을 펑터로 보는 설명이 의미론 문제에 대한 좋은 해법이라고 생각한다. 이는 System.Info.os :: String의 의미를 설명해준다. 그렇다고 만족스럽다는 뜻은 아니다. 여전히 모듈 이름을 모듈 의미로 매핑하는 환경이 있다는 문제가 남아 있다.
GHC에서는 이 환경이 ghc-pkg 데이터베이스에 보관된다. 최종 사용자는 그 매핑을 바꿀 수 있다(서로 다른 패키지를 설치함으로써) — 즉, 서로 다른 모듈을 임포트로 합성함으로써 당신의 코드의 의미를 바꿀 수 있다. 이 매핑은 기본적으로 시스템 의존적이기도 하다. Windows에서는 System.Info 모듈 이름에 대한 모듈 의미가 Linux에서와 다르다.
나는 이 모듈/펑터 설명이 Bool의 지시 의미가 사실은 MachineInfo -> Bool이라는 식의 설명보다 훨씬 낫다고 본다. 또한, 당신의 닫힌 표현은 고정된 지시 의미를 갖게 된다. 하지만 사실, 닫힌 표현을 갖는 경우는 매우 드물다. 프로그램에서 임포트한 모든 함수와 타입은 실제로는 자유 변수이며, 최종 사용자의 패키지 매니저가 패키지/모듈로부터 (닫힌) 프로그램을 합성할 때 그 사용자의 머신에서 치환된다. 이는 어떤 표현의 지시 의미를 고립적으로 기술하는 일을 어렵게 만든다.
실무적으로, 같은 모듈 이름 아래에 시스템 의존적 모듈 구현이 살지 않도록 노력해야 한다. 하지만 완전히 추방하기는 어렵다. 서로 같은 모듈 이름을 내보내는 새 패키지 버전을 사람들이 쓰는 일을 막을 수는 없다. 이 예는 물론, 서로 다른 시스템 간에 Prelude가 다른 것보다는 덜 나쁘다. 하지만 두 예는 같은 연속체 위의 사례다.
2009년 3월 31일 오전 9:06 29. #### Carlton Mills:
나는 진공관 IBM 650에서 프로그래밍을 시작했지만, Haskell에서는 완전한 뉴비다. 그러니 무지한 자만이 가질 수 있는 절대적 확신으로 말하겠다.
“모든 비용을 무릅쓰고서라도 성공을 피하라”는 말이 “유용함을 피하라”는 뜻인가? 그게 문제다. “2^32 == (0 ::Int)”를 구현을 확인하지도 않고 쓰는 사람은 어떤 유용한 것도 프로그래밍하게 해서는 안 된다. 하지만 “2^30 == (0 ::Int)”는 괜찮다고 가정할 수 있어야 한다(가끔 컴퓨터 과학자는 상위 비트로 장난을 치니까). 이상적으로, 모든 플랫폼에서 모든 시간에 똑같이 동작하는 라이브러리 인터페이스가 있어야 한다. 하지만 라이브러리 작성자는 플랫폼 고유의 원시 연산과 제어 흐름을 사용해야 할 것이다. 그런 라이브러리를 Haskell로 쓰고 싶은가? 그렇다면 Haskell 코드가 플랫폼 구체를 알아야 하는 일이 필요해질 것이다. 소프트웨어 공학은 계산 가능한 객체가 무엇인지, 그것을 어떻게 다루는지에 대해 합의된 이해를 만들어가는 오랜 과정이었다. 이 과정은 진행 중이다. Int32와 Int64 타입이 무엇인지는(궁극적으로는 Intel이 말하는 바가 무엇인지) 합의가 있다. 그리고 Int 타입은 적어도 2^14 값을 담는다는 합의도 있다. 한때 String은 [Char]라는 합의가 있었지만 이제는 아니다. 국제 알파벳과 관례가 다양해지면서 문자열 조작은 매우 주의해서 접근해야 한다. 지금 그대로 10년 뒤에도 동작할 문자열 함수는 가장 사소한 것들뿐일 것이다. String 타입은 네버랜드의 무정형 개념이다. 그게 현재 현실이다.
Haskell 타입 시스템에는 모든 플랫폼에서 모든 시간에 똑같이 동작할 타입도 있고, 현재의 합의에 대응하는 타입도 있다(예: Int). 범주론자나 타입 이론가가 아니라 소프트웨어 엔지니어로서 내 눈에는, Haskell은 앞으로 점점 더 중요하고 유용해질 것 같다. 나는 ‘모든 시간에 똑같이 동작하는’ 타입(일종의 알고리즘적 순수성)도 만들 수 있고, 플랫폼 구체를 통합한 타입도 만들 수 있다. 그리고 둘을 분리할 수 있다. Haskell은 실용적 순수성이다.
2009년 3월 31일 오전 9:18 30. #### sclv:
@Conal:
Int 문제에는 전적으로 동의한다 — 효율이 중요할 때 머신 특정 표현이 있어야 하는 건 당연하지만, 나머지 99%의 경우, 그리고 코어 라이브러리, 프렐류드 등에서는 보장된 건전한 표현을 쓰는 편이 낫다. 지금은 대부분에게 문제가 아닐지라도 언젠가는 반드시 버그의 원천이 될 것이다.
Float와 Double은 이미 마법의 영역이라 봐서, 거의 봐주고 싶다. 다만, IEEE 없는, 제한된 단일 부동 타입도 나쁘지는 않을 것이다.
FilePath 라이브러리에 대해서는, 파일 경로를 애초에 추상 데이터 타입으로 일관되게 다루는 게 낫다고 본다 — 문자열 함수를 제공하는 것 자체가 나쁜 코드를 부추긴다.
2009년 3월 31일 오전 10:09 31. #### sclv:
덧붙이자면, 아마 틀릴 수도 있지만, 모든 현실의 프로그래밍 언어에서 “참조 투명하다”는 것은 “불타고 있다(on fire)”보다 “맛있다(tasty)”에 가깝다고 느낀다 — 즉, 이분법적 명제가 아니라 ‘정도의 문제’라는 뜻이다.
2009년 3월 31일 오전 10:34 32. #### conal:
@sclv:
Int 문제에는 전적으로 동의한다 — 효율이 중요할 때 머신 특정 표현이 있어야 하는 건 당연하지만, 나머지 99%의 경우, 그리고 코어 라이브러리, 프렐류드 등에서는 보장된 건전한 표현을 쓰는 편이 낫다. 지금은 대부분에게 문제가 아닐지라도 언젠가는 반드시 버그의 원천이 될 것이다.
고맙다. 나는 한 술 더 떠서 제안한다: 관찰 가능한 의미가 온전히 일관적이기만 하다면, 빠른 구현을 위해 다양한 표현을 마음껏 쓰자. 예컨대 32비트 플랫폼에서는 Int32로, 64비트 플랫폼에서는 Int64로 Integer 표현을 구성하고, 두 Integer 구현이 지시 의미적으로 동일함을 보장하자.
Float와 Double은 이미 마법의 영역이라 봐서, 거의 봐주고 싶다. 다만, IEEE 없는, 제한된 단일 부동 타입도 나쁘지는 않을 것이다.
FilePath 라이브러리에 대해서는, 파일 경로를 애초에 추상 데이터 타입으로 일관되게 다루는 게 낫다고 본다 — 문자열 함수를 제공하는 것 자체가 나쁜 코드를 부추긴다.
나도 동의한다. 내가 이해하기로, 서로 다른 FilePath 구현의 요점은 바로 의미적 공통성에 도달하는 것이다. 기저 문자열 표현이 동일해야 한다는 뜻이 아니라, 파일 시스템 연산의 관점에서 의미적으로 일관된 값들을 지시해야 한다는 뜻이다. 또는 네가 더 간결히 말한 대로, 데이터 추상을 쓰자.
2009년 3월 31일 오전 11:21 33. #### conal:
@Duncan: 코멘트 고맙다. 나는 모듈의 서로 다른 ‘구현’에 대한 매개변수화와, 서로 다른 ‘의미’에 대한 매개변수화를 구분하고 싶다. 의미적 일관성의 원칙이 있으면, “(2 + 3) :: Integer” 같은 표현을(의미론적으로) 닫힌 것으로 볼 수 있다. 2, 3, fromInteger(리터럴의 설탕 제거에서), (+), Integer의 임의의 가능한 ‘의미’에 의해 매개변수화된 것으로 보는 대신에 말이다. 이런 방식으로, 모듈 구현은 따라야 할 서명뿐 아니라 그 서명의 ‘의미’도 부여받는다. 구현이 올바른 것은 서명 ‘그리고’ 그 의미를 만족할 때다. 형태뿐 아니라 실질도.
2009년 3월 31일 오전 11:49 34. #### Duncan:
물론 동일한 의미를 제공하는 서로 다른 모듈 구현을 가질 수 있고, 실제로 그렇게 할 때 보통 아주 타당한 이유가 있으며 삶이 더 쉬워진다 등등.
내 요지는, 아마, 모듈에 대한 설명이 모듈 이름이 서로 다른 모듈 의미로 매핑되는 경우와, 모듈 이름이 항상 같은 모듈 의미로 매핑되는 경우를 모두 설명한다는 것이다. 그다음, 서로 다른 환경에서 모듈 이름이 같은/다른 의미로 매핑되는 것이 더 중요하거나 덜 중요한, 연속체가 있다. 프리루드 같은 코어 모듈 이름이 항상 같은 의미로 매핑되는 것은 분명 도움이 된다 — “(2 + 3) :: Integer”를 마치 닫힌 것처럼 다룰 수 있게 해 준다. Int에 대한 이의는 바로 거기서 나온다. 다른 경우에는 모듈 이름에 연관된 의미를 바꾸는 게 매우 유용하다(프로그램을 바꾸지 않고 라이브러리의 버그를 고칠 수 있게 해 준다).
특정 모듈 이름에 대해 고정된/유연한 의미를 가질지에 관한 세부 사항에는 합리적 사람이 서로 다르게 판단할 많은 지점이 있겠지만, 내 핵심 요지는 우리가 처음 걱정했던 것처럼 큰 의미론적 구멍이 있는 것은 아니라는 것이다.
세부로 들어가면, 나는 일반적으로 당신에게 동의한다. 나는 의미적 일관성을 선호한다. FilePath가 플랫폼마다 다른 것이 최선의 해법인지는 확신하지 못하겠다. (ADT로 간주했을 때) 일관성에 대한 어떤 근사치를 달성하려는 것이긴 하지만, (문자열 표현을 보지 않더라도) 운영체제마다 FilePath의 의미가 실제로 다르기 때문에 불가피한 차이가 생긴다. ML에서 하듯, 파일 시스템 처리를 진정으로 FilePath 모듈에 대해 매개변수화하는 편이 더 좋다. 지금 어떤 OS와 대화 중인지를 알려주는 IO 함수에 기반해 사용할 모듈을 선택할 수 있다.
Int에 관해, 안타깝게도 일부 비교적 저수준 순수 코드는 Integer를 쓸 때보다 Int를 쓸 때 한 자릿수(10배) 가까이 더 빠르게 실행되며, 이를 개선할 뚜렷한 방법이 없다. 그렇지 않았다면 우리는 당연히 Int를 추방할 수 있었을 것이다. 지금처럼 유지되는 한, Int 또는 동등한 것을 제공하는 것은 필수적으로 보인다. Int가 일반적으로 과도히 사용되는지는 다른 문제다. 성능이 필요하고 더 까다로운 의미론을 감당할 준비가 되어 있는 경우에만 사용하는 모듈에 Int를 두고, 프렐류드에서는 빼자는 주장은 분명 합리적이다.
2009년 3월 31일 오후 1:19 35. #### Jason Dusek:
@conal
내게 파싱은 함수다. 같은 문자열로 시작하면 같은 표현을 얻는다.
실제로는, (컴파일러, 문자열)에서 값으로의 함수다. 다중 버전 정책 — 예컨대 파일 헤더에 버전 pragma가 있어야 한다는 정책 — 을 채택한다면, 언어 정의를 점진적으로 발전시키는 동안 예전 모듈의 의미를 ‘조용히’ 바꾸지 않을 수 있다.
2009년 4월 1일 오전 4:30 36. #### conal:
Conal이 썼다
내게 파싱은 함수다. 같은 문자열로 시작하면 같은 표현을 얻는다. 다른 언어, 예컨대 Haskell과 Standard ML처럼 서로 다른 언어를 말하는 게 아니라면.
Jason이 답했다
실제로는 (컴파일러, 문자열)에서 값으로의 함수다. 다중 버전 정책 — 예컨대 파일 헤더에 버전 pragma가 있어야 한다는 정책 — 을 채택한다면, 언어 정의를 점진적으로 발전시키는 동안 예전 모듈의 의미를 ‘조용히’ 바꾸지 않을 수 있다.
당신이 파싱을 ‘컴파일러’ 의존적이라고 말할 때, 정말 ‘언어(버전)’ 의존적이라는 뜻인가? 그렇다면 우리는 합의에 있다. 아니라면, 당신과 나는 서로 다른 개념적 틀에서 작업 중일 수 있고, 같은 세계에 대한 의견 차이가 아니라 다른 세계에 대해 말하고 있을 수 있다.
분명히 하자면, 내가 작업 중인 틀에서 ‘언어’는 언어의 ‘구현’과는 구별된다. 언어는 구체/표면 문법(문자열), 추상 문법(트리), (지시적) 의미론(수학적 값)을 가진다. 언어의 구현은 (적어도 구현과 일관된) (구현과 무관한) 의미론과 일치할 때 ‘정확’하다. 서로 비슷한 언어의 계열이 있을 수 있는데, 이를 “버전”이라 부를 수 있다. 그래도, 그 계열의 각 구성원은 그 구성원의 (언어 버전의) 구현과는 구별된다.
2009년 4월 1일 오전 6:54 37. #### wren ng thornton:
나는 이 가치들을 믿는다. Haskell에 왔을 때 이들을 진지하게 받아들였고, 그 진지함은 그 이후로 단조롭게 증가해 왔으며(최근에는 기하급수적으로).
내가 #haskell 채팅방에서 내 혼란과 실망을 이야기했을 때, 누군가가 이런 의미 차이를 서로 다른 라이브러리, 그리고 그에 따라 서로 다른 프로그램으로 설명하자고 제안했다
이런 의미 차이가 ‘설명이 필요하다’는 생각 자체가 질문을 구걸하는 일이라 본다. 의미론은 가능한 한 증명 무관적(proof-irrelevant)이어야 한다. 증명 무관성은, 의미론을 실제 구현과 충분히 분리해 정의하여 의미가 진정으로 지시적(denotational)이라고 말할 수 있음을 뜻한다.
나도 Haskell에는 편의를 위해 원칙을 배반하는 것들이 산더미만큼 있다고 믿는다. 그리고 그 배반에 대해 조건부로는 괜찮다고 생각하지만, 어딘가에는 그 배반이 있었고, 왜 그랬는지, 또 왜 가까운 시일 내에 해결책이 나오기 어려운지를 밝히는 성명이 있었으면 한다.
+1.
우리가 순수성 주장에 충실하지 못한 점을 한탄하지만 — 가차 없이 솔직해지자면 — 그 느슨함이 Haskell의 장점 중 일부이기도 하다. 다른 “정확한” 언어에는 없는 일종의 ‘뚝딱 만들 수 있는 능력(whipuptitude)’이 있다. 내가 가장 원하는 것은 이 단서를 없애는 것이다. 시체들이 어디 묻혀 있는지(그리고 왜인지)를 보여주는 지도가 첫걸음이다.
최근 내가 많이 생각해온 아이디어 하나가 있는데, 다른 사람들에게 설명하기 어려웠다. 하나의 언어를 정의하는 대신, 코드가 허용한 불완전성(infelicity)에 따라 구분되는 하위 언어들의 가족을 정의하자는 것이다. 즉, 코드의 의미론적 순수성은 항상 인식될 수 있어야 하며(그리고 컴파일러 플래그로 강제될 수 있어야 하며), 더 큰 하위 언어에서는 불순물이 명시적으로 금지되지 않을 수도 있다. (이는 값의 모나딕 순수성이나 언어 확장을 통한 언어 가족과는 직교한다.) 완전 함수형 언어가 핵심에 있고, ⊥의 도입이 첫 확장이다. IEEE-754 같은 다른 불완전성과 ⊥이 아닌 예외 값의 존재는 훨씬 위쪽 계층에 있을 것이다. 죄수함(sin-bin) 모나드는 최상위 수준에만 존재할 것이다(따라서 seq나 메모리 지역 같은 IO의 더 건전한 부분은 분리되어, 의미 일관성의 개념을 완전히 버리지 않는 코드에서도 사용할 수 있다). 이렇게 하면, 고객의 목을 죄지 않으면서도 우리가 감히 할 수 있는 만큼 구속대를 조일 수 있다.
뚝딱 능력을 포기하는 것은 큰 대가이고, 어떤 애플리케이션에서는 정당성의 부담이 지나칠 수 있다. 하지만 라이브러리, 특히 코어 라이브러리, 컴파일러, 운영체제에서는 우리가 짊어져야 할 부담이다. 해커도 v&v(검증/검증) 사람들도 각자 방식으로 옳지만, 동시에 각자 방식으로 틀리다: 이분법적 선택이 아니다. 우리는 프로그램의 일부에 대한 강한 보장을 주되, 전체의 완전성을 요구하지 않는(요청받지 않는 한) 하이브리드 체계를 필요로 한다.
2009년 4월 1일 오후 8:49 38. #### Jason Dusek:
Conal이 썼다:
당신이 파싱을 ‘컴파일러’ 의존적이라고 말할 때, 정말 ‘언어(버전)’ 의존적이라는 뜻인가?
네, 그 뜻이다.
…내가 작업 중인 틀에서 ‘언어’는 언어의 ‘구현’과는 구별된다.
실제로는, 컴파일러 개발자가 언어를 발전시키는 경향이 있다. 현재 GHC가 취하는 접근 — 그런 발전을 pragma로 표시하는 것 — 은 라이브러리 묶음에도 합리적으로 확장될 수 있다. (예컨대 곧 나올 “Haskell platform”의 변종들이 그렇게 표시될 수 있다.)
당신이 글에서 지적했듯, 실질적으로는 표준 라이브러리들이 언어 의미론의 큰 부분을 이룬다. 언어 부분집합을 식별하는 훈련되고 철저한 접근은, 당신이 상상하는 범위의 표준화를 위한 기반을 놓아줄 것이며, Haskell이 C++이나 FORTRAN처럼 한심한 상태에 갇히지 않도록 해 줄 것이다.
나는 Data.ByteString.ByteString이라는 문자열이 언제나 같은 것을 뜻하리라 기대하는 것이 합리적이라고 보지 않는다. 하지만, 라이브러리 버전을 지정하는 것처럼 지나치게 미세하고 좁은 방식이 아닌, 다른 용법들을 식별할 방법을 갖는 것은 매우 합리적이라고 본다. “플랫폼의 대수”가 필요하다.
2009년 4월 1일 오후 9:19 39. #### Improved Means For Achieving Deteriorated Ends / All Over Everywhere:
[…] Haskell과 관련해, Conal Elliott의 블로그에서는 이식성이 의미론 측면에서 무엇을 뜻하는지에 대한 아주 좋은 논의가 있었다. GHC 6.10.2가 릴리스되었고, Haskell-Platform 메일링 리스트는 자원봉사자를 모집했다 […]
2009년 4월 2일 오후 12:32 40. #### Jason Dusek:
wren ng thornton이 썼다:
최근 내가 많이 생각해온 아이디어 하나가 있는데, 다른 사람들에게 설명하기 어려웠다. 하나의 언어를 정의하는 대신, 코드가 허용한 불완전성에 따라 구분되는 하위 언어들의 가족을 정의하자는 것이다.
+1
2009년 4월 2일 오후 7:34 41. #### Paul Chiusano:
Conal, Haskell 프로그램 안에서 OS 이름 같은 정보를 접근하는 당신의 선호 방식은 무엇인가? 반환 타입을 IO String으로 바꾸는 것은 아무 것도 고치지 않는 것처럼 보인다 — 당신의 정의에 따르면 여전히 참조 투명하지 않다… 아니면 참조 투명한가? (그렇다고 주장한다면, 설명해 달라…)
2009년 4월 12일 오후 1:00 42. #### conal:
안녕하세요 Paul. 당신의 질문에 대한 답은 내 글의 두 번째 원칙에서 나온다. 후보 타입마다, 당신이 집어넣고 싶은 것을 담을 공간이 그 의미(지시 의미)에 있는지를 자문하라. “예”라고 답할 수 있는, 의미적으로 가장 단순한 타입을 골라라. 타입의 의미는 머신이나 구현과 아무 상관이 없음을 기억하라. 수학이다.
특히 참조 투명성에 관해서는, #haskell에서 설문해 보니 사람마다 다른 개념을 갖고 있었고, 어떤 이들은 Haskell이 참조 투명하다고는 확신하면서도 구체적 개념은 없었다. 당신은 어떤 의미를 염두에 두고 있는가?
2009년 4월 12일 오후 7:35 43. #### nolrai:
그 값들은 ‘어디서 실행되느냐’에 따라 달라지는 건가요, 아니면 ‘무엇을 위해 컴파일되느냐’에 따라 달라지는 건가요? (만약 두 번째라면, 내게는 괜찮아 보입니다. 그 느낌이 틀렸나요?)
Conal, Paul에게 한 당신의 답은 내게는 별로 이해가 되지 않습니다.
OS 이름의 타입은 무엇이어야 하죠?
2009년 4월 21일 오후 4:47 44. #### conal:
Conal, Paul에게 한 당신의 답은 내게는 별로 이해가 되지 않습니다.
안녕하세요 Nolrai,
Paul에게 답하면서, 내 의도는 ‘깨우침’을 주려는 것이 아니라 Paul과 당신, 그리고 다른 독자들이 스스로 깨우침을 찾을 수 있는 길로 이끄는 것이었다. 과정을 따라 스스로 답을 얻는다면, 내가 여기에 적은 말을 내가 직접 풀어 적는 것보다 더 깊게(따라서 더 전이 가능하게) 이해할 것이라 기대한다. (숙제를 ‘읽는 것’만으로는 충분치 않다. 실제로 ‘해야’ 한다.)
그 값들은 ‘어디서 실행되느냐’에 따라 달라지는 건가요, 아니면 ‘무엇을 위해 컴파일되느냐’에 따라 달라지는 건가요? (만약 두 번째라면, 내게는 괜찮아 보입니다. 그 느낌이 틀렸나요?) […] OS 이름의 타입은 무엇이어야 하죠?
다시 말하지만, 답은 내 글의 두 번째 원칙에서 나온다. 가장 중요한 단계는 정확한 질문에 도달하는 것이다(이 맥락에서 “괜찮다(fine)”와 “~해야 한다(should)”를 정의/대체하라). 그러면 당신은 명확한(따라서 의견에서 자유로운) 답에 거의 다가가게 될 것이다.
내가 여기서 한 말 가운데 이해되지 않는 부분이 있다면, 그 부분에 대해 명확화를 요청해도 좋다.
2009년 4월 21일 오후 9:09 45. #### Paul Chiusano:
Conal,
RT에 대해 당신은 이렇게 말했다: “닫힌 표현(자유 변수를 포함하지 않는 표현)의 값은 오롯이 그 표현 자체에만 의존하며 — 실행되는 동적 조건에 의해 영향을 받지 않는다.”
이 정의에 따르면, getStr :: IO String조차 RT가 아니다. 그 값은 실행되는 동적 조건에 분명히 의존하기 때문이다. System.Info.os를 IO String으로 바꾼다 해도 마찬가지다.
내가 한 가지 생각한 것은, Haskell 프로그램은 완전히 닫혀 있지 않다고 생각하는 편이 낫다는 것이다 — 즉, Haskell 프로그램을 특정 머신에서 실행할 때만 바인딩되는 어떤 자유 변수가 있다. 예를 들면, Int 산술의 세부 사항을 지정하는 함수들을 Haskell 프로그램에서의 자유 변수로 볼 수 있다. 우리가 Haskell 프로그램을 특정 시스템에서 실행하기로 결정할 때, 우리가 실제로 하는 일은 이런 자유 변수들에 값을 공급하고, 그 결과로 얻어지는 닫힌 표현을 실행하는 것이다.
2009년 6월 18일 오후 12:16 46. #### conal:
안녕하세요 Paul,
RT에 대해 당신은 이렇게 말했다: “닫힌 표현(자유 변수를 포함하지 않는 표현)의 값은 오롯이 그 표현 자체에만 의존하며 — 실행되는 동적 조건에 의해 영향을 받지 않는다.”
이 정의에 따르면,
getStr :: IO String조차 RT가 아니다. 그 값은 실행되는 동적 조건에 분명히 의존하기 때문이다. System.Info.os를 IO String으로 바꾼다 해도 마찬가지다.
getStr의 ‘값’이 동적 조건에 의존하는가? 어떻게 알 수 있나? 한 IO 값(액션)이 다른 IO 값과 같거나 다르다는 말을 할 때, 무엇을 의미하는가?
내가 한 가지 생각한 것은, Haskell 프로그램은 완전히 닫혀 있지 않다고 생각하는 편이 낫다는 것이다 — 즉, Haskell 프로그램을 특정 머신에서 실행할 때만 바인딩되는 어떤 자유 변수가 있다. 예를 들면, Int 산술의 세부 사항을 지정하는 함수들을 Haskell 프로그램에서의 자유 변수로 볼 수 있다. 우리가 Haskell 프로그램을 특정 시스템에서 실행하기로 결정할 때, 우리가 실제로 하는 일은 이런 자유 변수들에 값을 공급하고, 그 결과로 얻어지는 닫힌 표현을 실행하는 것이다.
이 제안에는 메스꺼운 느낌이 든다. 의미론 모델에 임의성이 스며드는 게 걱정된다. 왜 Haskell 코드를 특정 ‘머신’에 할당할 때 변수를 바인딩하는가? 왜 스레드나 코어가 바뀔 때마다 바인딩하지 않는가? 또는 시간이 바뀔 때? 내 코드의 의미가 매 시간 정각마다 바뀔 수도 있다. 아니면 순간순간, 일관되게 비RT인 언어들처럼. 게다가 앞서 댓글에서 언급했듯, 분산 실행을 생각해 보라.
나는 지능적으로 일시적인 분산 실행이 표준이 될 것이라 정말로 믿는다. 그리고 함수형 언어는 — 우리가 이미 한, 그리고 앞으로 하게 될, 의미론적으로 경솔한 선택들을 고치지 않는 한 — 준비될 수 있다.
게다가, 함수형 언어에는 동적 정보에 의존하는 값을 표현하는 우아하고 의미론적으로 건전한 방법이 이미 있다: 함수다. OS, 머신 비트 폭, 시각 등에 의존하는 코드를 원한다면, 그런 타입의 인자를 취하는 함수를 정의하는 편이 훨씬 더 편안하다.
2009년 6월 18일 오후 1:26 47. #### Paul Chiusano:
Conal,
당신이 이 댓글 스레드에서 소크라테스식 방법을 쓰기보다, 당신의 정의에 따르면 왜 getStr이 여전히 RT인지에 대한 주장을 그냥 제시해 주면 더 유용할 것이다고 생각한다. ![]()
내가 왜 getStr(그리고 부수효과가 있는 다른 함수들)이 RT라고 생각하는지에 대한 주장을 해 보겠다. 내 정의는 좀 더 제한적이다: 내게 RT 함수란, 그에 대한 어떤 호출이든 그 호출의 결과로 대체해도 프로그램의 의미에 영향을 주지 않는 성질을 가진 함수다. 나는 이것이 함수를, 그 모든 행위가 그 반환 값에 반영되어야 한다는 것을 뜻한다고 본다 — 프로그램의 나머지로 정보를 전파할 다른 방법이 없어야 한다. 이는 우리가 단순 치환으로 프로그램 행위를 추론할 수 있음을 뜻한다 — 각 함수 호출을 그 호출이 가리키는 값으로 대체하면 된다.
Haskell은 IO를 수행하는 모든 함수에 단일 IO 값을 스레딩하도록 강제하기 때문에, IO 타입의 값들이 실제로 부수효과를 갖는다는 것을 프로그램이 감지할 방법이 없다. 나는 IO를 수행하는 함수가 “World” 객체를 반환하고, 그 객체가 다음 IO를 수행하는 함수의 인자로 전달되어, 그 함수가 새로운 World 객체를 반환하고, … 하는 그림을 상상한다. 우리는 동적으로 선택되는 IO 값들을, 사실은 프로그램을 실행하기도 전에 미리 선택된 것이라고 가장할 수 있다.
2009년 6월 21일 오전 9:36 48. #### conal:
Paul Chiusano가 썼다:
당신이 이 댓글 스레드에서 소크라테스식 방법을 쓰기보다, 당신의 정의에 따르면 왜 getStr이 여전히 RT인지에 대한 주장을 그냥 제시해 주면 더 유용할 것이다고 생각한다.
먼저, 여기서 나는 일종의 미끼-바꾸기(bait-and-switch)를 감지한다(물론 의도하지는 않았으리라 본다). 나는 getStr이 RT(참조 투명)라고 말하지 않았다. RT에 대한 정의를 제시했다고도 말하지 않았다.
getStr이 RT라고 주장한 사람은 당신이지, 나가 아니다. 나는 어느 쪽 주장도 하지 않을 것이다. IO에 대해 RT가 무엇을 의미할 수 있는지 모르기 때문이다.
보통 참조 투명성은 어떤 부분 표현식을 그 ‘값’으로 대체해도 의미가 변하지 않는다는 뜻으로 정의된다. 이 정의는 일반적으로 문제가 있다. 표현과 값은 서로 다른 종류이기 때문이다. 어떤 타입들은 일종의 정규형(normal form)을 갖고 있어서, 그것을 “값”으로 간주할 수 있다. 예를 들어, 수치 타입에는 숫자/리터럴이 있다. IO에서는, “리터럴”의 역할을 하는 것이 무엇인지 나는 모른다.
참조 투명성의 또 다른 정의는, 어떤 부분 표현식이든 그와 같은 값/의미를 갖는 다른 표현식으로 대체해도, 그 표현식을 포함하는 표현의 의미/값이 변하지 않는다는 것이다. 여기에도 미묘한 부분이 있다. 우리가 두 (부분)표현이 ‘같은 값을 가진다’고 말하거나 그렇지 않다고 말할 때, 우리는 “같다”를 무엇으로 뜻하는가? 그 진술이 의미 있으려면, 의미상의 동등성을 정의해야 한다. 나는 IO에 대한 그런 정의를 알지 못한다. 흥미로운 정의가 있을 수도 있고, 없을 수도 있다.
IO는 우리 부족의 집단적 죄를 짊어진다. 고대 히브리인의 속죄 염소처럼. 또는 Simon Peyton Jones가 표현했듯이, “IO 모나드는 Haskell의 죄수함(sin-bin)이 되었다. 우리가 이해하지 못하는 것을 볼 때마다 IO 모나드에 던져 넣는다.”(Wearing the hair shirt – A retrospective on Haskell). 그러고 나서 우리는 나중에 와서, 지금까지 죄수함에 던져 넣은 모든 것들과 동시성에도 일관되면서도, 어딘가 건전하고 설득력 있는 동등성 개념을 IO에 부여할 수 있을까? 아니면 우리의 집에서 만든 Toxic Avenger처럼, 비사회적으로 행동하려 들까?
Paul이 계속해서 말했다:
내가 왜 getStr(그리고 부수효과가 있는 다른 함수들)이 RT라고 생각하는지에 대한 주장을 해 보겠다. 내 정의는 좀 더 제한적이다: 내게 RT 함수란, 그에 대한 어떤 호출이든 그 호출의 결과로 대체해도 프로그램의 의미에 영향을 주지 않는 성질을 가진 함수다. …
먼저, 여기서 나는 ‘함수’의 역할을 단순화/일반화하여 ‘표현’에 대해 이야기하고 싶다(함수 적용을 포함한다). ‘RT한 것은 함수’가 아니라 ‘RT한 것은 표현’이라고 말이다. 사소한 트집처럼 들릴지 모르지만, 이런 문제들에서 명료함(그리고 의견을 넘어서기)을 얻는 다른 방법을 모르겠다. David R. MacIver가 “언어의 문제”에서 말했듯이,
물론, 용어의 정의를 시작하면 사람들은 그 정의에 대해 논쟁하기 시작할 것이다. 지루하다는 건 안다. 하지만 정의 없이 논쟁하는 것만큼 지루한 일은 없다.
“getStr”는 함수가 아니라 표현이므로, 표현에 적용되는 정의가 필요할 것이다.
다음으로, 내가 위에서 제기한 불편한 질문들이 있다: IO 부분 표현식의 ‘값’은 무엇인가? 그런 값을 프로그램에 어떻게 대체할 수 있는가? 새로운 표현의 의미가 옛 표현의 의미와 ‘동등’하다는 말은 무엇을 뜻하는가?
계속해서,
Haskell은 IO를 수행하는 모든 함수에 단일 IO 값을 스레딩하도록 강제하기 때문에, IO 타입의 값들이 실제로 부수효과를 갖는다는 것을 프로그램이 감지할 방법이 없다. … 우리는 동적으로 선택되는 IO 값들을, 사실은 프로그램을 실행하기도 전에 미리 선택된 것이라고 가장할 수 있다.
이 문단을 여러 번 읽었다. 특히 당신이 “Haskell은 IO를 수행하는 모든 함수에 단일 IO 값을 스레딩하도록 강제한다”, 또는 “IO 타입의 값들이 실제로 부수효과를 갖는다”고 믿는 이유가 무엇인지 정리하려고 노력했다.
내 최선의 추측은, 당신이 “IO 값”을, GHC가 IO 구현에서 사용하는 가상의 World 값과 혼동했고, “IO를 수행하는 함수들”을 IO 값 그 자체와 혼동하고 있다는 것이다. 이 혼동이 당신의 언어 때문인지, 사고 때문인지, 아니면 내가 당신이 말하는 바를 놓친 것인지 모르겠다.
그건 그렇고, GHC의 World 전달 표현(World-passing representation)은 단지 구현 핵일 뿐이다. Haskell IO의 건전한 지시 의미 모델이 아니다. 동시성을 설명할 수 없기 때문이다. World 타입이 구현 핵 그 이상인 것처럼 생각하게 된 것이 somehow 유행했다.
Haskell에서 IO의 참조 투명성에 대한 인기 있고 훨씬 단순한 논변이 하나 더 있다. 이는 당신이 말한 것과는 매우 다르다고 생각한다. 이 관점에서, “IO 타입의 값들이 실제로 부수효과를 갖는다”는 것을 프로그램이 감지할 방법이 없는 이유는, IO 값들이 정직하게도 부수효과를 ‘갖지 않기’ 때문이다. 거짓말이 필요 없다. IO 값들은 아무 것도 ‘하지’ 않는다; 단지 ‘존재’할 뿐이다. 그런 값들의 특정 ‘해석’만이 부수효과를 낳는다. 사람들은 이 구분에 혼동하기도 하고, 어떤 이상한 마술이나 사기라고 생각하기도 한다. 나는 이를 Boolean과 Integer 같은 단순 타입의 상황에 비유한다. “True”나 “3+4”가 지시하는 값은 부수효과가 없다. 하지만 “print”라는 해석은 실제로 부수효과를 낳는다.
따라서, 많은 이들은 Haskell의 IO가 잘 행동하는 순수 함수형 타입이며, Haskell의 정결한 순수성 지위가 보존된다고 주장한다. 나는 이런 사고에 큰 팬이 아니다. 내가 다른 곳에서 설명했듯, 비슷한 추론은 C 언어가 순수 함수형임을 보여주기도 한다.
이 관점은 Haskell의 IO가 참조 투명하지 않다는 ‘반증’을 피하긴 하지만, 참조 투명성을 ‘증명’하지는 못한다. 우리의 참조 투명성 정의는 ‘동등성’에 의존하는데, IO에 대한 동등성 정의를 갖고 있지 않기 때문이다. 아마 동시성과 지금까지 죄수함에 던져 넣은 모든 것들과 일관되면서도, 어딘가 건전하고 설득력 있는 정의를 고안할 수 있을지 모른다. 아니면 그럴 수 없을지도.
2009년 6월 21일 오전 11:35 49. #### Luke Palmer:
나는 RT를, ‘정의를’ 가리키는 기호를 그 정의로 대체해도 의미에 영향을 주지 않는 성질로 이해해 왔다. 이는 IO 타입을 조작하고 그 조합자를 사용하는 순수 Haskell 조각에는 적용된다. 하지만 getChar, newIORef 같은 많은 IO “프리미티브”가 있다. 이 프리미티브에는 RT가 적용되지 않는다. 정의가 없기 때문이다.
순수 람다 미적분 — 추상과 적용만 — 은 완전히 RT하다. 모든 기호는 정의를 가져야 한다(모든 “기호”가 람다에 의해 바인딩되는 프로그램과 동등하다). 새로운 것을 단 한 가지라도 추가하는 순간 — 수/더하기, 불리언/if-then-else — 정의 없는 기호를 도입하게 될 수 있다. 반드시 RT를 깨뜨리는 것은 아니다. 그 용어가 그런 기호에는 적용되지 않을 뿐이다.
흠. 내 정의의 “… 의미에 영향을 주지 않고”는 흥미로운 표현이었다. 표현은 여러 종류의 의미를 동시에 갖는다. 그래서 RT에 대해 이야기할 때는 어떤 주어진 의미론에 ‘관해’ 이야기해야 한다. 예컨대, Haskell(및 IO, FFI, seq 제외)이 그것의 영역 이론적 의미론에 관해 RT라고 말할 수 있을 것이다. 하지만 (예컨대 GHC의) 운영적 의미론에 관해서는 분명 그렇지 않다. 정의는 공유를 도입하기 때문이다.
2009년 6월 21일 오후 4:08 50. #### conal:
의견 고맙다, Luke. RT에 대한 정의가 몇 가지 떠다닌다. 당신의 특정 정의는 프리미티브가 있을 때 적용되지 않는다는 점을 이해한다. 그래서 나는 아마 ‘같은 의미를 가진 다른 표현으로 부분 표현식을 대체’하는 쪽의 정의를 선호한다. 이는 (아직 IO에는 — 내가 아는 한 — 아니지만) 정확한 의미를 가진 타입들에는 적용된다.
2009년 6월 21일 오후 7:07 51. #### Paul Chiusano:
Conal,
먼저, 여기서 나는 일종의 미끼-바꾸기(bait-and-switch)를 감지한다(물론 의도하지는 않았으리라 본다). 나는 getStr이 RT(참조 투명)라고 말하지 않았다. RT에 대한 정의를 제시했다고도 말하지 않았다.
좋다, 아마 내가 혼란스러웠던 것 같다. 당신의 원문에서는 “System.Info.os :: String이 싫다”고 말한 듯했다. 그것이 (RT의 혼) 위배이기 때문이라고. 그러자 나는, 타입을 IO String으로 바꾸는 것이 아무 것도 고치지 않는다고 제안했다. 또한 (내가 당신의 RT 정의라고 생각했던 것에 따르면) getStr 역시 RT가 아니라고도 했다. 당신의 답변에서 나는 (잘못) “맞다, System.Info.os를 IO String으로 만들면 RT다”라는 인상을 받았다. getStr도 마찬가지로.
우리가 서로 엇갈리고 있는 듯하니, 이 시점에서는 IRC에서 해시하는 편이 낫겠다.
덧붙여, 나는 이 논의를 즐기고 있고 매우 흥미롭다고 생각한다!
2009년 6월 21일 오후 9:05 52. #### conal:
안녕하세요 Paul,
당신의 원문에서는 “System.Info.os :: String이 싫다”고 말한 듯했다. 그것이 (RT의 혼) 위배이기 때문이라고. […]
우리 사이의 소통 간극의 일부를 본 듯하다: 당신은 내 글이 참조 투명성에 관한 이야기라고 생각했다. 대신 나는 RT에 대한 초점을 다른 두 원칙으로 옮기고 있었다. 첫 번째(“닫힌 표현의 값은 … 그 표현 자체에만 의존한다”)에서는 RT에 대해 슬쩍 언급했는데, 그게 혼란의 문을 열어버렸다. 두 번째는 “내게 더 근본적”인 것으로, ‘타입은 의미를 가진다’는 것이다. 그리고 어떤 표현의 의미는 그 표현의 타입의 의미에 속한다.
RT가 아닌 다른 용어들로 이런 논의를 재구성하고 싶은 이유 중 하나는, RT에 관한 논의가 반복해서 심각하게 뒤섞이는 것을 보기 때문이다. 그리고 나에게 RT의 정의는 기술적이고 심장이 없다. 나는 이 글에서 내가 제시한 원칙들에서 훨씬 더 많은 명료함과 힘을 얻는다.
내가 전달하려던 바를 더 정확히 읽으면 이렇다: “System.Info.os :: String이 싫다. 그 타이핑은 내가 함수형 프로그래밍을 사랑하는 이유의 심장에 있는 두 원칙을 위배하기 때문이다. 그리고, 덧붙여, 그 원칙들은 RT의 심장이기도 하다.” RT에 대한 혼란이 많고 기술적/생기 없는 개념이기 때문에, 나는 RT 대신 아마 더 생산적인 틀을 제공하고 싶다 — 순수성에 대한 논의를 위한.
우리가 서로 엇갈리고 있는 듯하니, 이 시점에서는 IRC에서 해시하는 편이 낫겠다.
덧붙여, 나는 이 논의를 즐기고 있고 매우 흥미롭다고 생각한다!
즐기고 있다니, 그리고 안도감이 든다. 비언어적 신호가 없으니 알기 어렵다. 알려줘서 고맙다. IRC에서 더 이야기하자.
2009년 6월 22일 오전 8:01 53. #### Conal Elliott » Blog Archive » 더 게으른 함수형 프로그래밍, 1부:
[…] 다양한 구현에 우호적인 의미론. (Haskell은 Haskell에서의 순수성 개념에서 설명했듯, 이 점에서 약간 모자라다.) 안타깝게도, “덜 엄격하다”는 뜻의 기분 좋은 비교형을 모르겠다. 그래서 […]
2010년 12월 20일 오후 12:52 54. #### Tommy Thorn:
내가 말하면 너드 분노를 살 것들을, 당신이 훨씬 더 우아하게 표현해줘서 고맙다.
haskell-cafe에서 누군가가 실제 사례를 들었다. 끔찍한 Int 의미론 때문에, 같은 프로그램이 두 플랫폼에서 신비롭게 다른 행위를 했다.
여기서 내가 할 수 있는 유일한 “기여”는, Int에 조용한 오버플로 의미를 부여한 위원회의 전제 자체에 의문을 제기하는 것이다. 현대 컴파일러 기술과 현대 하드웨어에서는, 오버플로 포착이 반드시 큰 성능 오버헤드를 수반하지는 않는다. 덧셈의 오버플로에 대한 조건 분기는 거의 비용이 없는데, 완벽하게 예측되고 다른 모든 명령과 병렬로 발행될 수 있기 때문이다. SPARC 같은 일부 아키텍처에는, 오버헤드가 0인 오버플로 체크 add/sub 변종도 있다. 오버플로가 일어날 수 없음을 보기 쉬운 경우도 많다.
2013년 6월 2일 오후 1:58 55. #### Irene Knapp:
나도 이 원칙들을 소중히 여긴다. 기록을 위해 말해 둔다!
이름(필수)
이메일(공개되지 않음)(필수)
웹사이트
* [이항곱닫힌 범주로서의 회로](http://conal.net/blog/posts/circuits-as-a-bicartesian-closed-category)
* [CCC 최적화](http://conal.net/blog/posts/optimizing-cccs)
* [람다 오버로딩](http://conal.net/blog/posts/overloading-lambda)
* [카르테시안 곱닫힌 범주를 통한 Haskell에서 하드웨어로](http://conal.net/blog/posts/haskell-to-hardware-via-cccs)
* [행렬 다시 상상하기](http://conal.net/blog/posts/reimagining-matrices)
* [Conal](http://conal.net/)님이 [함수형 반응형 채터봇](http://conal.net/blog/posts/functional-reactive-chatter-bots#comment-167359)에 남긴 댓글
* Gregory Travis님이 [함수형 반응형 채터봇](http://conal.net/blog/posts/functional-reactive-chatter-bots#comment-167351)에 남긴 댓글
* [Conal](http://conal.net/)님이 [3D 그래픽스의 의미론에 대한 생각](http://conal.net/blog/posts/thoughts-on-semantics-for-3d-graphics#comment-128816)에 남긴 댓글
* [Doug Moen](https://curv3d.org/)님이 [3D 그래픽스의 의미론에 대한 생각](http://conal.net/blog/posts/thoughts-on-semantics-for-3d-graphics#comment-128815)에 남긴 댓글
* [Conal](http://conal.net/)님이 [3D 그래픽스의 의미론에 대한 생각](http://conal.net/blog/posts/thoughts-on-semantics-for-3d-graphics#comment-128806)에 남긴 댓글
* [2013년 9월](http://conal.net/blog/posts/2013/09)
* [2012년 12월](http://conal.net/blog/posts/2012/12)
* [2012년 11월](http://conal.net/blog/posts/2012/11)
* [2011년 6월](http://conal.net/blog/posts/2011/06)
* [2011년 5월](http://conal.net/blog/posts/2011/05)
* [2011년 3월](http://conal.net/blog/posts/2011/03)
* [2011년 2월](http://conal.net/blog/posts/2011/02)
* [2011년 1월](http://conal.net/blog/posts/2011/01)
* [2010년 10월](http://conal.net/blog/posts/2010/10)
* [2010년 9월](http://conal.net/blog/posts/2010/09)
* [2010년 8월](http://conal.net/blog/posts/2010/08)
* [2010년 7월](http://conal.net/blog/posts/2010/07)
* [2010년 1월](http://conal.net/blog/posts/2010/01)
* [2009년 12월](http://conal.net/blog/posts/2009/12)
* [2009년 11월](http://conal.net/blog/posts/2009/11)
* [2009년 6월](http://conal.net/blog/posts/2009/06)
* [2009년 5월](http://conal.net/blog/posts/2009/05)
* [2009년 3월](http://conal.net/blog/posts/2009/03)
* [2009년 2월](http://conal.net/blog/posts/2009/02)
* [2009년 1월](http://conal.net/blog/posts/2009/01)
* [2008년 12월](http://conal.net/blog/posts/2008/12)
* [2008년 11월](http://conal.net/blog/posts/2008/11)
* [2008년 10월](http://conal.net/blog/posts/2008/10)
* [2008년 9월](http://conal.net/blog/posts/2008/09)
* [2008년 7월](http://conal.net/blog/posts/2008/07)
* [2008년 6월](http://conal.net/blog/posts/2008/06)
* [2008년 5월](http://conal.net/blog/posts/2008/05)
* [2008년 4월](http://conal.net/blog/posts/2008/04)
* [2008년 2월](http://conal.net/blog/posts/2008/02)
* [2008년 1월](http://conal.net/blog/posts/2008/01)
* [2007년 11월](http://conal.net/blog/posts/2007/11)
* [2007년 7월](http://conal.net/blog/posts/2007/07)
* [2007년 6월](http://conal.net/blog/posts/2007/06)
* [2007년 3월](http://conal.net/blog/posts/2007/03)
* [2007년 2월](http://conal.net/blog/posts/2007/02)
* [2007년 1월](http://conal.net/blog/posts/2007/01)
* [2006년 9월](http://conal.net/blog/posts/2006/09)
* [2006년 4월](http://conal.net/blog/posts/2006/04)
* [2006년 1월](http://conal.net/blog/posts/2006/01)
applicative functorarrowbeautiful codebotcategorycomonadcreativityderivativedesignEroseventsfoldFRPfunctional reactive programmingfunctorfuture valuegraphics programmingicfpinteractionlibrarylinear mapmathmemoizationmonadmonoidnumberPajamapaperpartial valuePhooeyprogram derivationreactive valuereactivityscansemanticstrieTVtype classtype class morphismtype compositiontype familyunambvectorzipzipper
zipperzipvectorunambtype compositiontype class morphismtype classTVtriesemanticsscanprogram derivationPhooeypartial valuepaperPajamanumbermonoidmonadmemoizationmathlinear maplibraryinteractionicfpgraphics programmingfuture valuefunctorfunctional reactive programmingFRPfoldeventsErosdesignderivativeDeepArrowcreativitycontinuouscomonadCCCcategorybotbeautiful codearrowapplicative functor
* [로그인](http://conal.net/blog/wp-login.php)
* [글 RSS](http://conal.net/blog/feed)
* [댓글 RSS](http://conal.net/blog/comments/feed)
* [WordPress.org](https://wordpress.org/ "Powered by WordPress, state-of-the-art semantic personal publishing platform.")
글(Entries) RSS 및 댓글(Comments) RSS. 유효한 XHTML 및 CSS.
WordPress와 Fluid Blue 테마로 구동됩니다.
11:11