번역서 "C# 6.0 완벽 가이드" 나왔습니다. 독자 증정 이벤트도 있습니다.

류광, 2016/10/30 19:54
오라일리의 C# 6.0 In A Nutshell을 번역한 "C# 6.0 완벽 가이드"가 며칠 전에 출간되었습니다. 독자 증정 이벤트도 진행합니다.

"C# 6.0 완벽 가이드" 번역서 출간 소식 전합니다.

C# 6.0 완벽 가이드(전2권)

오라일리의 C# 6.0 In A Nutshell을 번역한 책으로, 지난 6월 말 탈고했는데 분량이 꽤 되다 보니 조판과 교정에 시간이 좀 걸렸습니다.

1300여 페이지나 되는데 C#의 거의 전 면모를 다루다 보니 그런 것이고, 억지로 분량을 불린 것이 아닙니다. C# 6.0의 새로운 기능에 관심이 있는 분은 물론이고 C#을 처음 배우는 분에게도 추천하고 싶은 아주 알찬 책입니다. 두 권으로 분책했으니 가지고 다니기에도 좋을 것입니다. (참고로, 원서는 한 권으로 나왔는데 글자가 좀 작고 조판이 전체적으로 빡빡합니다. 제목에 "In A Nutshell"이 있어서 무리하게 한 권으로 낸 게 아닌가 합니다...)

(아래 독자 증정 이벤트는 성황리에 잘 끝났습니다)

이번에도 독자 세 분께 번역서를 증정하려 합니다. 3월의 "클라우드를 관리하는 기술" 증정 이벤트에서는 간단한 퀴즈를 냈었는데, 이번에는 간단하게 선착순 댓글로 가겠습니다. 단, 무조건 선착순이 아니고 게임코디에서 자주 하는 방식처럼 특정 시점 기준 선착순입니다. 안타깝게도 이 블로그의 댓글 시스템(DIQUS)은 댓글이 바로바로 등록되지 않아서 GpgStudy.com의 포럼을 이용하겠습니다. 규칙은 이렇습니다.

  • 2016년 11월 1일(화) 오후 두 시 정각(14:00)부터 GpgStudy 포럼의 "C# 6.0 완벽 가이드" 증정 이벤트에 댓글을 달아 주세요. (참고로 GPG 6, 7 번역서 출판사는 "와우북스"입니다.)
  • 내용은 무관합니다. 단, 비회원인 경우에는 연락 가능한 이메일 주소를 반드시 써 주세요. (회원은 등록된 메일 주소로 연락 드립니다. 혹시 다른 주소를 원하시면 댓글에 써 주시고요.)
  • 댓글 상단 오른쪽에 "올려짐: 2016-11-01 HH:MM" 형태로 등록 시간이 표시되는데, HH:MM >= 14:00인 선착순 세 분께 "C# 6.0 완벽 가이드" 한 질(1, 2권)씩 택배로 보내 드립니다.
  • 한 사람이 최대 3회 시도할 수 있고, 중복 당첨은 없습니다.
  • 게시판 오류도 게임의 일부입니다:) 마음 편하게 복불복이라고 생각해 주세요~
  • 당첨자 세 분이 결정되면 메일로 통지해 드립니다. 주의: 36시간 이내로 답장이 없으면 당첨을 취소하니 편지함(특히 스팸함)을 자주 확인하세요! (지난 이벤트들에서 연락이 안 되는 한 분 때문에 다른 분들까지 기다려야 했던 경험이 있어서 그러니 양해 바랍니다.)

많은 참여 부탁합니다!

top
TAG 번역서
트랙백 0 : 의견 # + 0

C++의 형식 추론과 연역, 귀납, 귀추

류광, 2016/10/25 21:30
C++에서 왜 형식 추론(inference) 대신 형식 연역(deduction)이라는 용어를 더 많이 사용하는지에 관한 한 가지 가설 또는 "꿈보다 해몽".

프로그래머가 형식을 명시적으로 지정하지 않은 코드 요소(주로 변수)의 형식을 컴파일러가 초기치에 근거해서 결정하는 것을 흔히 type inference라고 부릅니다. inference는 흔히 추론이라고 번역하지만, 문맥에 따라서는 ‘추리’라고도 합니다(예를 들어 제 번역서 인공지능: 현대적 접근방식에서는 추론을 inference보다 좀 더 포괄적인 reasoning에게 양보하고, inference는 ‘추리’로 번역했습니다)[1].

그런데 Effective Modern C++에서 저자 스콧 마이어스(SM)는 일관되게 type deduction, 즉 ‘형식 연역’이라는 용어를 사용합니다. 번역서 p.27이나 p.45를 보면 알 수 있지만, SM이 애초에 형식 추론이라는 용어를 몰랐던 것은 아닙니다. 특히, p.45를 보면 type inference가 프로그래밍 세계에서 “흔히 쓰인다”는 점도 알고 있음을 확인할 수 있습니다.

사실 그 책뿐만 아니라, C++ 표준과 관련된 문서들은 거의 예외 없이 deduce/deduction을 사용합니다. C++98, C++03, C++11 표준 명세서에는 infer/inference가 한 번도 등장하지 않고, C++14에는 딱 한 번 등장하는 것으로 알고 있습니다.[2] deduce/deduction은 무수히 많이 나오고요. 애초에 형식 추론에 관련된 C++ 표준 개정 제안 문서들, 이를테면 Deducing the type of variable from its initializer expressionDecltype and auto 등에서 전적으로 deduce/deduction을 사용했으므로 표준 명세서에서 그런 용어를 사용하는 것도 당연한 일입니다. 그렇다고 C++ 표준 개정에 관련된 사람들이 type inference라는 용어의 존재를 몰랐을 것이라고 가정하는 것은 비현실적입니다. 아주 간단한 예로, C++ 표준 위원회(WG21)의 핵심 인물 중 하나인 Herb Sutter는 개인 블로그에 “Type Inference vs. Static/Dynamic Typing”이라는 글을 올린 적이 있습니다.

C++의 형식 추론과 관련해서 일관되게 deduce/deduction이라는 용어를 사용하는 전통(?)은 C++의 창시자 비야네 스트롭스트룹(BS)에서 비롯된 것으로 보입니다. 문자열 검색이 아니라 그냥 페이지를 넘기면서 눈으로 찾은 것이라서 확실하지는 않지만, Design and Evolution of C++의 템플릿 관련 부분에 infer는 없고 deduce만 있습니다(제가 가지고 있는 것은 2004년 판(쇄?)인데, 혹시 1994년 초판 가지고 계신 분은 확인을 좀...) 찾아보기에도 infer나 inference는 없고, deduction과 관련해서는 “deducing template argument”라는 항목이 있습니다.

더 거슬러 올라가서, C++ 템플릿의 역사를 공부하다 보면 BS의 1988년 논문 “Parameterized Types for C++”을 만나게 되는데요. 여기에도 infer/inference는 없고 deduce가 한 번 등장합니다.

C++ 공동체에서 inference 대 deduce에 관한 논의(용어 차원의)는 본 적이 없습니다. 어쩌면 다들 “C++에서 형식 추론은 곧 형식 연역이다”라고 생각하고 있는 게 아닌가 할 정도입니다. 그렇다면 자연스럽게 떠오르는 다음 질문은, “C++에 연역이 아닌 형식 추론, 이를테면 귀납적인 형식 추론은 없을까?”입니다. 다음 예를 봅시다.

auto a = 123.45;

정규 교과과정+일반 대중을 대상으로 한 교양서들 정도로 얻은 지식에 따르면, 연역은 일반적인 것에서 구체적인 것을 끌어내는 추론 방법이고 귀납(induction)은 그 반대 방향, 즉 구체적인 것에서 일반적인 것으로 되돌아가는 것입니다. 그렇다면 위의 예에서 123.45는 구체적인 수치이니까 귀납적인 추론이 아닐까요? 그런데 하나의 사례만으로 어떤 결론을 끌어내는 것은 좀 위험해 보입니다. 그런 해석보다는, “접미사 없는 수치 리터럴의 형식의 double이다”라는 이미 알려진 일반적 원칙에서 출발하는 연역이라고 보는 해석이 더 그럴듯합니다. 실제로 이 해석은 대표적인 연역 방법인 삼단논법에 딱 들어맞습니다.

삼단논법의 예:

  • 모든 사람은 죽는다(대전제).
  • 소크라테스는 사람이다(소전제).
  • 소크라테스는 죽는다(결론).

초기치 123.45의 형식 추론:

  • 접미사 없는 수치 리터럴의 형식은 double이다(대전제).
  • 123.45는 접미사 없는 수치 리터럴이다(소전제).
  • 123.45의 형식은 double이다(결론).

초기치 123.45의 형식에 근거해서 변수 a의 형식을 결정하는 과정 역시 책에 나온 몇 가지 규칙에 따라 연역적으로 일어나고요.

구체적인 값이 둘 이상이면 상황이 달라질까요? 그런 예로 제 머리에 가장 먼저 떠오른 것은 엉뚱하게도[3] 삼항 연산자입니다.

void f(bool cond)
{
	auto a = cond ? 123.45 : 'c';
}

컴파일러가 123.45'c'를 “함께 고려해서” 하나의 형식을 끌어내는 것이라면 귀납이라고 부를 수도 있겠지만, 실제로는 개별적으로 각각의 형식을 연역하고, 두 형식의 값들을 가능하면 정보의 손실 없이 담을 수 있는 하나의 형식을 찾아내는 것일 뿐입니다. 이 점에서 위의 예는 사실 다음과 별로 다를 바가 없습니다.

auto a = 123.45 + 'c';

그런데 좀 더 생각해 보면(사실 처음부터 깨달아야 했지만), 애초에 귀납 추론은 범용 프로그래밍 언어의 형식 추론에, 특히 C++처럼 컴파일 시점에서 최대한 많은 것을 확인하고 점검하려는 언어의 형식 추론에는 맞지 않는 것 같습니다. 연역은 필연적이지만 귀납은 개연적인 추론 방법이기 때문입니다.

예를 들어 삼단논법(연역)에서 대전제와 소전제가 참이면, 그리고 삼단논법에 맞게 결론을 끌어냈다면, 결론은 무조건 참입니다. 모든 사람이 죽는 게 옳고 소크라테스가 사람인 게 옳다면 소크라테스는 반드시 죽습니다. 반면, 귀납 추론을 삼단논법 비슷하게 표현하면 다음과 같은 형태가 됩니다.

  • 소크라테스, 아리스토텔레스, 플라톤, ..., 니체는 죽었다.
  • 소크라테스, 아리스토텔레스, 플라톤, ..., 니체는 사람이다.
  • 모든 사람은 죽는다.

이러한 추론에서, 처음 두 명제가 참이어도 “모든 사람은 죽는다”라는 결론은 허약합니다. 아직 죽지 않은 사람 중에 영원히 사는 사람이 있을 수도 있으니까요.

그리고 귀납과 연역 외에 중요한 추론 방법으로 귀추(abduction)가 있습니다. 귀추법을 소크라테스의 예에 적용하면 이렇습니다.

  • 모든 사람은 죽는다.
  • 소크라테스는 죽었다.
  • 소크라테스는 사람이다.

귀추는 형태상으로 귀납보다 연역에 가깝지만, 결론이 필연적이지 않다는 점은 귀납과 마찬가지입니다. 예를 들어 소크라테스가 죽긴 했지만 사람은 아닐 수도 있으니까요(외계 행성에서 납치(abduction)되어 지구에 온 외계인일 수도...)

정리하자면, 세 가지 주요 추론 방법 중 결론이 필연적인 것은 연역뿐이고, C++에서 형식 추론의 결론은 반드시 필연적이어야 하므로, C++에서 형식 추론은 곧 형식 연역이라고 해도 틀리지 않을 것입니다.

마지막으로, “C++에서 형식 추론의 결론이 반드시 필연적이어야 하는가?”라는 질문이 남아 있는데요. 저는 직관적으로는 “그렇다”라고 생각하지만, 증명이 필요하다면 아마 귀류법(“모순에 의한 증명”)으로 해결할 수 있을 것이라고 생각합니다. 즉, C++ 같은 정적 형식 언어에서 형식 추론의 결과는 ‘유일’해야 하는데(하나의 코드 조각이 두 가지 방식으로 컴파일될 여지가 있어서는 안 되니까요), 만일 추론의 결론이 필연적이지 않다고 가정하면 그 유일성이 깨져서 모순이 발생한다는 점을 보이면 되겠습니다. 혹시 관심 있는 분은 이 증명에 한 번 도전해 보시길!


[1] 예전 제 번역서들에서는 이러한 형식 추론 관련 용어들을 그리 정교하게 구분하지 않았습니다. 글을 쓰면서 살펴보니 2006년까지 나온 C++ 관련 번역서들에서는 type inference를 '형식 유추'라고 번역했는데(아마도 한국 MS의 용어를 참고했던 것 같습니다. 지금도 MSDN 한국어 페이지들에는 type inference가 형식 유추로 되어 있습니다), 유추는 추론의 한 방법인 analogical inference에 해당하므로 그리 좋은 선택이 아니었습니다. 또한, abduction을 귀추가 아니라 유추라고 한 경우도 있고 deduction을 그냥 '추론' 또는 '유도'로 두루뭉술하게 옮긴 책들도 있는데, 조만간 해당 책들의 정오표를 갱신하겠습니다.

[2] 이 문장은 실제 표준 명세서들(iso.org에서 판매하는)이 아니라 표준 명세서에 가장 가까운 공개 초안들에 근거한 것인데, 실제 표준 명세서와 그에 가장 가까운 초안의 차이는 별로 크지 않으므로 이 문장 역시 거의 틀리지 않을 것입니다.

[3] 그냥 template<typename T> f(T a, T b)가 더 직접적이라는 점에서.

top
트랙백 0 : 의견 # + 0

루아 모듈의 이름 충돌 방지(4/4)

류광, 2016/10/07 13:44
시리즈 네 번째(이자 아마도 마지막) 글인 이번 글에서는 객체지향적 표기법을 지원하는 루아 C/C++ 확장 모듈에서 발생할 수 있는 이름 충돌 문제의 좀 더 나은 해결책을 이야기합니다.

저번 글 마지막에서 “루아 레지스트리에 의존하지 않으면서도 메타테이블을 재활용할 수만 있다면”이라고 말했는데, 이번 글에서 구체적인 방법을 살펴보겠습니다.

문제를 좀 더 정확히 정의하자면 이렇습니다. “루아 확장 모듈 적재 함수(luaopen_XXX)에서 생성/설정한 메타테이블을 그 메타테이블을 필요로 하는 루아 확장 함수(특히, 루아 객체 생성 함수)에서 조회하되 그 어떤 루아 전역 정보에도 의존하지 않고 조회하려면?”

전통적인 luaL_newmetatable 기반 해법은 “그 어떤 루아 전역 정보에도 의존하지 않고”를 만족하지 않는다는(따라서 이름 충돌의 여지가 있다는) 것이 저번 글 마지막 부분의 요지였죠.

거두절미하고, 모듈 적재 함수에서 어떤 정보를 그 어떤 고유한 값에도 의존하지 않고 특정 루아 확장 함수에 전달하는 수단은 바로 윗값(upvalue)입니다. 아주 거칠게 말하면, 루아 함수는 실행 시점에서 생성, 전달, 삭제가 가능한 객체이고, 윗값은 함수 객체의 멤버 변수입니다. 다음 예를 봅시다.

local function make_adder(amount)
	return function(val)
		return val + amount
	end
end

local add_5 = make_adder(5)
local add_7 = make_adder(7)

print(add_5(6)) -- 11
print(add_7(6)) -- 13

여기서 amount가 바로 윗값입니다. add_5라는 함수 ‘인스턴스’의 amount는 5이고, add_7amount는 7입니다. 이 예에서 보듯이 윗값은 함수를 생성하는 시점에서 함수 내부의 정보를 설정하는 수단으로 유용합니다. 그리고 다행히 루아 C API도 루아 확장 함수를 루아 상태에 등록(생성)할 때 윗값을 지정하는 수단을 제공합니다.

그럼 저번 글의 동영상 재생 라이브러리 예제를 윗값을 이용해서 개선해 봅시다. 바뀐 것은 함수 두 개인데요. 우선 모듈을 적재하는 함수는 다음과 같습니다.

int luaopen_movie_lib (lua_State *L)
{
	lua_newtable(L); // t (모듈 자체)

	lua_newtable(L); // mt (메타테이블)
	luaL_setfuncs(L, movie_member_lib, 0);     // mt.play = play_movie
	lua_pushvalue(L, -1);
	lua_setfield(L, -2, "__index"); 	// mt.__index = 메타테이블
	lua_pushcclosure(L, open_movie_file, 1); // mt를 open_movie_file 함수의 윗값으로 설정
	lua_setfield(L, -2, "open"); t.open = open_movie_file
	return 1; // 모듈을 반환
}

이전과는 달리 lua_newmetatable을 사용하지 않고 lua_newtable을 이용해서 메타테이블을 직접 생성하고, 메타테이블을 필요로 하는 open_movie_file 함수를 등록할 때 메타테이블을 그 함수의 한 윗값으로 설정합니다. lua_pushcclosure의 셋째 매개변수는 주어진 함수에 설정할 윗값들의 개수입니다. lua_pushcclosure는 루아 스택에서 그 개수만큼의 값들을 뽑아서 윗값들로 설정합니다. lua_pushcclosure 호출 시점에서 스택 최상위에는 메타테이블이 쌓여 있으므로, 그것이 open_movie_file의 윗값으로 설정됩니다.

다음으로, 함수에서 자신의 윗값을 가져오는 방법을 봅시다. 새로운 open_movie_file은 다음과 같습니다.

static int open_movie_file(lua_State *L)
{
	const char * filename = luaL_checkstring(L, 1);
	auto userdata = static_cast<Movie**>(lua_newuserdata(L, sizeof(Movie*)));
	*userdata = new Movie(filename);

	lua_pushvalue(L, lua_upvalueindex(1)); // 윗값(메타테이블)을 가져와서
	lua_setmetatable(L, -2); // 이 사용자자료의 메타테이블로 설정

	return 1;
}

저번 글의 예제에서는 luaL_getmetatable(L, "MT for Movie object");로 메타테이블을 가져왔지만 이번에는 lua_pushvalue(L, lua_upvalueindex(1));로 가져옵니다. 루아 C API 함수 lua_pushvalue는 스택의 특정 항목을 스택 최상위에 복제하는데, 둘째 매개변수가 바로 그 항목의 스택 색인입니다. 그리고 lua_upvalueindex(1)는 현재 함수의 첫 번째(1) 윗값의 색인을 돌려줍니다.

저번 글의 예제에서는 메타테이블을 설정하고 조회할 때 "MT for Movie object"라는 문자열을 사용했지만, 이번 예제에서는 그런 문자열이 없습니다. 즉, 이름 충돌의 여지가 사라진 것입니다.

안타깝게도, 이 역시 ‘궁극의’ 해결책은 아닙니다. 다음과 같은 코드는 오류를 발생합니다.

local movie_player = require"movie_lib"

local movie = movie_player.open(arg[1])

local music = music.open(arg[2])

movie.play(music)

movie.play는 저번 글 예제의 static int play_movie(lua_State *L)에 해당합니다. 다시 제시하자면:

static int play_movie(lua_State *L)
{
	auto movie = static_cast<Movie*>(lua_touserdata(L, 1));
	movie->play();
	return 0;
}

이 함수는 첫 인수가 Movie 객체를 가리키는 포인터를 담은 사용자자료라고 기대하지만, movie.play(music) 호출의 첫 인수는 그런 사용자자료가 아니므로 movie->play();에서 뭔가 끔찍한 일이 발생할 것입니다. 이는 메타테이블과는(따라서 윗값이냐 루아 레지스트리냐와는) 무관하고, 그냥 play_movie 자체가 오류 점검을 소홀히 하기 때문에 생기는 문제입니다.

(제정신인 개발자라면 movie.play(music) 같은 엉뚱한 코드를 작성하지 않을 거라고 생각할 수도 있지만, 위의 예는 단순화된 것일 뿐입니다. 예를 들어 명령 대기열을 이용한 지연 실행 기능 같은 것을 구현한다고 하면, 사소한 실수 때문에도 movie.play(music) 같은 코드가 실행될 수 있습니다.)

이런 오류를 피하려면 주어진 인수가 우리가 원하는 종류의 사용자자료인지 확인해야 하고, 그러려면 애초에 그 사용자자료에 식별용 정보를 연관시켜야 합니다(사용자자료 자체는 결국 void*이므로 형식에 관한 정보를 얻을 수 없습니다). 문제의 근원은 Movie*가 아닌 void 포인터에 대해 static_cast<Movie*>를 적용하는 것이므로, Movie 객체 자체에 식별용 정보를 둘 수는 없습니다. 식별용 정보를 담을만한 곳으로 제가 떠올릴 수 있는 것은 사용자자료에 설정된 메타테이블 뿐입니다. 이해하기 쉽게 루아로 표현하면:

-- movie_lib.lua --

local video_engine = require"my_video_engine" -- 가상의 C/C++ 확장 모듈
local meta = {}
meta.__index = meta
meta.ID = "MT for Movie object"

function meta:play()
	if getmetatalbe(self)
		and getmetatalbe(self).ID == "MT for Movie object"
	then
		return video_engine.play(self.handle)
	else
		-- .. 오류 처리 ..
	end
end

허무하게도, "MT for Movie object"이라는 식별용 문자열이 다시 등장했습니다. 문자열 대신 이를테면 정수 값을 사용한다고 해서 근본적으로 달라지지는 않습니다. 그리고 문자열이든 정수 값이든 충분히 길고 복잡한 값을 사용한다면 충돌 가능성을 아주 낮출 수 있다는 말은 별로 위안이 되지 않습니다(그럴 거면 애초에 그냥 luaL_newmetatable 기반 기법을 사용하면 그만이니까요...). 안타깝게도, 루아 C API 차원에서 뭔가 특별한 수단을 제공하지 않는 한 궁극의 해결책은 없는 것 같습니다. 혹시 이름충돌의 뭔가 돌파구를 발견하신 분이 있으면 꼭 알려 주세요!

이 시리즈는 이걸로 마무리합니다. 궁극의 해결책을 찾지 못해서 아쉽지만, 윗값을 루아 글루 함수 간 정보 전달 수단으로 사용하는 유용한 기법을 발견한 걸로 위안을 삼습니다.

top
트랙백 0 : 의견 # + 0

루아 모듈의 이름 충돌 방지(3/n)

류광, 2016/07/26 19:40
시리즈 세 번째 글인 이번 글에서는 객체지향적 표기법을 지원하는 루아 C/C++ 확장 모듈에서 발생할 수 있는 이름 충돌 문제를 살펴봅니다.

본문 열기

top
트랙백 0 : 의견 # + 0

근황 - 2016-06-30

류광, 2016/06/30 21:54
C# 6.0 in a Nutshell 탈고, Introduction to 3D Game Programming with Direct3D 12.0 번역 예정.

본문 열기

top
트랙백 0 : 의견 # + 0

◀ PREV : [1] : [2] : [3] : [4] : [5] : ... [57] : NEXT ▶