반응형


타입 시스템.



실행되서는 안되는 프로그램들

프로그램을 만드는 방법을 충실히 따라서 제대로 생긴 프로그램을 만드는 것은 쉽습니다. 그런데, 그 중에는 실행될 수 없는 프로그램들이 많습니다.

(프로그램이라고 표현했지만, 좀 규모를 축소하면 코드라고 표현 하는게 프로그래머에게 더 와닫겠군요.)

실행되서 어떤 루틴에서 동작하지 않는 프로그램, 메모리를 과도하게 사용하여 시스템을 멈추게 하는 프로그램, 무한 루프에 빠진 프로그램.... 등등.

예를 들지 않아도 아마 대충은 감이 올것입니다.

실행되서는 안되는 이라고 했지만, 좀더 평범하게 표현하자면, 실행되지만 잘 돌지 않는 프로그램이라고 표현하는 것이 좋겠네요.


프로그램을 만드는 방법( 문법) 적으로는 문제가 없지만, 실제로 동작했을때 제대로 실행되지 않는 프로그램들이 있습니다.


사용자들은 제대로 동작하는 프로그램들을 동작시키길 원하고, 이런 프로그램들을 미리 확인하고 싶어합니다.

컴퓨터 사이언스에서는 이런 프로그램들을 찾고 검증하는 기술들이 점점 발전하고 있습니다.( 물론 완전한 프로그램은 없습니다... Halt problem ...)


다른 분야들에서는(기계설계, 건축 등) 인공물들이 자연세계에서 문제 없이 동작할 지를 미리 분석하는 수학적 기술들이 잘 발달해 있습니다.

뉴튼 역학, 미적분 방정식, 통계 역학 등등.


소프트웨어 분야에서는 아직 완전한 기술들은 없지만, 최종 목표는 소프트웨어가 오류없이 작동할 지를 실행전에 자동으로 검증하는 기술을 이룩하는 것입니다.

이 축을 따라 프로그래밍 언어에서 , 프로그래밍 언어가 어떻게 디자인 되면 이문제가 어디까지 해결될 수 있을까? 프로그래밍 언어 분야에서는 이 시급한 문제에 어떤 답을 내 놓고 있는가?

현재 어느 정도 수준인가?


프로그래밍 언어 분야를 돌이켜 보면, 다음과 같은 단계를 거치면서 한발짝을 나아가고 있습니다.

1.  우리가 확인 할 수 있는 오류의 한계를 정확하게 정의한다.

2.  정의한 오류의 존재를 찾는 방법을 고안한다.

3.  컴퓨터가 자동으로 실행할 수 있도록 소프트웨어를 만든다.


각 단계별로 애매하고 허술한 구석이 없을때에만 비로소 한 발짝의 전진이 있게 됩니다. 이 단계들이 반복되면서 소프트웨어의 오류의 정의는 점점 정교해집니다.

현재 이렇게 해서 발전된 단계는 겨우 두발자국 이라고 합니다.


1세대 기술 : 문법 검증 기술

1970년대에 달성된 첫 발짝은 ,생긴게 잘못된 프로그램을 찾아내는 기술입니다.

이 기술은 안전하고 빠뜨림 없이 문법이 오류인 것을 찾아주는 것을 의미합니다.( 새로운 것이 아니라 이미 우리는 이렇게 코딩하면서 컴파일러를 통해서 오류들을 찾아내고 있습니다.)

그러나, 여전히 제대로 동작하지 않는 프로그램을 찾아 내지는 못합니다.



2세대 기술 : 타입 검증 기술

1990년대에 빛을 보기시작한 기술입니다.

이때의 오류의 정의튼 겉모양이 틀린 프로그램 외에 속 내용이 잘못되어있을 수 있는 경우를 포함합니다.

프로그램 실행은 일종의 계산인데, 계산중에는 분별있는 일들이 일어납니다. 

더하기에 숫자만 들어와야 한다. 수력발전하기 에는 물이 들어와야 한다. 햄버거 만들기에는 퍼티와 빵이 들어와야 한다...

등등의 분별력있는 계산식과 그에 맞는 데이타가 필요한데, 그렇지 않는 경우들이 발생되면, 프로그램은 생각대로 동작하지 않습니다.


타입이 맞지 않는 계산이 실행중에 발생할지를 미리 검증하는 방법이 타입검증입니다. 프로그램의 안전한 타입검증은 그 프로그래밍 언어가 제대로 디자인된 경우에만 가능합니다.(ML, OCaml, Haskell)




프로그램 E의 문법 규칙을 다음과 같다고 하자.




이 때 2+"a" 는 문법 규칙에는 맞습니다. 하지만 프로그램의 흐름상 잘 동작하리라는 보장은 못합니다.

우리는 + 식에 대해서 두개의 부품식은 모두 정수여야 한다는 규칙이 필요합니다.

하지만, 문법 규칙에는 대개 표현하지 않습니다.

이런 문법을 표현하게 되면, 우리는 "문법규칙" 의 영역에서 "타입 시스템"의 영역으로 은근 슬쩍 넘어가게 됩니다.


"x+1"이 말이 되려면, x는 정수여야 합니다. 텍스트가 가지는 속 내용을 문맥으로 고려해야 합니다. 


우선 이를 해결하기 위해서 타입시스템을 만들어봅시다.( 실제로 이미 만들어진 시스템을 학습하는 차원에서 한발 한발 나아가는 것입니다.)

우선 실행중에 위에서 x가 정수인지 아닌지 판단하는 것을 실행기(interpreter)에서 구현했다면, 이를 동적 타입 검증 (dynamic type checking)이라고합니다.


덧셈 "E1+E2"의 두식이 "+" 연산을 하는 시점에 E1이나 E2가 정수가 아닌지 판별하려면, E1식의 결과가 정수이어야 하고, E2의 결과가 정수이어야 합니다.

"+" 연산 시점에서 비로소 판단 할 수 있는 것입니다.

쉽게 만들어지겠죠.? 



그러나 우리에게 필요한 것은 정적 타입 검증(static type checking) 입니다. 프로그램 실행전에 타입을 검증하기

즉, 문제를 미리 찾아내고 무결점임을 확인한 후에 실행을 시작하는 것이 필요합니다.

여러번 언급했던것 처럼 오류를 완전하게 찾아내는 시스템은 만들 수 없습니다. 그렇기 때문에 믿을만한, 안전한, Sound한 시스템 이었으면 합니다.

타입 검증 시스템이 문제 없을 것이라고 했는데 실행중에 문제가 발생하는 경우가 없었으면 좋겠다는 것입니다.



Type


우리가 정의한 언어에는 계산하는 값들의 종류가 5가지입니다.


             


다음 과정을 통해 type을 정의해봅니다.

1. 기본 값(primitive value)의 종류마다 이름을 붙입니다.

2. 레코드 는 복합 값입니다. 레코드의 부품들로 만들어진 복합타입을 갖습니다. (compound value,compound type)

3. 메모리 주소는 값을 보관하고 있고, 메모리주소의 타입은 보환하고 있는 값의 타입에 따라 분류한 복합 타입입니다.

4. 프로시져는 값은 아니지만 그 타입을 가지고 호출식들이 제대로 쓰이는 지 확인할 필요가 있습니다.



                                               



 

타입검증 규칙



                                              "식 E가 제대로 실행되고 만일 끝난다면 타입 r(tau)의 값을 계산한다" 


을 증명하는 논리 규칙들은 다음과 같습니다.


                                           

T(Gamma)는 프로그램에 나타나는 이름들의 타입을 가진 테이블입니다.









   :

   :



재미있는 사실은 이 타입 시스템은 프로그래머가 타입에 대해서 프로그램 텍스트 안에 써 넣은 것이 전혀 없습니다.

(우리는 실제로 이런 언어가 있다는 것도 알고 있습니다.)



이 타입 시스템을 보면, 프로그램의 구석구석이 어떤 타입을 갖는다고 판단 될 수 있는지 순수하게 논리 시스템으로 바라보자.




이 시스템은 그럴듯 하지만 아쉬운 면이 있습니다.  안전(sound)하지도 완전(complete)하지도 않습니다. 이 두가지 문제를 자세히 다루겠습니다.


안전하지 않다.

타입시스템이 문제없다고 판단한 프로그램이 실행중에 타입 오류로 멈출 수 있습니다.


type 리스트 = {int x, 리스트 next }

let 

   리스트 node = {x:=1, next:={}}

in 

   node.next.x

end


위와 같은 프로그래밍은 C나 C++에서 흔히 보는 경우입니다.

typedef struct _OT

{

  int x;

  struct _OT * pNext;

}OTStruct;


int getNextVal(OTStruct* pOt)

{

  return pOt->pNext->x;   //<-- 이 부분이 정상 수행되기 위해서는 여러가지 채크가 필요합니다. 그래야 정상으로 동작하죠.

}


"상식 수준에서 " 키워온 우리 언어에서는 해결할 수 없습니다. 언어를 다지아니 할 때 부터, 이 문제를 염두에 두고 잘 디자인 해 가야 해결 할 수 있는 문제입니다.

(C나 C++등 초창기에 만들어진 언어들에서는 알려지지 않은 오류들이었을 것입니다. 그래서 그 당시에는 이런 문제가 있을 것이라는 예상이 불가능 했을 것이고 말이죠. )



안전하게 되기 위해서 지금까지 설명했던 우리의 언어는 제 정의될 필요가 있습니다. 더이상 이전의 언어는 아닐 것입니다.

안전한 타입 시스템을 고안하고 구현할 수 있게 된다면, 그 타입 시스템을 통과하는 프로그램은 매우 소수가 될 것입니다.

그런 프로그램은 제한된 기능만 가지고 작성된 프로그램들이 도리것입니다.

또, 안전하게 되기 위해서 매우 정교한 타입 시스템을 고안하게 될것이고, 그 정교함이 극에 달해서 아마도 지금의 기계로는 구현할 수 없는 것이 될것입니다.


어떻게 실용적인 범용의 언어가 이 문제를 해결한 편리한 타입시스템을 갖추게 되었는지 는 값 중심의 언어에서 다루게 될것입니다.


그 전에 좀더 타입에 대해서 다뤄볼 필요가 있습니다.(두번째 문제점을 다룰 차례죠)


완전하지 않다.

제대로 도는 프로그램중에서 위의 타입 시스템을 통과하는 것은 일부분 입니다.

제대로 잘 도는 프로그램도 타입이 없다고 판단할 수 있습니다. 


let x:=1

in x:=true 

>> reject !!



let procedure f(x) = x

in 

   f(1);   f(true)

// reject!!


let procedure f(x)= x.age:=19

in f ( {age:=1, id:=001}); 

    f ( {age:=2, height:=170})

//reject !!!!


1. 메모리 주소의 타입이 변할 수 가없다.

    메모리 주소에 처음으로 보관되는 초기값의 타입이 그 주소가 저장할 수 있는 값들의 타입으로 고정됩니다.

2. 어떤 프로시져는 인자값의 타입에 상관없이 실행될 수 있는 프로시져 들이 있다. 

    이러한 프로시져는 다양한 타입의 인자를 받으면서 아무 문제 없이 실행될 수 있습니다.

    그러나 이 타입 시스템은 통과 할 수 없습니다.

3. 레코드 타입 관련해서도 타입 검증이 너무 까다롭다.

   복합적인 데이타를 표현하는 데 쓰이는 레코드의 타입이 유연하지 않습니다.

   리스트를 예로 들어보면,

  lst :={val:=0,next:={}};

  lst.next:= {val:=100,next:={}};

lst의 next 는 빈 레코드 타입으로 고정될 수 없습니다.초기에는 빈 레코드이지만 다음 라인에서 다른 레코드로 변경되어 버렸습니다.

즉, static하게 next의 타입을 판단 할 수 없다는 의미가 됩니다.

이 문제는 타입의 안정성을 개의치 않는다면 쉽게 해결 방법이 나옵니다. (대부분의 현재 언어들이 취하고 있는 방법입니다.)


프로그래머가 타입을 정의하고 이름을 지을 수 있도록 합니다.

그리고 그 이름이 타입 정의에서 재귀적으로 사용될 수 있도록 합니다.

                         type list = {int val, list next}


이렇게 정수를 가지는 val 필드와 재귀적으로 list를 품을 수 있는 next 필드로 표현하고, 리스트의 깊이는 표현할 필요가 없습니다.





1. 타입 이름들의 유효범위는 프로그램전체가 되도록 하였습니다. 

2. 타입 이름들이 정의 되는 곳은 프로그램의 처음 부분 뿐입니다.


빈 레코드의 식은 임의의 타입으로 결정될 수 있다고 타입 규칙으로 정의합니다.


                      

대신 


                       

로, 빈 레코드를 초기에 가지게 되는 리스트 레코드의 next 필드는 list타입이 되는 것으로 판단 되는 것이다.

  lst :={val:=0,next:={}};

  lst.next:= {val:=100,next:={}};



lst의 타입은 list loc으로 판단할 수 있고, 두번째 줄의 지정문도 타입 검증을 통과합니다. lst.next 도 list loc 이기 때문입니다.


여기서 중요한 아이디어이름을 사용한다는 것입니다. 타입에 이름을 붙이고 그 이름으로 속 내용을 감춥니다.

재귀적인 구조를 구현하는 레코드는 재귀 고리 역할을 하는 필드가 있습니다.

임의의 깊이로 레코드를 품을 수 있는 필드의 타입을 사용해야 하지만 이를 표현하지 않기 위해서 이름을 사용하는 것입니다. 



이 해결책 역시 어설픈 이유는, 타입 시스템이 안전하지 않은 문제가 해결되지 않았기 때문입니다. 

안전성을 포기하고 이렇게 고안된 방법들이 현재 대부분의 프로그래밍 언어에 적용되었습니다.



자! 다시 좀더 깐깐하게 짚어보면, 위의 타입시스템으로 작성된 프로그램의 구석 구석 타입이 제대로 되었는지 증명해 낼 수 있을까?

타입 규칙들은 의미규칙과 다른 양상이 있습니다. 타입 검증기 구현은 의미규칙을 보고 실행기를 구현할 때와 같이 간단하지 않습니다.


이 구현의 문제를 간단히 넘어가는 방법(선택의 여지가 없었는지도 모른다)은 프로그래머가 타입 검증을 돕는 코맨트를 프로그램 택스트에 포함시키도록 강제하는 것입니다.

C,C++,Pascal, Java 등의 언어가 이런 방식으로 구성되어있습니다.



의 증명을 구현하는 타입 검증 함수의 입력은 두가지가 될것입니다. 프로그램 식 E와 T(Gamma).

주어진 두개의 입력에 대해서 타입 검증 함수는 위의 타입 판단에 해당하는 r(tau)가 있는지를 찾게 됩니다.

타입 환경 T는  프로그램에 나타나는 이름들의 타입들에 대해서 알려진(가정) 내용입니다.

이것을 가지고 프로그램 텍스트 E가 타입 r를 가질 수 있는지 타입 규칙에 준해서 알아보는 것입니다.


프로그램 전체에 대해서는 시작 T는 물론 비어있을 것입니다. 타입 검증 함수가 프로그램의 부분들에 대해서 재귀적으로 호출되면서 T에 정보가 쌓이게 됩니다.


우리의 타입시스템에서 구현이 어려운 이유는 다음 규칙에서 드러납니다.


                                   



타입 검증에서 함수에 입력으로 줄 타입환경


                                                


은 이미 우리가 알아내야할 r1과 r2가 사용되고 있습니다. 그 두 타입들이 프로시져의 타입을 구성하기 때문입니다.

출력으로 알아내야 할 것을 입력으로 넣어 주어야 하는 것입니다.




이렇게 x의 E1의 타입을 프로시져에서 명시적으로 주면 간편해 집니다.


지금까지의 내용을 종합해서 프로그래머가 타입을 프로그램 코드( 택스트) 에 포함시키도록하자. 

프로시저와 변수의 타입을 프로그래머가 명시하도록 하는 것입니다.






이 문법으로 프로그래밍을 하면 다음과 같습니다. 그리고 타입 검증이 통과 할 것입니다.

e.g.)

type bbs = { int name, bool zap }

let bbs x:={ name:=0, zap:=true }

in 

   if x.zap then 1

              else x.name


e.g.)

type intlist = { int x, intlist next }

let inlist l := { x:=0, name:={} }

in

   l.next :={ x:=1, next:={} }

   l.next.next := { x:=1, next:={} }


e.g.)

type inttree = { inttree l, int x, inttree r }

let procedure shake( inttree t):inttree

= if t={} then t

   else 

        let inttree t':={}

        in

             t':= call shake(t.l);

             t.l : = call shake(t.r);

             t.r:= t'

in

  call shake( { 

                     l:={}, 

                     x:=1, 

                     r:={

                            l:={}, 

                            x:=2,

                            r:={} 

                          }

                    } )

        



타입 환경(type environment)이 이전에는 프로그램에 나타나는 이름들 (변수, 프로시져 이름, 레코드 필드)의 타입에 관한 것이었으나, 이제부터는 타입 이름들의 속 내용(타입)에 대한 환경도 필요해 집니다.


                 




타입에 대해서 다른 두가지 이슈가 더있는데 이부분에 대해서 간단히 언급하겠습니다.

우선 이름 공간(namespace)와 같은 타입 (type equivalence)에 대한 내용입니다.

우리 언어에서는 많은 것에 이름을 지을 수 있습니다. 메모리 주소(변수), 레코드의 필드, 플그램코드(프로시져), 타입에 이름을 정할 수 있었습니다.

타입 이름은 프로그램 전체가 유효범위 였고, 변수 이름과 프로시져 이름은 유효범위를 한정할 수 있었습니다.

같은 이름으로 타입 이름이나 변수 이름으로 사용해도 프로그램 텍스트에서 문제 없이 구분 할 수 있었습니다.

타입 이름과 메모리 주서의 이름은 다른 이름 공간을 가지고 있다고 예기 할 수 있고, 프로시저 이름 과 변수 이름은 같은 이름 공간을 공유합니다.


같은 타입이란 무엇인가?

type a= {int x; int y}

type b= {int y; int x}

내용이 같더라도 위와 같이 이름이 다르면 다른 타입인 것으로 정의(name equivence) 할 수도 있고, 내용만 같다면 같은 타입으로 정의(structural equivalence)할 수 도 있습니다.

우리의 언어에서는 타입 이름이 같아야 같은 타입인 것으로 정의한 셈입니다.




여기까지 해서 기계중심의 언어, 즉 상식선에서 프로그래밍 언어를 디자인 했던 과정을 따라가 보았습니다. 현재 널리 쓰이고 있는 명령형 프로그래밍 언어들이 만들어진 과정이기도합니다.

그 대표적인 언어가 C 언어 입니다. 어떻게 프로그래밍 언어를 디자인해야 하는지, 무엇이 문제일지가 드러나지 않은 상태에서 , 주어진 디지털 컴퓨터를 편하게 사용하기 위한 도구로서 프로그래밍 언어를 디자인 했던 과거죠.


현재 시점에서 바라보면 아쉬움이 많습니다.


다음 >> 값 중심의 언어




반응형

메모리 관리



컴퓨터가 프로그램을 실행할 때 소모하는 자원은 시간과 메모리입니다. 시간은 무한하지만, 메모리는 유한합니다. 프로그램이 한없이 메모리를 소모할 수는 없습니다.


메모리가 어디에서 소모되는지 볼 필요가 있습니다.


프로그램 실행중에 메모리는 let- 식과 프로시져 호출식의 실행을 위해 하나씩 소모됩니다.





                                                 



let 식으로 선언된 이름과 프로시져의 인자 이름을 위해 매번 새로운 메모리가 소모됩니다. 그리고 소모되기만 합니다.

이렇게 메모리만 소모시키면 언젠가는 프로그램을 더이상 수행 못하게 되겠죠.


따라서 메모리를 재활용 해야 합니다. 그러면 어떤 메모리를 재활용 해야 하는가?

프로그램 실행중 사용한 메모리중에서 더이상 사용하지 않을 메모리를 재활용 해야 합니다.

그럼  어떻게 더이상 사용하지 않을 메모리인지를 알아 낼 것인가?

앞에서 우리가 디자인한 언어의 경우에는 쉽습니다. 메모리 주소의 사용기간이 곧 그 주소의 이름의 유효범위 이기 때문입니다.

메모리 주소는 하나의 이름만 붙고 그 이름을 통해야만 그 메모리 주소를 사용 할 수 있기 때문입니다.


우리의 언어에서는 let 식의 마지막이나, 프로시져의 마지막입니다. 


언어가 복잡해지면, 이렇게 간단하게 메모리 재활용이 안되죠..(garbage collection)


메모리 주소가 따로 프로그램 식의 결과값으로 프로그램 여기저기로 흘러다니면, 메모리 주소의 사용기간은 그 주소에 붙은 이름의 유효범위와 일치하지 않게 됩니다.

이런 언어의 경우 메모리관리는 어떻게 해야 할까요... 

이 문제는 좀 미뤄 둬야 할것 같습니다. 아직 이 문제를 다룰 단계가 아니기 때문이죠. 우리 언어는 아직 이런 기능이 없죠.


현재 우리가 설계한(사실은 과거의 언어를 따라서 규칙을 하나하나 추가한것이죠) ,이름을 붙일 수 있는 메모리 주소와 프로그램 코드, 이름의 유효범위, 재귀호출 , 값전달과 주소전달 호출이 가능하고,

while, for 식으로 반복이 가능합니다.



리스트나 나무구조를 만들고 사용하는 프로그램을 짤수 있을까요? 집합을 만들고 사용하는 프로그램을 작성할 수 있을까요? 할수는 있습니다.

그러나 , 손쉽게 다룰 수 있도록 프로그래밍 언어에서 지원이 되면 좋겠습니다.


기초적인 값 이외에 구조가 있는 값을 프로그램에서 손쉽게 다룰 수 있도록 하려면, 구조가 필요합니다.

기초적인 값은 메모리 주소 하나면 됩니다. 구조를 갖춘다는 것은 여러개의 기초적인 값들을 모아서 하나의 새로운 형태를 만든다는 것입니다.

즉, 여러개의 메모리 주소가 필요하게 됩니다. 메모리 뭉치가 됩니다. 이를 레코드(record)라고 합시다. C에서는 struct이라고 하죠.


이렇게 뭉쳐진 메모리 안에 각각의 메모리 주소들도 이름이 있습니다. 레코드 필드라고 합시다.  배열도 메모리 뭉치입니다. 각각의 메모리 주소들의 이름이 자연수로 되어있죠.


                                                                                 


로 표현 할수 있습니다.



                                                                                


값들은 레코드 까지 포함하게 되었습니다. 



                                                                             

                                                                           


프로그래밍 언어에서 레코드를 만들고 사용하는 문법이 아래와 같이 제공 됩니다.


                                                                              



레코드에 이름을 붙이고, 레코드의 필드가 아래와 같이 사용됩니다.

let

   item:= {id:=200012, age:=20}

in 

   item.id + item.age



이제 나무를 작성하는 것이 쉬워졌습니다.


let

    tree := {left:={},v:=0, right:={}}

in

   tree.right := {left:={}, v:=2,:right:=3};

   shake(tree)



이렇게 레코드가 메모리 뭉치를 뜻하면서, 그리고 레코드가 값이 되면서, 메모리 주소의 사용기간을 간단히 알 수 없게 되었습니다.

...

f( let tree:={left:={}, v:=0, right={}}  in tree })

...





포인터 : 메모리 주소를 데이타로


지금까지의 값들은 자유자제로 사용되었습니다. 변수에 저장되기도 하고 프로시저에 전달되기도 하고 프로시져의 결과값이기도 하였습니다.

값들로 자유롭게 움직 일 수 있다는 것은 이름이 붙어 있을 필요가 없이 계산 될 수 있음을 의미 하기도 합니다.

정수마다 이름을 붙여 사용한다거나, 레코드마다 이름이 있어야 하고, 정수를 더할때 마다 그 결과에 이름을 붙여야만 한다면,

불편할 것입니다. 그리고 불평은 당연한 것입니다.



지금까지 항상 이름을 붙여야만 사용할 수 있는 값들이 있었습니다. 메모리 주소와 프로시져 였죠.

메모리의 주소를 이름 없이 쓸 수 있게 하려면, 값들 중에 메모리 주소 역시 값이 되게 하면 됩니다.


                                           

 

프로그램에서 메모리 주소를 값으로 다룰 수 있으려면,문법을 제공해야 합니다.


                                           


malloc E와  &- 식은 메모리 주소값을 만드는 식입니다.



레코드는 메모리 주소 뭉치이고, 레코드는 값이므로, 레코드를 이용해서 메모리 주소가 값으로 여겨지는 프로그램을 할 수 는 있습니다. 

어떻게 ?





                                         


                                      



                                        



                                 




                                



1. *E는 그 자체가 식인 경우와 메모리 지정문의 왼쪽에 있을 경우, 의미가 다르다.

  *E가 지정문의 왼쪽에 있는 경우 E가 뜻하는 메모리의  주소값을 뜻한다.

  *E가 지정문의 오른쪽에 있을 경우 E가 뜻하는 메모리 주소에 보관되어있는 값을 뜻한다. 

(C 언어가 이렇게 되어있지요.)


그래서 다음 프로그램을 실행 시키면,

let 

   x:= malloc(2)

in

   *x := 1;

   *(x+1) := *x+2


x의 주소의 위치에 1을 담고, 다음 위치에 3을 담습니다.





메모리관리

메모리 블럭을 설정하는 방법이나, 레코드 등에 대해서 정의하는 법이 만들어졌으니, 이제 재활용에 대해서 본격적으로 고민해볼 차례입니다.


초기에 작성했던 언어의 경우에는 메모리와 메모리 주소의 이름의 사용기간이 일치했기 때문에 메모리 재활용이 쉬웠습니다.

그러나,  지금은 쉽지 않습니다.


어떻게 해야 할까요?


수동 메모리 재활용


해결을 미루는 쉬운 방법이 있습니다. 메모리 재활용을 프로그래머에게 맡기는 것입니다. 메모리 재활용이 안되는 문제는 프로그래머의 책임으로 ...

그래서 프로그램 언어에 메모리 재활용 명령문을 제공하는 것입니다. (C가 이렇습니다.)


                                       




free E 식의 의미를 정의 해봅시다. 식 E는 메모리 주소를 계산합니다. 그 주소는 반드시 할당받은 메모리 주소이어야 합니다. 메모리 뭉치를 재활용하려면 메모리 뭉치 에 포함된 모든 주소를 재활용 해야 합니다.


                          

\frac { \sigma,M\vdash E \Rightarrow \ell,M' }{ \sigma, M\vdash free\quad E \Rightarrow \cdot ,M' } \quad \ell \in Dom \quad M


좀이상합니다. 메모리 재활용 효과가 표현되지 않았습니다.메모리 재활용 효과가 표현되지 않아도 충분한가???  



메모리 재활용은 이제 프로그래밍 언어 밖의 일이 되어버렸습니다.


언어의 의미정의 목적은 우리가 필요로하는 디테일의 정도에서 혼동이 없게 정의하는 것입니다. 메모리 재활용에 대해서 명시 할 필요는 없습니다.


이 언어의 의미정의로 메모리 관리에 대해 알 수 있는 사실들은.


1. 새 메모리가 필요할 때, let-식이나, 프로시져 호출, 레코드 식, malloc 식, 새로운 메모리는 항상 있어야 한다.

2. 메모리 주소에 저장된 데이타가 없을 때, 그 주소를 접근 하는 식은 의미가 없다.

    let x:= malloc(1) in *x

3. 할당된 메모리들은 서로에게서 서로에게로 도달할 수 있는 방법이 없다. 할당 받은 주소로 부터 몇 발자국을 움직여서 별도로 할당받은 다른 메모리에 도달할 수 있는 방법은 없다.

  ( 이 부분은 C와 다른 점이기도 합니다. C에서는 임의의 주소로 접근이 가능하기 때문이죠)



자.. 오랜 기간동안 문제가 되어왔던 "메모리 재활용을 프로그래머에게 미루는 것은 바람직한 것인가?".

어쩔 수 없는 선택이라고 봐야 할 것입니다.

언어가 복잡해 질 대로 복잡해졌고, 메모리 재활용 방안은 없고.... 


프로그래머가 신경 써야 할 것은 많아졌습니다. 프로그램의 논리적인 흐름만이 아니라, 유한한 메모리를 가진 컴퓨터가 자신의 프로그램을 실행하게 된다는 조건을 염두해 두어야 하고, 적절한 시점에 메모리를 해재 해줘야 합니다. 사용한 메모리 주소 마다 말이죠.


다른 질문, 과연 프로그래머가 자신의 프로그램이 소모하는 메모리의 사용기간을 가장 잘 알 수 있는가? 정말로 그런가?


C 프로그래밍의 경험이 지난 20년 넘게 쌓이면서, 그 대답은 "아니다" 라고 결론이 났습니다. 현재 프로그래머가 가장 많이 하는 실수가 메모리 재활용 오류입니다.


메모리 관리를 프로그래머에게 맡기면서 C 프로그램의 가장 골치아픈 오류가 메모리 재활용이 되었습니다.

그 오류는 두가지 인데,  재활용 지점을 너무 늦게 잡는 오류와 너무 늦게 잡는 오류입니다.

너무 늦게 잡으면 메모리 자원이 고갈되고, 너무 빠르게 잡으면, 사용중인 메모리가 용도변경이 되어버리는 것입니다.

memory leak 과 dangling pointer 죠.



자동 메모리 재활용


메모리 재활용은 사실 프로그래머가 제대로 완벽하게( 안전하게 말구요) 처리하는 것은 어렵습니다. 제 경험도 그렇고, 여러사람들의 경험을 들어봐도 그렇죠..

자동 메모리 재활욜의 개념은 ... 


1. 프로그램 실행은 계속 메모리를 소모한다.

2. 메모리 소모량이 어느 수위에 오르면, 실행을 잠시 멈춘다.

3. 메모리를 재활용한다.

4. 이제 컴퓨터의 메모리 자원이 풍부해졌다.

5. 멈춘 프로그램의 실행을 재개한다.


가능할까? 


프로그램이 멈추고, 앞으로 사용하지 않을 메모리를 찾아서 재활용해야 합니다. 그런데 과연 앞으로 사용하지 않을 메모리를 찾아서 재활용한다는 것이 가능한것인가요?

어떻게 앞으로 사용하지 않을 것을 알까요?  어렵습니다. 사실은 모든 사용하지 않을 메모리를 찾는 것은 불가능합니다. 빠.짐. 없. 이.찾는 것...

확신을 어떻게 할 수 있을까요? 모든 재활용 할수 있는 메모리를 찾을 수 있다는 것은, 이미 불가능하다고 증명된, Halting problem 을 푸는 프로그램을 작성할 수 있다는 것이 되기 때문입니다.

Halting problem program역시 메모리를 사용할  것이고, 이 프로그램의 재활용 가능한 메모리를 빠짐없이 재활용 할 수 있다는 의미가 되기 때문입니다.


H라는 프로그램이 있다고 하자. 프로그램 p를 받아서 p 가 유한한 시간에 끝난다고 하면 true를 리턴하고, 그렇지 않으면 false를 리턴한다. 

이 프로그램H를 가지고 다음과 같이 모순된 f라는 함수를 정의할 수 있습니다.


function f() = if H(f) then (while true skip)

                         else skip


C 로 표현 해보면 아래와 같은 형식..

( 자꾸 C 문법을 가져오는데, 이해를 돕기 위해서 가져오는 것입니다. 실제로 이해를 위해서는 C 문법을 배재하고 이해하는 것이 앞으로 나아가는데 더 도움이 됩니다.)

int f() 

{

    if H(f) 

    {

            while (true);

    }

    else 

    {

            return 1;

    }

}


f가 끝이 난다면H(f)는 true를 리턴할 것이고, 그러면, f는 while true skip 에 의해서 무한히 끝나지 않습니다.

f가 무한히 끝나지 않는다면, H(f)는 false를 return 할 것이고, f는 끝날 것입니다.


이것이 디지탈 컴퓨터의 한계라고 합니다.  


여기서 조건을 조금만 완화하여 그럴듯한(?: 완전하지 않지만, 쓸만한)  재활용 이라고 하면, 우리에게 가능성이 생깁니다.

- 몇개는 빠뜨릴 수 있지만, 꽤 많은 부분은 재활용 해주는 프로그램을 만들수 있다.


원칙 : 프로그램의 진행을 멈추고 나서, 지금까지 할당된 메모리중에서 미래에 사용할 수 있는 메모리를 제외하고 나머지는 모두 재활용해야 한다.

사실: 재활용을 완전하게 할 수 있는 방법은 없다. 

양보: 재활용을 안전하게 할 수 있는 방법은 있다.


- 앞으로 실행할 식인 E만 있다고 하자, 식 E의 현재 환경 environment 으로 부터 현재의 메모리를 통해 다다를 수 있는 모든 주소들은 앞으로 E를 실행중에 다시 사용될 수 있다. 

이것들만 빼고 재활용하자. 즉, 그렇게 해서 다다를 수 없는 메모리 주소들은, 과거에 할당되어 사용되었으나 앞으로의 E를 실행하는데 사용되지 않을 것이 분명하므로,

재활용 해도 된다.


현대 언어들의 메모리 재활용기 (GC: garbage collector)는 위의 방식으로 구현되어있습니다.(Java, ML, Scheme, Haskell, C#, Prolog, etc)


메모리 재활용기(Garbage collector) 구현 알고리즘은 대표적으로 두가지가 있습니다.


프로그램 실행중에 메모리에 만들어진 구조물들을 모두 따라가면서 앞으로 사용될 수 있는 메모리 주소들을 모으게 됩니다. 

메모리가 없어서 메모리 재활용기를 돌리는 것인데, 메모리를 필요로 하게 되죠.

위의 방법으로 메모리 주소들을 모으기 위해서는, 그래프를 누비고 다니는 알고리즘(depth-first-, breath-first-traversal  algorithms )들은 메모리를 소모합니다.

그래프의 가장 긴 경로만큼의 스택을 갖게 되죠.


그러면, 메모리를 소모하지 않고 그래프를 누비는 방법이 있을까요? 스택없이 가능한 알고리즘이 있습니다.



1. 헨델과 그레텔 알고리즘 -Mark & Sweep

2. Stop & copy 

- 메모리 영역을 2개로 나누고, garbage collection이 발생할때, 다른 메모리 영역으로 copy함.

- fragmentation 발생이 거의 없음.

- 메모리를 반밖에 사용할 수 없음.

- copy overhead가 있음.

3. generational 

-  모든 메모리를 탐색하는 것보다, 일부만 탐색하도록 하여 속도를 높이는 방법.

   - 생성된 시기별로 세대를 나눈다.

   - Heap을 두개 이상의 sub heap으로 나눔.

   - 객체가 처음 생성되면, 0 번 세대의 heap에 할당.

   - 0세대의 Garbage collection이 수행될때, (대부분 객체는 수명이 짧기 때문에 사라짐,) 살아남은 메모리들은 다음 세대로 넘어감




다음 >> 타입 시스템


반응형

기계 중심의 언어


초기에 컴퓨터 사이언스는 튜링 머신이나 람다 컬큐러스를 기반으로 컴퓨터와 프로그래밍 언어를 만드는 것에 의의가 있었고, 그러다 보니 자연스럽게 기계 자체를 동작시키는데 가장 큰 목적이 있었습니다.

그 당시는 이제 시작 단계라서 어떻게 프로그래밍 언어를 디자인해야 하는지 무엇이 문제일지가 드러나지 않은 상태 였습니다.


 처음에 기대했던, 기계중심의 언어는 이런것이다.!! 라고 간단하게 정의 되어있을 것이라 기대하고 읽기 시작 하실 것입니다.

그러나, 내용은 사실 그렇게 명쾌하게 나오지 않습니다. 언어가 어떤 수학적 정의를 바탕으로 설계가 되었고, 

그 한계가 무엇인지는 프로그래밍 언어의 요소들을 하나 하나 살펴보면서  짚어갈 수 밖에 없습니다.

예를 들면 변수란 무엇인가? 함수는 무엇인가? 재귀 호출이 무엇인가?  하는 것들이죠. 이미 프로그램 개발 경험이 많고, 여러가지 프로그래밍 언어 교제들을 통해서, 다들 알고 있는 개념들입니다.

하지만 그것들이 무엇인지 정확하게 수학적으로 어떤 의미가 있고, 그래서 어떤것이 미흡한지에 대한 이해와는 다를 것입니다.

언어의 요소들에 대해서 깊이있게 이해하고 새로운 시각을 갖기 위한 과정이 이 programming language principles  입니다. 



"옆에 컴퓨터가 있다, 사용해야 한다, 컴퓨터에게 시킬 일을 편하게 정의할 수 있는 방법을 고안하자." 이렇게 해서 C언어를 만들었던 과정은 .....

아.주. 상식적인 과정(현재에서 바라봤을때)을 밟았습니다.


이 과정들에는 현재 불합리한 부분들도 있고, 이런 과정을 바탕으로 프로그래밍 언어의 발전에서 살아남은 것들도 있습니다. 

우리는 이 살아남은 것들은 앞으로도 살아남을 가능성이 매우큰 것들이고, 우리는 이런 것들을 확인하고 이해해둔다면,  프로그래밍 언어와 컴퓨터 사이언스 부분에 대한 이해의 폭이 넓고 깊어질 것이라 보입니다.



어쩌면 현재의 바람직한 프로그래밍 기술 및 기법들은 미래에 보면 어설픈 모습으로 보일 가능성이 있습니다.



주어진 기계 - 메모리, 중앙처리장치, 입출력이 가능하다.


중앙 처리장치는 기초적인 기계어 명령들을 처리하는 실행기 입니다. 실행할 수 있는 기계어 명령의 갯수는 유한하고 고정되어 있습니다. 

(매우 작은 전기줄로 구성되어있고...)

그 기계는 폰 노이만 머신(Von Neuman machine) 입니다.        


"기계가 실행할 명령문들이 메모리에 보관될 수 있다. 그 기계는 명령문 하나 하나를 메모리에서 읽어와서 실행한다. 명령문 들에 따라 그 기계는 다른 일을 한다" 그 명령문들이 프로그램이죠.


이렇게 기계를 구동시키는 명령어들을 다루는 방식을 좀더 편리하게 사용해보자... 언어입니다. 기계어 보다 좀더 상위의 언어를 고안해봅시다. (사실은 이미 있는 것을 밟아나가갔던 과정을 지나가봅시다.)


1. 변수 : 메모리 주소에 이름 붙이기


 "명령문 여러개가 순서대로 실행된다. 반복이 있다. 입출력이 있다. 조건에 따라 명령이 구분되어 실행된다."

이런 기초적인 조건들을 갖는 기초적인 문법의 언어가 있습니다.


명령문


Command\quad C\quad \rightarrow \quad skip \\

\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad|\quad x:=E\\

\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad|\quad C;C \\

\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad|\quad if \quad E \quad then \quad C \quad else \quad C \\

\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad|\quad while\quad E \quad do \quad C\\

\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad|\quad for  \quad x:=E\quad to \quad E \quad do \quad C\\

\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad|\quad read \quad x \\

\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad\quad|\quad write \quad E





프로그램

                                      




각 명령문과 식의 의미를 정확히 정의할 필요가 있습니다.

우선 의미를 정의해갈 때 사용할 원소 semantic object들이 어느 집합(의미공간) semantic domain 의 원소들인지를 정해야 합니다. 

1. 값은 정수거나 참/거짓이다.- 값의 집합은 정수집합이거나 참 거짓의 집합이다.

2. 메모리는 기계적으로는 전기줄에 매달린 트랜지스터 이겠지만, 좀더 추상화 시키면, 논리 게이트, 더 추상화 시키면 주소에서 값으로 가는 테이블 입니다.

3. 테이블은 수학적으로는 하나의 함수입니다. - 정의 구역은 메모리 주소, 공변역은 값.

4. 정의 구역은 메모리 주소 집합중에서 유한한 집합이다.

5. 주소는 프로그래머가 사용하는 이름들의 집합으로 정의한다.



의미공간( semantic domain) 들은 아래와 같습니다.





프로그램의 의미는 논리 시스템의 증명 규칙들로 정의 된됩니다.

MㅏC=>M'MㅏE=>v 를 증명하는 규칙들이 될것 입니다. 이러한 증명이 불가능 한 C와 E는 의미 없는 것이 됩니다.


이름은 메모리 주소가 됩니다.


이름이 하는 역할에 대해서 대해서 살펴보자. 이름은 2가지 의미를 가집니다.

1. 메모리 주소



2. 때로는 메모리 주소에 있는 값



이렇게 이름이 뜻하는 두개의 값을 L-value R-value라고 부릅니다.  

x:=E의 왼쪽과 오른쪽에 대한 정의가 다름을 의미합니다. 왼쪽은 메모리 주소, 오른쪽은 메모리 주소에 보관된 값 입니다.


깊이있게 들어와서 변수가 뜻하는 의미가 모호해 지는 것이 싫다면, 두가지를 다르게 표현 하는 방법도 있습니다.

메모리 주소의 값을  뜻할때 이름 앞에 !를 표시 하는 것으로 합시다. 그러면 다음과 같아집니다.


x:=x+1 은 x:=!x+1 과 같이 사용되게 됩니다. x+y+1 대신 !x+!y+1 로 사용해야 합니다.


이름은 기계중심의 언어에서는 변수(variable)이라고 부릅니다.




프로그래머가 메모리 주소에 이름을 짓기 시작하면서 문제가 생깁니다. 

같은 이름을 다른 용도로 사용 하고 싶을때, 같은 이름을 다른 메모리 주소를 위해 재사용 하고 싶을때 , 프로그램이 커지다 보면 이런 경우들이 발생하게 됩니다.

항상 다른 이름을 사용해야 한다면, 프로그래머의 불편은 클 것입니다.


우리가 서두에서 정의한 언어는 같은 이름을 다른 메모리 주소로 사용할 수 없습니다. 때문에 매번 다른 이름을 사용해야 합니다.

이를 해결하기 위한 방안은 이미 있습니다. 수학이나 모든 엄밀한 논술에서 사용하는 방안입니다. 이름의 유효범위를 정하는 것입니다.


유효범위가 겹치지 않는다면, 같은 이름을 다른 메모리 주소로 사용할 수 있습니다. 

그 유효범위는 어떻게 표시 할것인가? 수학에서 사용하는 방식을 빌려옵니다.(이 방식은 수학에서 이미 지난 2000년간 사용해온 믿을만한 방식입니다.)

프로그램의 텍스트의 범위로 이름의 유효 범위를 결정하는 것입니다.


                                                      


x라는 이름은 새로운 메모리 주소이고 이는 명령문 C 안에서만 유효합니다. let 구문에 의한 유효범위가 일부만 겹치는 경우는 없습니다.  포함되거나 별개이거나...

같은 이름으로 다른 것을 지칭 할 수 있도록 하기 위해서는 위와 같은 규칙을 정확히 드러내야 합니다. 


환경(Environment)

환경이라는 것을 가지고 의미구조를 정확히 정의 할 수 있습니다.

환경은 이름의 실체를 정의한 함수입니다. 


- 환경(시그마)은 이름들에서 주소들로 가는 유한한 함수 입니다.

- 메모리는 주소들에 값들을 대응시키는 유한한 함수입니다.

- 주소는 더이상 프로그램에 나타나는 이름이 될 수 없습니다. (주소는 다른 집합이 될것입니다.)



설탕 구조 - 패스



프로시져(Procedure)

메모리 주소 뿐아니라 명령문에도 이름을 붙일 수 있도록 하자.!!!! 명령문을 반복적으로 사용하지 않고 이름 만으로 그 명령문을 수행 할 수 있도록,...



  let f(x) = C in C'



1. 프로시져 이름 은 f, 인자 이름은 x, f는 명령문 C의 이름 입니다.

2. x는 프로시져가 사용될 때의 인자값을 보관하는 메모리 주소의 이름입니다.

     x의  유효 범위는 C 입니다.

3. f의 유효범위는 C'. 재귀호출이 가능한 프로시져를 위해서는 f의 유효범위는 C' 뿐 아니라 C까지 포함되어야 합니다.


4. f(E) 는 f로 이름붙은 명령문을 실행합니다.

   그런데 E 값을 인자로 사용하라는 것인데, 의미를 정확히 해보자면, 

   우선 환경을 확장할 필요가 있습니다. 





동적으로 결정되는 이름의 유효범위(Dynamic scope)


정의 :Procedure = Id x Command

(이렇게 해보자)



두 집합 A, B가 있을 때 AxB는 두집합의 원소들으 쌍으로 구성된 집합입니다.


- 프로시져가 선언되면 프로시져 이름의 실체는 해당 명령문과 인자 이름이 됩니다.

- 프로시져 호출이 일어나면, 인자에 대해서는 let 명령문 같은 일이 벌어집니다. 

  새로운 메모리 주소를 프로시져 인자 이름으로 명명하고 그 주소에 인자로 전달할 값을 저장 하고, 프로시져 이름이 지칭하는 명령문을 실행합니다.

- 프로시져 이름이 지칭하는 명령문을 프로시져 몸통이라고 합니다.


아래의 프로그램 실행을 생각해보자.


let x := 0 in 

    let proc inc(n) = x:=x+n in

         let x := 1 in (inc(1); write x)

 


let x:=10 in

    inc(5);--> 10+5

    inc(6); --> 15+6

    let x:=3 in 

       inc(1)



--- 재귀호출 이죠.


프로시져 호출 inc(1) 의 실행중에 몸통 명령문인 x:=x+n 에 나타나는 x는 무엇이 될까요? 처음 선언된 x? 아니면 두번째 선언된 x ?

위의 의미로는 두번째 선언된 x입니다.

이것이 이상한가요? 다음 C와 문법이 똑 같은 Dynamic scope 으로 동작하는 언어를 생각해봅시다.( 익숙한 것이 이해하기 편하니까요.)


int x = 10;

int test(int a) {    return x + a; }  -> x는 결정되지 않음.


int test2()

{

  int x = 50;

   return test(5);  <-- 50 +5 : main에서 test2가 수행될때 x가 결정됨.

}


void main(void)

{

 int res = test2();   <-- 50 +5 : x가 test2 내부에서 결정 결정됨.

 int res1 = test(5); <-- 10+5 : x는 10 (맨 위에서 global로 선언된 )

 

 int x = 15;

 int res2 = test(5); <-- 15+5 : x는 15 (바로 위에 있는)

}




이렇게 X가 procedure 가 선언 된 시점에 결정 된 것이 아니라, 호출 되는 시점에 결정되는 구조는 프로그램이 실행중에 동작이 바뀐다는 의미가 됩니다. 어려운 프로그램이 되겠죠.


정적으로 결정되는 유효 범위( static scope)

이름의 실체는 실행전에 정의 되어야 하지 않을까요?

프로시저 안에서 어떤 수식을 계산하기 위한 변수들은 프로시저를 정의 하는 시점에 이미 결정되어야 쉬워집니다. 적어도 동적으로 결정되는 것 보다는 말이죠.

이런 방식이 프로그램을 이해하기에 간단하고 쉽습니다. 그리고 이런 방식이 위에서 언급한 2000년간 사용해온 규칙이기도 합니다.



                                                                      


이렇게 되면 프로시져 정의와 호출의 의미가 아래와 같이 정의 됩니다.








                       




프로시져 호출 방법

지금까지 방식은 값 전달 방식(Call by value)이었습니다.



                       



그런데 이름이 뜻하는 메모리 주소를 전달해 줄 방법이 있었으면 한다. 그러기 위해서는 프로시져 호출문의 생김새부터 구분해주자.



주소를 전달해줄 call-by-reference 의 정의는 다음과 같습니다.


                 


이렇게 되면, 다른 이름이 같은 메모리 주소를 지칭할 수 있게 됩니다.. 같은 대상을 지칭하는 다른 두개의 이름들이 생기게됩니다.( 별명:alias)


생각해 볼 문제는..

별명이 많아지면 프로그램을 이해하기가 어려워지지 않을까? 이런 별명들이 프로시져 호출에 의해서 실행중에 만들어진다면, 이런 다이나미즘, 혼돈스럽지 않을까?

이런 복잡한 상황을 지원하는 언어는 바람직한가?




명령문과 식의 통합


프로시져 호출은 명령문이었습니다.( 지금까지 정의한 언어로는 그렇다) 

메모리에 반을을 일으키는 프로시져 호출의 결과로 값이 계산되게 하고 싶으면?, 메모리를 변화시킨후에 최종적으로 어느 값을 결과로 내놓는 식이 있으면 어떤가? 

C에서는 return E 같은 명령문이 이 역할을 합니다. 

그런데 생각해보면, 명령문과 식, 두가지는 과연 다른가요? 

1. 메모리에 값을 쓸수 있는 것과 메모리를 읽기만 하는것. 

2. 결과 값이 없는 것과 결과 값이 있는것. 

3. 식의 실행결과는 항상 어떤 값이 된다.

4. 그리고 식은 메모리 변화를 일으킬 수 도 있다.


하나로 합칠 수 있습니다.




프로그램

                                  


프로그래머는 더욱 간단한 언어를 가지고 더욱 자유롭게 프로그래밍을 하게 될 것입니다. 명령문의 개념이 없어지고 모든 것이 식이 되기 때문입니다.


명령문 과 식 두가지 다른 타입의 프로그램 부품을 운용할 필요가 없습니다.


의미 정의는

                           

을 증명하는 규칙들로 통일 됩니다.    명령문 들은 의미없는 빈 값 "." 을 계산한다고 하면,


                      

와 같습니다.





다음 >> 기계중심의 언어 - 메모리 관리와 타입 시스템.


반응형

모양과 뜻


프로그래밍 언어는  문법구조(syntax)와 의미구조(semantics)로 구성되어있습니다.  문법구조는 언어의 겉 모양이고 의미구조는 언어의 속뜻을 의미합니다.

이 두가지를 정의하지 못하면 언어라고 볼수 없고, 이 두가지를 알지 못하면 언어를 사용할 수 없습니다.


제대로 생긴 프로그램을 어떻게 만들고 그 프로그램의 의미가 무엇인지에 대한 정의가 없이는 언어를 구현할 수 도 없고 사용할 수 도 없습니다. 

또, 그 정의들은 애매하지 않고 혼동이 없어야 합니다.



문법구조


문법구조는 프로그래밍 언어로 프로그램을 구성하는 방법입니다. 제대로 생긴 프로그매들의 집합을 만드는 방법으로, 귀납적으로 규칙을 정의합니다.

이 귀납적인 규칙으로 만들어지는 프로그램은 나무(TREE)구조를 갖춘 2차원의 모습입니다.

(여기서 나무라고 표현 했는데, 자료구조의 Tree라고 생각하시면 됩니다. 

책에 나무 라고 용어를 풀어서 사용하고 있는데, 아무래도 실제 프로그래밍에서 좀 멀어져서 바라보고자 하는 의도가 있다고 보여집니다. 

이 글에서 설명하고자 하는 내용들을 실제로 programming 언어의 특정 알고리즘 차원에서 바라보지 말고 좀 떨어져서 바라보는 것도 나름 의미가 있습니다.)


정수식을 만드는 귀납규칙을 생각해봅시다.


1. 임의의 정수는 정수식이라고 한다.

2.두개의 정수식을 가지고  + 를 이용해 정수식을 만들수 있다고한다.

3. 하나의 정수식을 가지고 -를 이용해 정수식을 만들수 있다.



이와 같은 형식으로 간단한 명령형 언어를 생각해봅시다.





0. E는 위에서 정의한 정수식이라고 하자.

1. skip 만 있는 명령문은 그것만 말단으로 가지고 있는 그것만 말단으로 가지고 있는 나무가 된다.

2. 정수식을 오른쪽에, 하나의 변수를 왼쪽에,  루트는 := 임을 표시하는 나무가 된다.

3. 세계의 나무를 하부나무로 가지고 있고 루트노드에는 "if"문 임을 표시하고 있는 나무가 된다.

4. 두개의 명령어 나무를 하부나무로 가지고 있고 루트 노드에는 순차적인 명령문임을 표시하는 ";" 나무가 된다.


위 규칙들은 읽는 사람을 돕기 위해 필요 이상의 정보가 들어있습니다.

if 와 then, else 이런 단어들이죠. 또 명령어의 순서 역시 마찬가지죠.


이를 필요한 정보만 가지도록 간략화 한다면,  








이와 같이 각 규칙마다 간단한 symbol만 있으면 그만이다, then else 같은 장식이 필요 없어집니다.


아니면, 우리가 각 규칙을 구분할 수 있다면, 심볼 조차도 생략할 수 있게 됩니다.







사실, 이렇게 심볼까지 생략해가면서 간소화 시킬 필요는 없지요. 프로그래밍 언어의 사용자는 사람이기 때문에 사람이 이해하는데 도움이 되는 수준으로 규칙을 만들 필요가 있습니다.

그래야 문법 규칙들을 보고 만드는 방법에 대한 힌트가 될 수 있기 때문이죠.




요약된 문법 구조와 구체적인 문법구조

(abstract syntax and concrete syntax)


지금까지 봐온 규칙들은 요약된 문법 규칙이라고도 합니다. 프로그램을 만들때 사용하는 문법은 지금까지 보아온 귀납적인 방법이면 충분합니다. 결과물은 나무구조를 가진 2차원의 구조물이죠.


그러나, 프로그램을 읽을 때의 문법은 더 복잡해집니다.  만들어진 프로그램을 표현할때는 나무를 그리지 않고 일차원적인 글로 표현하게 되고, 쓰거나 말하는 사람의 머리에 그려져 있었을 2차원의 나무구조를 복원시키는 규칙들이 되기 때문입니다.


즉,요약된 문법구조와 구체적인 문법구조의 차이를 표현하자면 , 프로그램을 만들때 사용하는 규칙이냐, 읽을때 사용하는 규칙이냐 하는 차이로 보시면 됩니다.


그렇다면 구체적인 문법은 얼마나 구체적일까?


다음의 정수식을 살펴봅시다.

-1+2


위 정수식은 다음 두개의 나무구조중 하나를 일차원으로 펼친 것입니다.

      <<-1>+2>      -<1+2>


어느것인가? 프로그램 "-1+2"를 둘 중 어느것으로 복구 시킬 것인가?



이 요약된 문법으로는 어느 구조로 복구 시켜야 할 지 알 수가 없습니다.

정수 문법식이 다음과 같다면 어떨까요?




이 규칙을 좀 설명하면,

음의 부호가 앞에 붙은 정수식인 경우는 2가지 밖에 없습니다.

1. 말단 자연수

2. 괄호가 붙은 정수식


구체적인 문법구조는 혼동없이 2차원의 프로그램 구조물을 복구하는데 사용되는데, 이 문법은 프로그램 복원 또는 프로그램의 문법검증이라는 과정의 설계도가 됩니다.(parsing)


의미구조 


문법구조로 1+2를 쓰는 방법을 앞에서 봤다면 1+2가 무엇을 뜻하는가에 대해서 살펴볼 때가 되었습니다.

1+2가 뜻하는 것을 2가지로 분류해서 볼수 있는데요.

1. 결과값 3을 뜻한다.

2. 1과 2를 더해서 결과를 계산하는 과정을 뜻한다.


결과값 3을 의미하는 스타일은 뜻하는 바 궁극을 수학적인 구조물의 원소로 정의해 가는 스타일 "궁극을 드러내는 의미구조( denotational semantics)"라고 합니다.

반면, 프로그램의 계산 과정을 정의함으로서 프로그램의 의미를 정의해 가는 스타일 " 과정을 드러내는 의미구조(operational semantics)" 라고 합니다.


이를 C 언어로 이를 표현해보자면, 아래와 같은 코드일것입니다.


int func_3() return 1+2;}

void main()

{

   int  r = func_3(); // denotational semantics

   printf("%d",r);

   printf("%d",func_3()); //operational semantics

}



그런데 새로운 언어인 ZX 라는 언어가 있다고 합시다. 이 언어의 표현 형식이 operational semantics만 지원한다고 한다면, 아래와 같은 동작으로 이어질 것입니다.


program start;

var x=1+2;

var y=x; // 3이 아닌 1+2라는 연산식을 넘겨줍니다.

display x;  // display 에 1+2 라는 연산식을 넘깁니다.

display y;  // display 1+2라는 연산 식을 넘깁니다.

program end;



다음 코드도 같이 확인해봅시다.

program start;

var f = ax+b;

x=2;

a = 4;

b=4;

display f;  // 4*2+4 라는 연산 식을  display에 넘겨줍니다.

a = 10;

b = 5;

display f; // 10*2+5  라는 연산 식을  display에 넘겨줍니다.

program end;



프로그래밍 언어들은 denotational semantics 와 operational semantics 를 모두 다 제공하고  있고 적절한 상황에서 사용 할 수 있도록 설계되어있습니다.



과정을 드러내는 의미구조(operational semantics)는 어느정도 수준으로 표현해야 할까요?

아주 low 하게 CPU에 전달되는 전기적 신호를 일일이 표시해야 할까요? 아니면 , 1이 1을 계산하고 2가 2를 계산하고 +가 1+2=3이라는 등식을 적용하는 것으로 표현해야 할까요?

아니면 실행되는 과정을 , 프로그램과 그 계산 결과를 결론으로 하는 논리적인 증명 과정이라고 표현해야 할까요?


의미구조를 정의하는 목표에 맞추어 그 디테일의 정도를 정하게 됩니다. 대개는 상위의 수준, 가상의 기계에서 실행되는 모습이거나, 기계적인 논리 시스템의 증멍으로 정의하게 됩니다.


과정을 드러내는 의미구조도 denotaional semantics 처럼 충분히 엄밀합니다.

1. 조립식이 아닐 수 있다: 프로그램의 의미가 그 프로그램의 부품들의 의미로만 구성되는 것은 아니다.

2. 하지만 귀납적이다 : 어느 프로그램의 의미를 구성하는 부품들은 귀납적으로 정의된다. 이 귀납이 프로그매의 구조만을 따라 돌기도 하지만, 프로그램 이외의 것을 따라 되돌기도 한다.

(이 문장들의 의미는 잘 이해 못했습니다.)





프로그램의 실행이 진행되는 과정을 큰 보폭으로 그려보자.  <<big-step semantics>>

명령문 C와 정수식 E에 대해서 각각 아래와 같이 정의합니다.



1. 명령문 C가 메모리 M의 상태에서 실행이 되고 결과의 메모리는 M'이다.

2. 정수식 E는 메모리 M의 상태에서 정수값 v를 계산한다.


이에 대한 모든 규칙을 쓰면 이렇습니다.


                                              


                                    


                       


               


               


                             



                                                          


                                                      


                                   


                                                       



이 규칙들은 논리 시스템의 증명 규칙이라고 이해해도 됩니다. 명령 프로그램 C의 의미는 임의의 메모리 M 과 M'에 대해서 MㅏC => M' 를 증명할 수 있으면 그 의미가 그 의미가 됩니다.

대개는 M이 비어있는 메모리일때 C를 실행시키는 과정을 나타내고 싶으므로, 0ㅏC=>M' 의 증명이 가능하면 C의 의미가 됩니다.

그것이 증명 불가능 하면 C의 의미는 없는 것입니다.


명령문 C : x:=1 ; y:=x+1; 

는  다음과 같은 증명으로 표현된다.



                     




이렇게 의미구조를 정의하는 방법을 자연스런, 구조적인, 혹은 관계형 의미구조라고 부릅니다.

natural semantics, structural operational semantics , relational semantics


1. 자연스런 : 추론규칙 꼴로 구성되어있고, 자연스런 추론 규칙(natural deduction rules)이라는 규칙때문에 이런 이름이 붙었다고 합니다.

2. 구조적: 계산 과정을 드러내는 의미구조 방식은 이전의 방식 보다 더욱 짜임새가 있었기 때문입니다.

 -   이전의 방식은 가상의 기계를 정의하고 프로그램이 그 기계에서 어떻게 실행되는지를 정의합니다. 이러다 보니, 기계의 실행과정중에 프로그램이 부자연스럽게 조각나면서 기계 상태를 표현하는 데 동원되기도 하고, 프로그램의 의미를 과도하게 낮은 수준의 실행과정으로 세세하게 표현하게 됩니다.

이런 방식을 구현을 통해서 정의하기(definition by implementation) , 어떻게 구현하는지 보임으로서 무엇인지 정의하기 라고 합니다.

프로그램의 구조마다 추론 규칙이 정의되고, 계산과정은 그 추론규칙들을 결합하여 하나의 증명이 되는 것입니다. 

- 의미규칙들이 한 집합을 귀납적으로 정의하는 방식이고 , 그 귀납이 프로그램이나 의미장치들의 구조를 따라 흐르기 때문입니다.


프로그램의 실행을 작은 보폭으로 정의해보자 <<small-step semantics>>


                                                                


                                                                


                                                        


           


                                        


                                          


                                                                



                                                                      


                                                                 


                                                                   


                                                                      



이 과정을 변이과정 의미구조(transition semantics) 라고도 합니다.

이 과정에서는 문법적인 부분들과 의미적인 부분들이 한데 섞이고 있습니다.


전통적으로는 것 모양을 구성하는 것과 속 뜻을 구성하는 것은 반드시 다른세계에 멀찌감치 떨어져  있어야 하지만, (Tarski :수학자가 시작한 전통)

문제될 것 또한 없습니다.


프로그램 이외의 의미장치 없이 프로그매만을 가지고도 변이과정 의미구조를 정의 할 수 있습니다.

예를 들어 메모리를 뜻하는 M이라는 프로그램이외의 의미장치 없이, 1+2+3 을 다시 쓰면 3+3 다시쓰면 6 으로 프로그램 이외의 다른 장치가 사용되지 않습니다.


위의 1+2+3 을 3+3으로 다시 6으로 다시 써 가는 과정이 프로그램의 실행과정과 같습니다.

이 경우 어느 부분을 다시 써야 하는가?, 다시 써야 할 부분을 무엇으로 다시 써야 하는가 ? 하는 것에 대한 정의가 필요합니다.


어느 부분을 다시 쓸것인가 하는 것은 실행 문맥(evaluation context)에 의해서 정의됩니다.

실행 문맥의 정의에 따라서 프로그램을 구성하다 보면, 다시 써야할 부분이 결정됩니다. 그 정의가 문법적으로 가능합니다.


실행 문맥을 가지고 있는 프로그램 K를 정의합니다.

K 안에는 [] 딱 하나가 있습니다. 그 곳이 프로그램에서 다시 써야할 부분, 먼저 실행되어야 할 부분이 됩니다.

이런 특징을 강조하기 위해서 "K[]" 라 쓰고, 그 빈칸에 들어있는 프로그램 부분 C까지 드러내어 "K[C]" 라고 표현합니다.


이 형식은 아래와 같은데요.



1. 지정문, 대입 처리에서 실행되어야 할 부분은 K는 오른쪽 식이고 x:=K 

2. 순차적 처리 구문 에서 명령문이 다음에 실행되어야 할 부분이라면, 앞의 명령문은 시행이 이미 끝났을 때 만이고, done; K 

3. 덧셈 식에서 오른쪽 식이 다음에 실행되어야 할 부분 이라면, 왼쪽 식의 결과는 나와 있어야 한다. v+K


위의 문법적인 표현을 다시쓰기 규칙을 정의하면, 아래와 같습니다.



그릭 속에서 어떻게 다시쓰이는가 하면, 즉,위의 규칙이 어떻게 동작하는지 보면,


                                                        

                                     

                      

                       

                                                    

                                                              

                                                                 




x:=1 ; y:=x+1

의 의미를 위의 문맥구조로 풀어보면,

1       ) 




2) 



3  ) 



4 )



5)   



이러한 과정으로 풀이된다.




C : 명령문

M: 메모리

K: 실행 문맥






가상의 기계를 통해서

프로그램의 의미는 어떤 가상의 기계(virtual machine)가 정의되어있고 그 프로그램이 그 기계에서 실행되는 과정으로 나타나게 됩니다.

기계에서 실행되는 과정은 기계상태가 매 스탭마다 변화되는 과정이 됩니다.


예를 들면, 정수식의 의미가 한 게계의 실행과정으로 다음과 같이 정의됩니다.


변수가 없는 정수식만을 생각해보면,



이는 소위 말하는 "스택머신 (stack machine)" 입니다. 이 기계는  스택 S 와 명령어  C 로 구성되어있습니다.



스택은 정수들로 차곡 차곡 쌓여있습니다.




명령어들은 정수식이나 그 조각 들이 쌓여있습니다.



기계 동작 과정의 한 스텝은 다음과 같이 정의됩니다.



               

  

           

<S,n.C>\quad\rightarrow\quad<n.S,C>\\

<S,E_1+E_2.C>\quad\rightarrow\quad<n.E_1\cdot E_2,+,C>\\

<n_2.n_1.S,+.C>\quad\rightarrow\quad<n.S> \quad\quad(n=n_1+n_2)\\

<n.S,-.C>\quad\rightarrow\quad<-n.S,C>


정수식 E의 의미는 <e,E> --> .... 입니다.



정수식이 아닌 우리가 프로그래밍에서 사용하는 command 형식으로 다시 정의해보면 아래와 같습니다.




그리고 , 기계 작동과정도 다음과 같이 정의 됩니다.

<S,push n.C> --> <n.S,C>

<n.S,pop.C>  --> <S,C>

<n1.n2.S,add.C>  --> <n.S,C>   (n = n1+n2)

<n.S,rev.C> --> <-n.S,C>


그리고 스택 머신 안에서 정수식들은 다음과 같은 변환됩니다.

[[n]] = push n

[[E1+E2]] = [[E1]].[[E2]].add

[[-E]] =  [[E]].rev



이러한 기계의 과정이 매우 임의적이라 생각될수 있습니다. 가상의 기계를 어떻게 디자인 하는가? 그 기계의 디테일은 어느 레벨에서 정의해야 하는가? 

그 대답은 프로그램의 의미를 정의하는 목적, 그에 따라 결정될 것이다.



[내 생각]

프로그래밍 언어가 어떤 래벨에서 정의되는 가가 프로그램이 돌아가는 환결과 시스템을 정의하는데 기준이 될것입니다.

프로그래밍 언어가 컴퓨터 system 에서 동작하는 프로그램을 작성하기 위한 용도이기 때문에, 프로그래밍 언어의 syntax나 semantic이 메모리에 한정되어 설계됩니다.

만약 프로그래밍 언어가 인터넷의 서비스나 인프라위에서 프로그램을 작성하기 위한 용도로 개발되었다고 한다면, 이때도 물론 임시적인 memory를 정의될 필요가 있지만, 훨씬 큰 의미로 접근 하게 됩니다. file open , memalloc, socket 등 local system에 대한 정의도 필요할지도 모르겠지만, 이런 레벨의 환경과 리소스를 접근하는 인터페이스는 아예 생략(숨기고)하고, openUrl, connectSite, httpRequest 등의 internet 리소스에 접근하기 위한 용도의 명령어들로 서비스 구축을 위한 언어를 설계 할 수 있을 것이라 생각됩니다.

(Java script 처럼 말이죠)



또 다른 예를 들면, 

어떤 프로그래밍 언어로 표현 하고 싶은 것이 "지구" 라는 행성이라고 하고 지구의 자원들을 다루는 프로그래램을 개발 한다고 하면, 

우리가 생각하는 프로그래밍 언어와는 모양이 많이 달라 질 수도 있습니다.

파일이나, 메모리, URL , WEB, HTTP 이런 용어나 computer level의 명령어가 아닌, 지구상의 객체들을 query 하고, 해당 객체에 메세지를 전달하고, 어떤 행동을 요구하고, 그에 대한 응답을 받고,

하는 형식의 명령어들로 정의 되지 않을까요? 


언어에는 표현 되지 않았지만, 그 밑에는 수많은 sub system들이 존재 하겠죠. GPS, WIFI, 각 객체들을 식별하기 위한 인식 장치, 모니터링 장치, 대상을 알 수없는 경우 모르는 대상에 대한 처리 시스템, 인력 관리, 수행에 필요한 지불 시스템... 등등..


하지만 언어에는 이런 서브시스템에 대한 내용이 표시 되지 않아도 된다면 더없이 편한 "지구 관리 프로그램"을 작성 할 수 있는 도구가 되겠죠.

(영화에서 많이 보던 시스템인데, 만약 프로그래밍 언어가 이런 래밸로 개발된다면, 가능 할 것도 같군요. 

현장에 요원을 투입하고, 회사를 설립하고,)

 


대개 가상 기계를 사용해서 프로그램의 의미를 정의하는 것은 프로그래밍 언어를 구현하는 단계에서 사용됩니다: 프로그래밍 언어의 번역기나 실행기를 구현할때 말이죠.

프로그래밍 언어의 성질을 굴이하거나 그 언어로 짜여진 프로그램들의 관심있는 성질을 분석할 때에 사용하는 의미구조로 사용되는 예는 드뭅니다.

가상 기계는 대개가 이와 같은 분석에서는 필요하지 않은 디테일을 드러내기 때문입니다.



기계중심의 언어로 빨리 넘어가고 싶기 때문에, 다음 내용들에 대해서는 현재는 생략 합니다. 다음에 기회가 되면 정리하도록 하겠습니다. 

- 궁극을 드러내는 의미 구조 (denotaional semantics)  

- 조립식일 수 있는 이유(의미공간 이론 : domain theory)

- CPO, 연속함수, 최소고정점



다음 >>기계중심의 언어




반응형


2. 기본기 


프로그래밍 언어가 어떤 형태로 되어있는가 를 이야기 하기 위해서는 기본적으로 사용하는 어휘들과 말하는 방식이 있습니다. 그 어휘와 방식의 기본이 수학입니다.


이런 베이스적인 부분들을 언급할 필요성이 있고, 이해해야할 필요 역시 있습니다.



귀납법 ( inductive definition)


집합을 정의하는 방법에는 조건제시법, 원소 나열법, 귀납법이 있습니다.

원소나열법 :{0,1, 참새, 비둘기}

조건 제시법 : {x|x는 1,2 의 양의 정수}

귀납법은, 

참 매력적인 분야라고 합니다. 무한한 것을 유한한 것들로 정의 할 수 있기 때문입니다.


프로그래밍 언어에서는 이 귀납법에 대한 이해가 반드시 필요합니다.


한번 귀납법에 대해서 살펴 볼까요? ( 저처럼 수학에서 멀어진지 오래된 사람들은 기억이 가물가물 하답니다.^^)


자연수의 정의를 귀납적으로 표현 하는 방법은 아래 같습니다.


방법 1


A.




3단 논법이죠?


또는

B.





B의 식은 사실 A를 살짝 수학스럽게(?) 표현한 것에 불과 합니다.

해석해보자면 "/은 자연수이다." 반드시 참인 식이어야겠죠.

분모의 정의("/#이 자연수이다.")를 만족하는 조건식 필요 조건은 "#은 자연수이다." 입니다.



방법 2

A.

음, 해석을 살짝 하자면,  "/ 가 자연수(N) 이다. /에 자연수(N)을 붙여도 자연수 이다." 정도로 이해하면 됩니다.

또는

B.





프로그래밍 언어에서는 다음과 같은 형식으로 표현됩니다.


그 예를 아래  나열 해 봤습니다.


자연수 집합



혹은 


 




스트링 집합



혹은




리스트


혹은 




정수 트리 



혹은




정수식 들의 집합








자연수 n은 정수 식이고, e가 정수식 이라면 그 앞에 음의 부호를 붙여도 정수식이고, 두개의 정수식 사이에 덧셈 부호나 곱셈 부호를 끼워넣어도 정수식이다.





간단한 프로그래밍 언어 표현


 


프로그래밍 언어에 대한 이야기 중인데 너무 귀납법이라는 것에 치우치게 되는것 같아서 나중에 나올 내용인데 미리 적어봤습니다. 


이 식을 처음 봤을때, 느낌이 강렬 했거든요. "아 이래서 프로그래밍 언어가 수학에서 나왔다라고 할 수있겠구나." 라는 생각을 했었으니까요.

정확한 표현은 아니지만, 이렇게 해석해보면 뭘 의도하는 표현식인지 이해가 쉬울 것입니다.

1. s -> x  syntax(또는 식) x  로 되어있다.

2. x는 e (expression : 식, 또는 명령어, 또는 상수 등등...) 이다.

3. x는 s;s 일 수도 있고, if e s s 일 수도 있고, print e 일 수도 있다.

4, s;s 는 각 식은 ; 으로 연결된다. (여러 언어들에서 문장의 맨 마지막에 ; 붙이는 것 말이죠)

5. if e s s  if 명령문은 e가 참이면 첫번째 s를 수행하고 두번째 s를 수행한다.

6. print e  e의 결과 값을 출력한다.


이렇게 해석해 보면, 프로그래밍 언어의 규칙을 표현한 것으로 보이죠?




형식논리

선언논리에서 생각하는 논리식 f는 다음의 귀납법칙으로 만들어 지는 집합의 원소이입니다.

논리식 이라고 하면, 전자공학에서 배우는 논리식을 생각 하실 수 있을 것입니다. 맞습니다. 바로 그것을 의미하는 것입니다.

그 논리식을 귀납법의 규칙에 따라 적으면 다음과 같습니다.








논리식을 다음과 같이 표현 방법을 바꿔봅시다.








implication :  ⊃ → 


implication 은 '논리 승' 이라고 표현하기도 하는데 아래와 같은 참,것짓 테이블을 가집니다.




A

 A

T





추론규칙


이제까지 참인지 거짓인지에 대한 표현 방법을 배웠다면, 논리식이 참인지 판별하는 방법에 대해서 고민해볼 차례인것 같습니다.

물론, 논리식의 의미를 정의한대로 따라가다보면 결과를 알수 있습니다. 음... 참, 혹은 거짓, 

논리식을 따라가면서 본다는 것이 결국, 프로그램을 돌려보고 그 프로그램이 참의 결과를 내는지 판별하는 것과 같습니다.



그런데, 혹시,,, 프로그램을 돌리지 않고 알아내는 방법은 없을까요? 논리식의 의미를 계산하지 않고 참인지 거짓인지 알수는 없을까?

논리식의 모양만을 보면서 참인지를 판별할 수는 없을까?


이런 규칙을 증명 규칙, 또는 추론 규칙이라고 합니다.




            





                       



                               




                          





                                



(감마, f) 쌍들의 집합을 만드는 규칙들이 위와 같습니다. 이는"감마에 있는 모든 논리식들이 참이면 f는 참" 이라는 의미를 갖습니다.

대개 형식논리에서는 "(감마, f)" 를 "감마 ㅏ f" 로 표기합니다.


이렇게 위의 식들을 다시 표시해보면,


(R1)     (R2)    (R3) (R4)




(R5)   (R6)




(R7)    (R8)



(R9)  (R10)



 

 

(R11) (R12)


  


의 증명을 위의 증명규칙으로 만들면 아래의 나무 구조가 됩니다. 

  





문법 구조와 의미 구조 







안전한 혹은 완전한


위의 규칙들이 만들어 내는  의 f는 모두 참인가? 반대로, 참인 식들은 모두 위의 증명규칙들을 통해서  꼴로 만들어지는가?

첫 질문에 예라고 답할 수 있다면, 우리의 규칙은 안전하다고 , 믿을만 하다고 한다.

둘째 질문에 예 라고 답할 수 있으면 유리의 규칙은 완전하다고, 빠뜨림이 없다고 한다.


증명 규칙들이 안전하기도 하고 완전하기도 하다면, 참인 식들만 빠짐없이 만들어 내는 규칙이 된다.



안전하냐 완벽하냐는 기계적인 증명규칙들의 성질에 대한 것이다.


그 기계적인 규칙들의 성질은 오직 우리가 생각하는 의미에 준해서 결정될 수 있다.


프로그램에서 그 겉모양(문법)과 속내용(의미)은 대개 다른 두개의 세계로 정의되고, 프로그램을 관찰하고 분석하는 모든 과정은 기계적으로 정의된다.

컴퓨터로 자동화하기 위해. 그 기계적인 과정들의 성질들(과연 맞는지, 무엇을 하는 것인지) 등에 대한 논의는 항상 그 과정의 의미나 결과물들의 의미를 참조하면서 확인된다.





다음 >> 모양과 뜻




반응형

 

1. 컴퓨터는 프로그래밍 언어에서 시작되었다!!.

 

대부분 프로그래밍 언어란 무엇인가요? 라는 질문에, 컴퓨터에서 돌아가기 위해 만들어진 언어 라고 대답할 것입니다.

 

맞는 예기죠. 당연한 것이죠.

 

그런데 컴퓨터는 왜 만들어 졌나요? 라고 질문한다면.... 어떻게 대답해야 할까요?

필요에 의해서? 그럼 무슨 필요에 의해서 일까요?

 

이 질문에 답하기 위해서는 컴퓨터의 시초로 거슬러 올라가서 한계단 한계단 밟으면서 컴퓨터 사이언스 역사를 살펴봐야 합니다.

 

결론 부터 예기하고 이것 저것 살펴보죠. 

 

"컴퓨터는 프로그래밍 언어를 구동시키기 위해 만들어 졌습니다."

즉, 컴퓨터 보다 프로그래밍 언어가 먼저 만들어졌고, 이를 확인하고 증명하는 과정에서 컴퓨터가 만들어 졌습니다.

정말이냐구요?

 

자.간략히 프로그래밍 언어의 시초에 대해서 언급하겠습니다.

1920년대 당대에 위대한 수학자 중 하나였던  힐베르트(다비트 힐베르트) 할아버지가,"모든 참인 명제를 증명할 수 있는 공리계가 있다" 라는 가설을 만들었습니다.

당시 힐베르트는 기하학(유클리드 기하학)을 공리화 하였고, 공간을 정의하여 함수해석학의 기초를 닦았다고 합니다. 아마 이런 과정에서 힐베르트는 위와 같이 모든 것은 증명할 수 있는 어떤것이 있다는 가설을 세우게 되었습니다.

이 의미는 간단하게 "모든 수학적 명제는 참인지 거짓인지 자동으로 계산 할 수 있다."라는 것이었습니다.

 

1930년대 초 쿠르트 괴델이 불완정성 정리를 증명하면서, 수학의 형식화가 태생적으로 한계가 있다는 것을 증명 했습니다. <불완전성의 원리>

1936년 쿠르트 괴델의 증명을 보고 , 앨런 튜링은 다른 형식으로 이를 증명하게 됩니다.

이때 나온게 튜링 머신이라는 것인데, 초보적인 컴퓨터라고 이해하는 사람들이 있는데, 초보적인 기계어라고 이해 하는것이 더 맞을 것입니다.

 

튜링 머신 자체가 실제로 존제하는 기계가 아니라 이론이었고, 표현 자체가 기호와 수학에 의해 정리되어있기 때문입니다.<튜링머신>

 

이와 비슷한 시기인 1930년대 알론조 처치가 람다 대수의 형식을 제안하였습니다.<람다 대수>

람다대수는 계산이론, 언어학 등에 중요한 역할을 하고, 프로그래밍 언어 이론의 발전에 토대가 되었고, 리스프, 함수형 프로그래밍 언어는 람다 대수로부터 직접적인 영향을 받아 탄생했으며, 단순 타입 람다 대수는 현대 프로그래밍 언어의 타입 이론의 기초가 되었습니다.

 

이와 같이 1930년대가 Computer라는 정의가 처음 시작되던 시기인데, 힐베르트의 가설이 불가능 하다는 것을 증명하기 위해서 고안된것이 컴퓨터의 시초이고 프로그래밍 언어의 시초입니다. 

힐베르트의 가설은 참, 매력적인데, "모든 참인 명제를 증명할 수 있는 공리계가 있다" 는 것이 만약 참으로 증명되었다면, 현재의 컴퓨터는 아마도 더 완벽한 것이었을 거라고 생각됩니다.

 

그러나 아이러니하게도, 우리는 컴퓨터는 이 명제가 틀렸다는 증명을 위해 고안되었습니다.

(이것이 불행일까요? 인간이 불완전 하다는 것을 생각해본다면, 뭐 나쁘진 않다고 봅니다. 오히려 발전의 가능성이 더 있다고 생각해 볼수 있겠죠... 뭔가 철학적인 ...)

 

때문에, 이런 거짓인 명제를 포함한 채로 Computer science 와 Computer engineering 은 발전해왔고, 그래서 지금도 여전히 불완전 한것은 맞습니다.

 

아무튼, 튜링이 튜링머신이라는 가상의 기계를 만들고, 그 기계가 동작하는 방식을 설명했는데, 기계라고 표현했지만, 기계적 장치는 없고, 이는 현재 프로그래밍 언어가 컴퓨터를 동작시키는 방식을 설명한 것이었습니다..

(지금의 CPU,RAM, 출력장치 등의 기본 개념이 이때 제안 되었다고 봐도 됩니다.)

 

 

이런 일련의 사건들을 보면, 컴퓨터가 없던 시절에 프로그래밍 언어의 시초가 되는 튜링 머신이나, 람다 대수식 등이 등장했고, 이후 이 튜링머신이나 람다 대수를 동작시켜볼 수 있는 장치로 컴퓨터가 고안되고 발전 했다 라고 할 수 있습니다.(실제로 그랬구요.)

 

개인적으로는 별로 생각해보지 않았던 부분이고, 누군가 질문했다면, "글쎄요.." 라고 대답 했을 것 같았는데, 막상 여기저기 찾아보고, 조사해보니, 프로그래밍 언어와 컴퓨터라는 것에 대해서 새로운 시각을 갖게 되는 것 같습니다.

 

 

힐베르트의 가설이 잘못된 것임을 증명한, 괴델(리커시브 함수), 튜링( 정지 문제), 처치( 결정 불가능 문제) 는 본질적으로 같은 것이라는 사실도 재미 있습니다.

 

 

 

 

다시 한번, 컴퓨터는 왜 만들어 졌나요?  라고 질문 한다면,

이제 우리는 프로그래밍 언어를 돌리기 위해서 컴퓨터가 만들어 졌어요.. 라고 답할 수 있습니다.

 

 

 

이러면 날 수 있을 꺼라 생각 했었던 시절.

 

 

<< Appendix >>

 

튜링기계

 



"...무한한 저장공간은 무한한 길이의 테이프로 나타나는데 이 테이프는 하나의 기호를 인쇄할 수 있는 크기의 정사각형들로 쪼개져있다. 언제든지 기계속에는 하나의 기호가 들어가있고 이를 "읽힌 기호"라고 한다. 이 기계는 "읽힌 기호"를 바꿀 수 있는데 그 기계의 행동은 오직 읽힌 기호만이 결정한다. 테이프는 앞뒤로 움직일 수 있어서 모든 기호들은 적어도 한번씩은 기계에게 읽힐 것이다"
 - 튜링 ,1948, p.61

 

 

 

1935년 봄, 케임브리지 킹스 칼리지의 젊은 박사 과정 학생이었던 튜링은 한 과제에 직면했다. 그는 논리학자 뉴먼의 강의에 자극을 받았으며, 결정 문제에 대한 괴델의 연구에 대해 알게되었다. 뉴먼은 '기계적'이라는 단어를 사용했으며, 1955년 튜링의 부고에 뉴먼은 다음과 같이 기술 했습니다.

 

 



" '[기계적] 과정이란 무엇인가?'라는 질문에 튜링은 '기계로 할 수 있는 것' 이라는 답을 내 놓았다."
- Gandy, p.74

 

 

테이프 튜링기계

 

 

M= \left \langle Q, \Gamma, b, \Sigma , \delta , q_{0}, F \right \rangle

 

  • Q는 유한하고 비어있지 않은 상태들의 집합
  • \Gamma는 유한하고 비어있지 않은 기호와 알파벳들의 집합
  • b\in\Gamma는 비어있음을 알려주는 기호 (테이프 위에서 유일하게 무한하게 나타날 수 있는 기호)
  • \Sigma \subseteq \Gamma \backslash \left \{ b \right \}는 입력가능한 기호들의 집합
  • q_{0} \in Q는 초기상태
  • F \subseteq Q는 최종상태, 또는 수락 상태
  • \delta : Q \backslash F \times \Gamma \rightarrow Q \times \Gamma \times \left \{ L, R \right \}는 부분함수

 

 

 

 

 

 

람다 대수 Lambda Calculus

 

 

 

  • 알파-변환(α-conversion): 속박 변수를 바꿈
  • 베타-축약(β-reduction): 식의 인수에 함수를 적용
  • 에타-변환(η-conversion): 외연성을 통해 축약 (겉으로 보이는 행동이 같은 함수는 동일 함수로 간주함)
 
 

 

 

 

다음 >> 기본기

 

반응형

C++ 11 을 사용하기 위해서는 gcc 의 경우에는 -std=c++11를 넣어줘야 합니다.

 

CMake 를 이용할 경우 아래와 같이 사용할 수 있습니다.

SET(CMAKE_CXX_FLAGS "-std=c++0x")

 

 

  • GCC: 4.5. You must specify the -std=c++11 option.
  • Intel C++ Compiler: 11. Specify the /Qstd=c++0x option.
  • Microsoft Visual C++ 2010 (included in Visual Studio 2010).
 
 
 
 
 

 

 

반응형


CPU architecture  구분을 위한 predefined macro 입니다.



Please send updates/corrections to predef-contribute.

Alpha

TypeMacroDescription
Identification__alpha__Defined by GNU C
Version__alpha_ev'V'__V = Version
Identification__alphaDefined by DEC C
Identification_M_ALPHADefined by Visual Studio
Example
CPUMacro
Alpha EV4__alpha_ev4__
Alpha EV5__alpha_ev5__
Alpha EV6__alpha_ev6__

AMD64

TypeMacroDescription
Identification__amd64__
__amd64
__x86_64__
__x86_64
Defined by GNU C and Sun Studio
Identification_M_X64
_M_AMD64
Defined by Visual Studio

Notice that x32 can be detected by checking if the CPU uses the ILP32 data model.

ARM

TypeMacroDescription
Identification__arm__Defined by GNU C and RealView
Identification__thumb__Defined by GNU C and RealView in Thumb mode
Version__ARM_ARCH_'V'__V = Version

Defined by GNU C 1
Identification__TARGET_ARCH_ARM
__TARGET_ARCH_THUMB
Defined by RealView
Version__TARGET_ARCH_ARM = V
__TARGET_ARCH_THUMB = V
V = Version
Version__TARGET_ARCH_'VR'VR = Version and Revision
Identification_ARMDefined by ImageCraft C
Identification_M_ARMDefined by Visual Studio
Identification_M_ARMTDefined by Visual Studio in Thumb mode
Version_M_ARM = VV = Version
Example
CPUMacro_M_ARM
ARM 2__ARM_ARCH_2__
ARM 3__ARM_ARCH_3__
__ARM_ARCH_3M__
ARM 4T__ARM_ARCH_4T__
__TARGET_ARM_4T
ARM 5__ARM_ARCH_5__
__ARM_ARCH_5E__
5
ARM 5T__ARM_ARCH_5T__
__ARM_ARCH_5TE__
__ARM_ARCH_5TEJ__
ARM 6__ARM_ARCH_6__
__ARM_ARCH_6J__
__ARM_ARCH_6K__
__ARM_ARCH_6Z__
__ARM_ARCH_6ZK__
6
ARM 6T2__ARM_ARCH_6T2__
ARM 7__ARM_ARCH_7__
__ARM_ARCH_7A__
__ARM_ARCH_7R__
__ARM_ARCH_7M__
__ARM_ARCH_7S__
7

ARM64

TypeMacroDescription
Identification__aarch64__Defined by GNU C 1

Blackfin

TypeMacroDescription
Identification__bfin
__BFIN__
Defined by GNU C

Convex

TypeMacroDescription
Identification__convex__Defined by GNU C
Version__convex_'V'__V = Version
Example
CPUMacro
Convex C1__convex_c1__
Convex C2__convex_c2__
Convex C32xx series__convex_c32__
Convex C34xx series__convex_c34__
Convex C38xx series__convex_c38__

Epiphany

TypeMacro
Identification__epiphany__

HP/PA RISC

TypeMacroDescription
Identification__hppa__Defined by GNU C
Identification__HPPA__Defined by Stratus VOS C
Identification__hppa
Version_PA_RISC'V'_'R'V = Version
R = Revision

See also OpenPA.net.

Example
CPUMacro
PA RISC 1.0_PA_RISC1_0
PA RISC 1.1_PA_RISC1_1
__HPPA11__
__PA7100__
PA RISC 2.0_PA_RISC2_0
__RISC2_0__
__HPPA20__
__PA8000__

Intel x86

TypeMacroFormatDescription
Identificationi386
__i386
__i386__
Defined by GNU C
Version__i386__
__i486__
__i586__
__i686__
Defined by GNU C
Identification__i386Defined by Sun Studio
Identification__i386
__IA32__
Defined by Stratus VOS C
Identification_M_I86Only defined for 16-bits architectures

Defined by Visual Studio, Digital Mars, and Watcom C/C++ (see note below)
Identification_M_IX86Only defined for 32-bits architectures

Defined by Visual Studio, Intel C/C++, Digital Mars, and Watcom C/C++
Version_M_IX86V00V = Version
Identification__X86__Defined by Watcom C/C++
Identification_X86_Defined by MinGW32
Identification__THW_INTEL__Defined by XL C/C++
Identification__I86__Defined by Digital Mars
Version__I86__VV = Version
Identification__INTEL__Defined by CodeWarrior

Notice that Watcom C/C++ defines _M_IX86 for both 16-bits and 32-bits architectures. Use __386__ or _M_I386 to detect 32-bits architectures in this case.

Notice that the Stratus VOS is big-endian on IA32, so these macros cannot be used to detect endianness if __VOS__ is set.

Example
CPU_M_IX86__I86__
803863003
804864004
Pentium5005
Pentium Pro/II6006

Intel Itanium (IA-64)

TypeMacroFormatDescription
Identification__ia64__
_IA64
__IA64__
Defined by GNU C
Identification__ia64Defined by HP aCC
Identification_M_IA64Defined by Visual Studio
Identification_M_IA64Defined by Intel C/C++
Version_M_IA64?
Identification__itanium__Defined by Intel C/C++
Example
CPU_M_IA64 (Intel C/C++)
64100

Motorola 68k

TypeMacroDescription
Identification__m68k__Defined by GNU C
Version__mc'V'__
__mc'V'
mc'V'
V = Version
IdentificationM68000Defined by SAS/C
Identification__MC68K__Defined by Stratus VOS C
Version__MC'V'__V = Version
Example
CPUMacro
68000__mc68000__
__MC68000__
68010__mc68010__
68020__mc68020__
__MC68020__
68030__mc68030__
__MC68030__
68040__mc68040__
68060__mc68060__

MIPS

TypeMacroDescription
Identification__mips__
mips
Defined by GNU C
Version_MIPS_ISA = _MIPS_ISA_MIPS'V'V = MIPS ISA level
Version_R3000
_R4000
_R5900
Identification__mipsDefined by MIPSpro and GNU C
Version__mipsThe value indicates the MIPS ISA (Instruction Set Architecture) level
Version__MIPS_ISA'V'__V = MIPS ISA level
Identification__MIPS__Defined by Metrowerks
Example
CPU_MIPS_ISAGNU C Macro__mipsMIPSpro Macro
R2000_MIPS_ISA_MIPS11
R3000_MIPS_ISA_MIPS1_R30001
R6000_MIPS_ISA_MIPS22__MIPS_ISA2__
R4000_R4000
R4400_MIPS_ISA_MIPS33__MIPS_ISA3__
R8000_MIPS_ISA_MIPS44__MIPS_ISA4__
R10000_MIPS_ISA_MIPS44__MIPS_ISA4__

PowerPC

TypeMacroDescription
Identification__powerpc
__powerpc__
__powerpc64__
__POWERPC__
__ppc__
__ppc64__
Defined by GNU C
Version__ppc'V'__V = Version
Identification_M_PPCDefined by Visual Studio
Version_M_PPC?
Identification_ARCH_PPC
_ARCH_PPC64
Defined by XL C/C++
Version_ARCH_'V'V = Version
Version__PPCGECKO__Gekko

Defined by CodeWarrior
Version__PPCBROADWAY__Broadway

Defined by CodeWarrior
Version_XENONXenon
Example
CPU_M_PPCMacroXL Macro
PowerPC 440_ARCH_440
PowerPC 450_ARCH_450
PowerPC 601601__ppc601___ARCH_601
PowerPC 603603__ppc603___ARCH_603
PowerPC 604604__ppc604___ARCH_604
PowerPC 620620

Pyramid 9810

TypeMacro
Identificationpyr

RS/6000

TypeMacroDescription
Identification__THW_RS6000Defined by XL C/C++
Identification_IBMR2
Identification_POWER
Identification_ARCH_PWR
_ARCH_PWR2
_ARCH_PWR3
_ARCH_PWR4

SPARC

TypeMacroDescription
Identification__sparc__Defined by GNU C
Identification__sparcDefined by Sun Studio
Version__sparcv8
__sparcv9
Defined by Sun Studio
Example
CPUMacro
SPARC v8 (SuperSPARC)__sparcv8
SPARC v9 (UltraSPARC)__sparcv9

SuperH

TypeMacroDescription
Identification__sh__Defined by GNU C
Version__sh1__
__sh2__
__sh3__
__SH3__
__SH4__
__SH5__

SystemZ

TypeMacroDescription
Identification__370__
__THW_370__
Identifies System/370

Defined by XL C/C++
Identification__s390__Identifies System/390

Defined by GNU C
Identification__s390x__Identifies z/Architecture

Defined by GNU C
Identification__zarch__Identifies z/Architecture

Defined by clang
Identification__SYSC_ZARCH__Identifies z/Architecture

Defined by Systems/C

TMS320

TypeMacroDescription
Identification_TMS320C2XX
__TMS320C2000__
C2000 series
Identification_TMS320C5X
__TMS320C55X__
C5000 series
Identification_TMS320C6X
__TMS320C6X__
C6000 series
Example
DSPMacro
C28xx_TMS320C28X
C54x_TMS320C5XX
C55x__TMS320C55X__
C6200_TMS320C6200
C6400_TMS320C6400
C6400+_TMS320C6400_PLUS
C6600_TMS320C6600
C6700_TMS320C6700
C6700+_TMS320C6700_PLUS
C6740_TMS320C6740

TMS470

TypeMacro
Identification__TMS470__


+ Recent posts