SICP_CH1_1_프로그래밍 기본 요소

빡커·2025년 3월 18일
0

Learning/SICP

목록 보기
1/1

추상화

  • 관념 (idea) → 우리가 마음속에서 떠올리는 생각이나 개념
  • 단순 관념 (simple idea) → 더 이상 쪼갤 수 없는 가장 기본적인 개념
    • 예시: ‘빨강’(색깔 개념), ‘둥글다’(형태 개념), ‘차갑다’(온도 개념)
  • 복합 관념 (compound idea) → 여러 개의 단순 관념을 합쳐서 만든 개념
    • 예시: ‘사과’(빨강 + 둥글다 + 차갑다)
  • 복잡한 관념 (complex idea) → 복합 관념과 같은 의미로 쓰임 (더 많은 개념이 합쳐진 것)
  • 관계 관념 (relation idea) → 둘 이상의 관념을 비교해서 만드는 개념
    • 예시: ‘더 크다’, ‘더 작다’, ‘옆에 있다’ 같은 개념
  • 추상화 (abstraction) → 특정한 사례에서 공통적인 특징만 뽑아서 일반적인 개념을 만드는 과정
    - 예시: ‘개의 관념’ → 여러 종류의 개(골든 리트리버, 푸들 등)에서 공통적인 특징(4발, 짖음)을 추출하여 만든 개념


  1. 단순한 개념들을 조합하면 더 복잡한 개념(복합 관념)이 된다.
  2. 두 개념을 비교하면 관계에 대한 개념(관계 관념)이 생긴다.
  3. 불필요한 요소를 빼고 공통적인 특징만 남기면 더 일반적인 개념(추상화)이 된다.

계산적 과정 computational process

  • 계산적 과정 (Computational Process)

    • 컴퓨터에서 어떤 연산을 수행하는 과정
    • 단순한 계산보다 더 넓은 개념 (예: 반복문, 조건문, 알고리즘 실행 등)
    • 실제 세상에 존재하는 것이 아니라, 컴퓨터 내부에서 실행되는 추상적인 개념
  • 과정 (Process)

    • 어떤 일을 단계적으로 수행하는 것
    • "계산적 과정"이란 결국 컴퓨터가 수행하는 일련의 계산 및 연산 과정을 의미함.

    🔹 과정과 ‘점차 전개됨’

  • 컴퓨터에서 실행되는 과정(process)은 한 번에 끝나지 않고, 단계적으로 진행됨.

  • 즉, 프로그램이 실행되면서 여러 개의 연산이 차례대로 수행됨.

  • 알고리즘 (Algorithm)

    • 문제를 해결하기 위한 명확한 절차나 규칙의 집합
    • 컴퓨터는 이 알고리즘을 계산적 과정으로 실행함.
    • 예: 두 숫자의 최대공약수를 구하는 방법
  • 재귀적 과정 (Recursive Process)

    • 어떤 함수나 과정이 자기 자신을 반복적으로 호출하는 것
    • 예: 팩토리얼 계산 (n! = n * (n-1)!)
  • 반복적 과정 (Iterative Process)

    • 일정한 조건이 만족될 때까지 같은 연산을 계속 반복하는 것
    • 예: for 루프, while 루프

  • 데이터 (Data)

    • 컴퓨터가 처리하는 정보 (숫자, 문자, 이미지 등)
  • 프로그램 (Program)

    • 컴퓨터가 실행할 수 있도록 작성된 명령어들의 집합
    • 프로그램이 곧 계산적 과정(computational process)을 제어하는 도구
  • "프로그램 = 주문(Spell)"?

    • 프로그래머는 컴퓨터에 명령을 내리는 역할을 함
    • 프로그램을 작성하는 행위가 마치 "마법 주문을 외우는 것"과 같다는 비유

자바스크립트

주문을 외우기 위해선 언어가 필요하다.
그러한 언어로 자바스크립트를 채택함. LISP의 방언인 Scheme의 주요 아이디어와, 셀프의 핵심 기능들을 전부 물려받아 완성되었기 때문

스킴: 어휘순 범위(lexically scoped), 일급함수(first-class function)

셀프: 프로토타입 기반 객체지향 언어

웹 페이지 개발자들은 자바스크립트 코드를 페이지내에 실어 넣고, 웹브라우저 개발자들은 자바스크립트 해석기(인터프리터 및 컴파일러)를 탑재하는 형태

1.1 프로그래밍의 기본 요소

프로그래밍 언어는 컴퓨터가 수행할 일을 지시하는 수단이다. 그런데 이 지시를 표현할때 다음 규칙이 없다면 강력한 프로그래밍 언어라고 할 수 없다.

  1. 원시 표현식 (primitive expression): 가장 단순한 개체를 나타내는 표현 ex) x, 1, 0b123
  2. 조합(combination) 수단: 단순한 요소들을 복합적으로 만드는 것 ex) int x = 10;
  3. 추상화(abstraction) 수단: 복잡적인 요소에 이름을 붙여 하나의 단위로 다루는 것 ex) add();

단순비교: 함수는 데이터를 다루는 규칙. 데이터는 재료

고급비교: 함수나 데이터는 그리 크게 다르지 않다.

1.1.1 표현식 expression

표현식은 컴퓨터가 이해하는 어떤 식을 의미한다. 수학에서 말하는 단항식일 수도 다항식일 수도 있다. x일지 2x+3y일지 모르지만 하여간 이런 것들이 식이다.

이러한 expression에 semicolon(;) 을 붙이면 문장(statement)가 되고 비로소 해석기는 표현식의 결과에 응답하게 된다. 표현식은 문장의 재료라 할 수 있음

문장은 반드시 하나 이상의 primitive expression이 포함되어 있다.
여러 표현식이 포함된 표현식을 특히 combination이라고 칭함.
일반적으로 가장 흔한 피연산자(operand) 연산자(operator) 피연산자(operand) 이런식의 피연산자 사이에 연산자가 오는 수식같은 형태를 중위 표기법(infix notation)이라고 함.

수식 안에 괄호로 수식의 결과를 피연산자로 만드는 방식을 중첩(nesting) 이라 하고 이는 프로그래밍 언어에서도 흔히 쓰인다. 인간이 알아보기 힘든걸 알아보기 쉽게 만들어주지만, 논리만 같다면, 프로그램은 괄호를 쓰든 안쓰든 같은 속도로 처리해버린다.

REPL(Read Evaluate Print Loop): 우선순위에 맞게 읽고 평가하여 결과를 내뱉는 행위를 빠른 속도로 반복한다고 하여 붙여진 행위의 명칭이다.

REPL은 특별한 호출 명령없이 즉각적으로 해석되는 동작이다.

1.1.2 이름 붙이기와 환경

계산적 객체(computational object)에 이름(name)을 붙일 수 있는 것은 프로그램언어의 필수 기능이다.

실제로 어떠한 컨셉을 표현하는데 있어 엄청나게 복잡할 수 있는 데이터나 식을 간단한 이름하나만으로 연산에 참여 시킬 수 있어야. 더 추상화되고 복잡한 개념을 표현 할 수 있어진다.

자바 스크립트는 모든 문장이 값이라는 관례를 따른다. 이 관례와 자바스크립트 프로그래머들이 효율성은 신경쓰지 않는다는 평판이 자바스크립트 프로그래머는 모든 것의 가치를 알지만 그 대가는 전혀 모른다는 문구로 이어진다.
위의 말이 이해가 안되었었는데 풍자적 표현이었다.

자바 스크립트는 모든 문장이 값이라는 관례를 따른다. -> 실제로 const size = 2; 라는 문장은 그냥 단순한 할당 문이 아니라 그 자체로 값을 반환한다. 이걸 보여주는 대표 사례는 다음과 같다.

int a; //선언문
int b = (a = 5) * 2; //할당과 동시에 5를 반환하기 때문에 연산에 참여가 되는것



모든 것의 가치를 알지만 -> 자바스크립트는 모든 문장이 함수가 전부 value로 처리되기 때문

그 대가는 전혀 모른다 -> 유연함과 함수형 프로그래밍 덕분에 발생하는 무수히 많은 바레이션이 다양한 스타일을 만들고, 다양한 스타일 때문에 협업에서의 cost 발생을 우려하는 표현

환경(environment)

이름과 값의 쌍을 만드는 것을 지원하기 위해서 컴파일러가 해석한 내용들이 결국 어딘가에 저장되어 있어야함. 그 공간을 환경이라고 표현함. 환경은 여러 곳이 존재 할수 있고, 하나의 연산에 다른 여러 환경을 참조하는 경우도 많다.

1.1.3 연산자 조합의 평가

연산자(Operator): 특별한 규칙 및 우선순위가 지정된 기호 및 키워드. 산술 연산, 논리 연산, 할당 연산, 선언 연산 등등 다양한 원시동작을 구현함.

조합(Combination): 연산자와 피연산자가 결합된 조합. 그리고 그 조합과 조합이 다시 연산자로 결합된 복합 조합 등 연산자와 피연산자가 뭉쳐진 형태를 조합이라 함.

평가(Evaluation): 컴파일러가 하는 행위인데, 결국 하는건 문장 내의 표현식을 value화 하는 걸 칭함.

연산자 조합 평가: 연산자 조합을 평가하는 과정에는 순서(우선순위)가 존재함.
연산자 우선순위에 따라 평가 순서가 결정됨.
연산이 계층적으로 나뉘며, 이를 트리(Tree) 구조로 나타낼 수 있음.
낮은 계층의 연산(우선순위가 높은 연산)이 먼저 평가됨.
이러한 과정은 재귀적인 아이디어이다. 복잡한 산술연산을 처리하기 위해 산술연산을 반복 호출하되 연산 결과가 하나씩 벗겨져서 최종 연산까지 전달된다.

재귀적(Recursive): 자기 자신을 호출하는 방식으로 문제를 해결하는 방법. 큰 문제를 해결하기 위해 끝나는 조건이 존재하는 작은 문제를 해결하고 그 해결된 값을 가지고 다시 한단계 큰 문제를 해결하는 형태로 최종 문제해결까지 반복함.

트리(Tree): 재귀적인 데이터를 표현하기에 적합한 구조 형태. 노드와 간선으로 표현이되면 전부 트리인데, 복합 조합 산술연산의 경우 가장 최종에 해야될 연산을 루트 노드에 놓고, 그 아래가지에 바로 피연산자들을 놓는다. 피연산자가 또 조합이라면 역시나 그 노드에는 피연산자의 최종 연산을 노드에 놓는다. 이를 반복하다보면 가장 작은 브랜치 노드에는 숫자들이 깔리게 되는데 이 숫자들을 부모 노드와 결합한 결과를 부모노드와 교체하면서 재귀적으로 문제가 해결됨을 표현할 수 있게 되는 것.

트리 누산(tree accumulation): 위에 다 설명해버렸다. 위로 계산 결과를 보내면서 누산한다고 해서 트리 누산이라 표현한다.

원시 표현식 평가 규칙:
1. 수치의 값은 해당 숫자들이 나타내는 바로 그 값이다.
2. 이름의 값은 현재 환경에서 그 이름에 연관된 객체이다.

키워드(keyword): 이름짓기로 사용될 수 없는 특별한 단어들이다. 컴파일러는 이 키워드들이 어떻게 배치되어 있냐에 따라서 각기 다른 동작을 구현하는 것

구문형(syntactic form): 각 키워드마다 반드시 따라야하는 규칙이 있고, 해당 규칙을 따른 형태가 구문형이다. 함수 선언형, 클래스 선언형, 변수할당형, 산술연산형 등등 여러 구문형이 있다.

구문론(syntax): 문법을 의미하는것, 문법이 올바른지 아닌지는 키워드의 시작과 구현 배치 등을 통해 컴파일러가 판단한다.

1.1.4 복합 함수

앞서 단순한 수치계산을 위해서 원시 표현식들과 원시 연산자들을 이용하여 했던 수치 계산을 넘어 더욱 복잡한 연산자를 만드는 방법이 무엇이냐? 그게 함수이다.

프로그래밍 언어는 원시타입을 제공하고, 프로그래머는 필요한 함수를 원시타입을 이용해 만들어야한다

그게 추상화다. 중첩과 조합의 수단을 이용하여 추상화를 하는것

함수 선언: 변수 선언과 마찬가지로 추상화 기법이며, 이름을 붙이는거다. 변수 선언은 간단한 표현식에 이름을 매겼다면, 함수선언은 좀 더 복잡한 조합에 이름을 붙인다고 일단 이해.

function 이름(매개변수들) { return 표현식;}

함수 적용의 순서
1. 함수 표현식과 인수 표현식을 각각을 전부 부분 평가. 일단 잘라 놓고 시작. 중첩되더라도 이는 마찬가지
2. 평가 이후 각각의 값이 도출되면 함수 값(구현된 그 내용)을 안쪽에서부터 표현식 값에 하나씩 적용하여 적용 값을 토해낸다.

1.1.5 함수 적용의 치환 모형

복합 함수의 적용을 표현하는 모형(Modeling)이다. 자세한 내용은 책에서.

핵심은
1. 함수표현식을 만나면 1.1.4에서 보았듯 표현식을 인수와 함수로 분리하여 적용하기 시작한다는 것.
2. 그렇게 단순 인수에 함수 표현식을 적용해보려했더니 반환 타입이 원시 연산자가 아닌 또 다른 정의된 함수라면? 다시 1번을 반복한다는 것. 그렇게 최종 값이 만들어지면 그게 연산의 결과라는 것.

인수 우선 평가 대 정상 순서 평가

앞서 예시를 들었던 모든건 인수 우선 평가에 관한 얘기였음.

실제 평가 순서를 정의하고 이를 모델링 하는 방법에는 한가지 방법만 있는게 아닌데, 대표적인 대척점이 정상 순서 평가라고 하는 방법임

자바스크립트는 인수 우선 평가 방식으로 평가를 진행하니 그대로 알면 되고, 정상 순서 평가가 어디서 어떤 가치를 드러내는지는 4장에서 나온다고 한다.

정상 순서 평가를 지금 요약하자면, 계산을 전부 전개한뒤에 해당 계산을 한번에 수행하여 값을 도출하는 거임.

예를들어야 확실히 설명이 됨. 인수 우선평가는 f(6)을 만날때 일단 평가 먼저.(인수도 함께 평가가 된다는 거임.) 함수표현식: sum_of_square(x+1 , x+2) | 인수표현식: 6 -> 적용! -> sum_of_square(6+1, 6+2) -> 평가! -> 함수표현식:square(x) + square(y) | 인수표현식: 7, 8 -> 적용! square(7) + square(8) -> 이때 함수는 총 3개이므로(스퀘어2개 + 1개) 우선순위에따라 위의 행위를 반복한다. 즉 인수가 연산에 계속 직접 찹여함.

정상 순서 평가는 거대한 복합함수를 함수표현식만 계속 평가하며 쫙 전개하고 막판에 x를 대입한다고 생각하면 됨.
f(x) -> sum_of_square(x +1, x+2) -> square(x+1) + square(x+2) -> ... 이런식으로해서 최종적으로 "원시 계산 단계까지 전개한뒤에 원시 연산 조합을 통한 수행"으로 대입해서 처리함.

겉으로 보기에는 정상 순서 평가가 뭔가 사람의 사고와 더 닮은 느낌인데, 프로그래밍적으로는 인수 우선 평가가 더 안전하다고 한다. 계산 순서에따라서 위법함이 나오는 경우가 있기 때문이라고 하는데 이는 뒤에 나온다고 한다...

1.1.6 조건부 표현식과 술어

조건부 표현식이 뭐냐?
조건부 + 표현식이다.
표현식이란: 컴퓨터가 하나의 값으로 이해하는 식이다.
정확히 표현하면 조건에따라서 결과값이 달라지는 식이 조건부 표현식이다.

조건문하고는 다르다. if else 얘는 식 하나로 즉 누군가의 값이 될 수 있는 식 하나에 조건 판별이 들어간다는 뜻이다! 그래서 강력한거고 그래서 중요하다.

수학으로 따지면 절대값 연산이 있다. x|x|

술어 ? 귀결 표현식 : 대안 표현식

술어(predicate)란 결과값이 항상 boolean인 true || false 인 표현식을 술어라고 표현한다. primitive 타입 뿐만 아니라, 조합 or 함수 or 복합함수도 결국 boolean 값으로만 귀결이 된다면 술어로 사용이 가능하다.

조건부 표현식의 해석
1. 술어 평가(evaluate predicate)
2. 귀결 표현식 평가 or 대안 표현식 평가.

여기서 중요한건 평가이다. 평가를 받는건 표현식 뿐만이 아니다. 마찬가지로 함수나 복합함수도 전부 평가 받을 수 있는건 뭐든지 올 수 있다는 것! 게다가 평가를 통해 결과를 반환해야되는데 이를 길게 늘여 뜨려 중첩을 무한으로 이어 버릴 수도 있다.

p: 술어(값) e: 표현식(값) 일때

p1 ? e1 : e2;
p1 ? e1 : (p2 ? e2: e3);
p1 ? (p2 ? e2: e3) : p3 ? e4: e3;

중첩의 예시이다. 우측 결합이란 점만 인식하고 정신 바짝 차리면 해결된다. 먼저 ?를 만난순간 ?의 결과를 나타내는 맨 오른쪽을 먼저 본다. 이후 오른쪽에 오는 ? : 를 기준으로 술어 ? 귀 : 대 관계를 파악하여 결과를 예측후 왼쪽으로 넘어간다. 다시 ? : 관계가 있다면 이를 반복하여 중첩을 평가한다.

정말 보기 힘들지만, 함수형 표현을 위해서는 상당히 자주 쓰이는 패턴이다.

원시 술어 연산자들 외에 복합 술어 연산자들에대해서도 볼필요가 있다. 이는 표현식의 값들을 단순히 같냐 크냐 다르냐로 비교하는게 아닌 논리가 같냐 논리가 다르냐 논리가 부정이냐 등 좀 더 고차원적인 결과를 내뱉기 때문이다.

  • 논리곱(logical conjunction): 술어 && 술어. 술어? 술어: false 의 문법적 설탕(syntactic sugar)
  • 논리합(logical disjunction): 술어 || 술어. 술어? true: 술어의 syntactic sugar
  • 논리부정(logical negation): !술어

좀 더 깊게

사칙 연산은 확실한 이항 연산자이다.
!는 논리부정으로 단항 연산자이다.
&&, || 는 연산자가 아니다.

엄밀하게 보면 따져보자. 연산자의 정의가 평가되고나서 인수 표현식을 이용해서 새로운 값을 정의하는게 연산자라고하면,
논리곱과 논리합은 인수를 전부 평가하지 않고, 앞에 인수만 보고도 바로 결과표현식으로 귀결되는 단락 평가(short circuit evaluation) 현상이 일어나고, 이를 연산했다고 생각하지 않고, 제어흐름을 컨트롤하는 구문형으로도 해석할 수 있는거지.

공식문서에서 아무리 찾아봐도 전부 그냥 논리연산자라고만 지칭하지 엄밀하게 따지진 않았는데, SICP에서는 Operator라고 인정하고 있지는 않고 있다.

연습문제

1.1 해석기 출력결과 쓰기

나의 답변

10
12
6
3 //a=3
4 //b=4
19
false
4 //자바스크립트에서 술어 %% 표현식에서 술어가 true라면 표현식으로 넘어가고 그냥 표현식의 결과가 답
16
6
16



오답노트

Welcome to Node.js v18.13.0.
Type ".help" for more information.
> 10;
10
> 5 + 3 + 4;
12
> 9 - 1;
8
> 6 / 2;
3
> 2 * 4 + (4 - 6);
6
> const a = 3;
undefined
> const b = a + 1;
undefined
> a + b + a * b;
19
> a === b;
false
> b > a && b < a * b ? b : a;
4
> a === 4 ? 6 : b === 4 ? 6 + 7 + a : 25;
16
> 2 + (b > a ? b : a);
6
> (a > b ? a : a < b ? b : -1) * (a + 1);
16
>

1.2 수식을 자바스크립트 표현식으로

나의 풀이

(5+4+(2-(3-(6+4/5)))) 
/
(3*(6-2)*(2-7));



1.3 세 개의 수를 받고 셋 중 가장 작은 것을 제외한 두 수의 제곱들을 합한 결과를 돌려주는 함수 선언

function square(x){
		return x*x;
}
function sum(x,y){
		return x + y;
}
function solution(a,b,c){
//		const min = a > b ? (b > c ? 'c' : 'b' ) 
//	c>b>a			: c > b ? 'a' 
	//	b>c>a		: c > a ? 'a' : 'c';		
		return a > b ? 
		(b > c ? sum(square(a), square(b)) : sum(square(a), square(c))) 
		: c > b ? sum(square(c), square(b)) 
		: c > a ? sum(square(c), square(b)) : sum(square(a), square(b));	
}
solution(1, 2, 3);



1.4 앞에서 본 함수 적용 평가 모형은 함수 표현식 쪽이 복합함수여도 관계없다. 다음 복합 함수 작동방식 서술

function plus(a,b){return a+b;}
function minus(a,b){return a-b;}
function a_plus_abs_b(a,b){
		return (b>=0? plus: minus)(a,b);
}

/*
1. 일단 괄호를 만났으니 괄호 식을 평가 -> 조건부 표현식임을 ?를 통해 확인 우측결합 우선순위를 이용해서 하나씩 요소들을 평가
2. 대안식:minus(환경에 정의된 함수명으로 평가됨), 귀결식:plus(환경에 정의된 함수명으로 평가됨), 술어:b가 양수면 true로 귀결
3. 인자인 b를 대입하여 첫번째() 괄호를 해결
4. 두번째괄호는 자연스레 함수명과 결합하여 합수 적용식이 됨.
5. 결론적으로 양수면 플러스 음수면 마이너스가 되어 함수명대로 a 더하기 절대값 b가 됨
*/



1.5 벤 빗디들 ... 인수 우선 평가를 사용할때 정상 순서 평가를 사용할때 각각 어떤식으로 평가되는지를 서술

function p() {return p();}
function test(x,y){
	return x===0? 0:y;
}
test(0, p());

인수 우선 평가(일반적)
1. test 평가: 함수식. 인수식 평가: 0, p() 함수식. 따라서 함수식 평가 하니 또 함수식.
2. return 적용(함수의 적용)
3. 술어의 결과: true. 귀결식: 0. 대안식: p()
4. 따라서 결과는 0

정상 순서 평가(전부 전개하고 대입)
1. test 전개: 0 === 0 ? 0 : p( )
2. 대입하여 푸니 0?

solution

핵심은 인수 우선평가는 인수를 확실하게 정의해서 다음 평가로 넘겨야되는데 P()가 무한루프이기때문에 무한루프에 빠진다는 것.

정상 순서 평가는 인수는 쳐다도 안보고 일단 전개해서 최종 식을 만든다음에 필요한 인수를 가져다가쓰기에

if(x === 0){
		0
}else{
		p()
}

의 형태를 만들고 대입만 하기에 결론적으로 p()가 호출이 안된다.

1.1.7: 제곱근 구하기

수학에서 제곱근 정의하는 방법과 이를 어떻게 구하는지를 선언하는 방법을 프로그래밍적으로 바로 옯길 수가 있나? -> 결론적으로 없다. 왜냐하면 수학에서 하는 방식은 선언적 지식(declarative knowledge)이고, 프로그래밍에서 필요한 함수는 명령적 지식(imperative knowlege)이 필요하기 때문이다.

제곱하면 x가 되는 y를 뱉어라? 그래서 어떻게 뱉어야 되는데를 오차없이 알려줘야한다.

그걸 하는 방법이 뉴턴 매서드라고 하는 제곱근 구하는 공식이다.

제곱근을 알고 싶은 수: 피제곱근수
추측값: 제곱근에 근사한값
을 정의해두고, 계속해서 추측값을 제곱근에 가까운지 판별하며 루프를 끝내는 방법.

function square(x) { return x * x; }
function average(x, y) { return (x + y) / 2; }
function improve(guess, x) { return average(guess, x / guess); }
function abs(x) { return x >= 0 ? x : -x; }
function is_good(guess, x, strength) {
    return abs(square(guess) - x) < (0.1) / strength;
}
function sqrt_iter(guess, x, strength) {
    return is_good(guess, x, strength) ? guess 
           : sqrt_iter(improve(guess, x), x, strength);
}
function sqrt(x) {
    return sqrt_iter(1, x, 10);
}
sqrt(4);



연습문제

1.6

조건부 표현식이 보기 불편해서 조건부 표현식을 감싼 함수를 만들기로 함.

function conditional(predicate, then_clause, else_clause){
	retrun predicate ? then_clause: else_clause;
}

이 때 이함수를 이용한 복합 함수를 만들어서 뉴튼식을 적용하여 제곱근을 구하려할때

function sqrt_iter(guess, x){
	return conditional(is_good_enough(guess, x), 
		guess, 
		sqrt_iter(improve(guess, x), x));
}

이 함수로 제곱근을 구하려면 어찌될까?

function square(x) { 
    console.log(`square 함수 시작합니다. x: ${x}`);
    let result = x * x;
    console.log(`square(${x}) = ${result}`);
    return result; 
}

function average(x, y) { 
    console.log(`average 함수 시작합니다. x: ${x}, y: ${y}`);
    let result = (x + y) / 2;
    console.log(`average(${x}, ${y}) = ${result}`);
    return result; 
}

function improve(guess, x) { 
    console.log(`improve 함수 시작합니다. guess: ${guess}, x: ${x}`);
    let improved = average(guess, x / guess);
    console.log(`improve(${guess}, ${x}) = ${improved}`);
    return improved; 
}

function abs(x) { 
    console.log(`abs 함수 시작합니다. x: ${x}`);
    let result = x >= 0 ? x : -x;
    console.log(`abs(${x}) = ${result}`);
    return result; 
}

function is_good_enough(guess, x) {
    console.log(`is_good_enough 함수 시작합니다. guess: ${guess}, x: ${x}`);
    let difference = abs(square(guess) - x);
    let result = difference < 0.01;
    console.log(`is_good_enough(${guess}, ${x}) -> |square(${guess}) - ${x}| = ${difference} -> ${result}`);
    return result;
}

function conditional(predicate, then_clause, else_clause){
    console.log(`conditional 함수 시작합니다. predicate: ${predicate}`);
    let result = predicate ? then_clause : else_clause;
    console.log(`conditional 결과: ${result}`);
    return result;
}

function sqrt_iter(guess, x){
    console.log(`sqrt_iter 함수 시작합니다. guess: ${guess}, x: ${x}`);
    return conditional(
        is_good_enough(guess, x), 
        guess, 
        sqrt_iter(improve(guess, x), x)
    );
}

function sqrt(x) {
    console.log(`sqrt 함수 시작합니다. x: ${x}`);
    return sqrt_iter(1, x);
}

// 테스트 실행
console.log(`sqrt(10) 결과:`, sqrt(10));



실제 돌려보면 무한루프가 발생함. 원인은 모르겠음.. 잘 가다가 무슨 조건 때문인지 계속 호출이 맴돌음..

1.6 Solution

🎯 실제 함수를 구현해서 무한 루프가 발생하는 이유를 증명해보자!


📌 1. 무한 루프가 발생하는 이유: 자바스크립트의 인수 우선 평가(Eager Evaluation)

자바스크립트는 함수를 호출할 때 "인수 우선 평가"를 한다.
👉 즉, 함수의 본문이 실행되기 전에, 모든 인수(파라미터)들을 미리 계산해서 확정해놓고 실행한다!

✅ 예제 1: 함수 인수는 먼저 평가된다!
function test(a, b) {
    console.log("함수 실행됨!");
    return a + b;
}

console.log(test(1 + 2, 3 + 4));
📌 실행 과정
  1. test(1 + 2, 3 + 4)가 실행될 때
    • 먼저 1 + 2 = 3 계산
    • 그리고 3 + 4 = 7 계산
    • 결국 test(3, 7)이 실행된다!
  2. "함수 실행됨!" 출력
  3. test(3, 7)3 + 7 = 10 반환

🔥 결론: 자바스크립트는 함수의 인수를 미리 평가한 후에야 함수가 실행된다.
즉, 함수 본문에서 필요한 경우에만 실행되는 게 아니다!


📌 2. conditional 함수가 문제를 일으키는 이유

자, 이제 문제의 핵심인 conditional 함수를 보자.

function conditional(predicate, then_clause, else_clause) {
    return predicate ? then_clause : else_clause;
}

일반적으로 우리가 생각하는 if-else 구조랑 비슷하게 보이지?
👉 그런데 자바스크립트의 인수 우선 평가 방식 때문에 문제가 발생한다!

✅ 예제 2: conditional에서 예상치 못한 실행이 일어난다!
function square(x) {
    console.log(`square(${x}) 호출됨`);
    return x * x;
}

console.log(conditional(true, square(3), square(4)));
📌 실행 과정
  1. conditional(true, square(3), square(4)) 호출됨
  2. 인수 평가가 먼저 진행된다!
    • square(3) 실행 → square(3) 호출됨 출력 → 9 반환
    • square(4) 실행 → square(4) 호출됨 출력 → 16 반환
  3. 이제 predicate == true이므로 then_clause(9) 반환

🚨 문제점:

  • predicatetrue인데도 square(4)가 실행되었다!
  • 즉, 선택되지 않은 else_clause가 실행되는 것 자체가 문제!
  • 이게 sqrt_iter에서 무한 루프를 만드는 원인이다.

📌 3. sqrt_iter에서 무한 루프가 발생하는 이유

자, 이제 진짜 문제 코드를 보자.

function sqrt_iter(guess, x){
    console.log(`sqrt_iter 실행. guess: ${guess}, x: ${x}`);
    return conditional(
        is_good_enough(guess, x),
        guess,
        sqrt_iter(improve(guess, x), x)  // ⚠️ 여기서 이미 실행됨!
    );
}

와... 이제 문제의 원인이 보이지?

🚨 conditional을 실행하는 순간, sqrt_iter(improve(guess, x), x)가 먼저 실행된다!

👉 즉, is_good_enough(guess, x)truefalse든 상관없이 sqrt_iter이 무조건 실행된다!


📌 4. sqrt_iter(1, 10)이 어떻게 무한 루프에 빠지는가?
🔍 함수 호출 과정
sqrt(10) 호출
    ├── sqrt_iter(1, 10) 호출
        ├── conditional(
                is_good_enough(1, 10) → false,
                1,
                sqrt_iter(5.5, 10) → 이미 실행됨 ❌
            )
        ├── sqrt_iter(5.5, 10) 호출됨 (이미 평가됨)
            ├── conditional(
                    is_good_enough(5.5, 10) → false,
                    5.5,
                    sqrt_iter(3.659, 10) → 이미 실행됨 ❌
                )
            ├── sqrt_iter(3.659, 10) 호출됨 (이미 평가됨)
                ├── conditional(
                        is_good_enough(3.162, 10) → true,
                        3.162,
                        sqrt_iter(3.162, 10) → 이미 실행됨 ❌
                    )
                ├── sqrt_iter(3.162, 10) 호출됨 (이미 평가됨)
                    ├── (무한 루프)

🔥 결론:
👉 조건문이 true가 되어도 이미 실행된 sqrt_iter이 계속해서 새로운 재귀를 만들기 때문에 무한 루프가 발생한다.


📌 5. 해결 방법: if-else로 바꾸면 해결된다!
🚀 if-else를 사용하면 어떻게 되는가?
function sqrt_iter(guess, x){
    console.log(`sqrt_iter 실행. guess: ${guess}, x: ${x}`);
    if (is_good_enough(guess, x)) {
        return guess;  // ✅ 여기서 즉시 종료됨!
    } else {
        return sqrt_iter(improve(guess, x), x);
    }
}
🔍 이제 스택이 어떻게 정리되는가?
  1. is_good_enough(3.162, 10) == true이면 즉시 종료됨.
  2. sqrt_iter 재귀 호출이 멈추고, 정상적으로 sqrt(10) = 3.162 반환됨.

🔥 최종 결론

"자바스크립트의 인수 우선 평가 방식 때문에 conditional을 사용할 경우, else_clause가 필요 없을 때도 평가되어 무한 루프가 발생한다."
"즉, is_good_enough(guess, x)true가 되어도 이미 sqrt_iter이 실행되었기 때문에 탈출하지 못하는 것이다."
"이를 해결하려면 if-else를 사용해서 is_good_enough(guess, x)true일 때 즉시 반환하도록 해야 한다." 🚀🔥


1.7

제곱근 계산에 쓰인 is_good_enough 술어의 판정 방식은 아주 작은 수의 제곱근을 구할 때는 그리 효과적이지 않다.
그리고 실제 컴퓨터에서 산술 연산은 거의 항상 정밀도(유효자릿수)가 제한된 상태로 수행되기 때문에, is_good_enough의 판정 방식은 아주 큰 수의 제곱근 계산에도 부적합하다.

이러한 점을 좀 더 자세히 설명하고, 작은 수와 큰 수에 대해 판정이 실패하는 사례들을 제시하라.

is_good_enough를 구현하는 또 다른 전략은 반복 과정에서 guess의 변화량을 추적하면서 변화량이 guess의 아주 작은 비율보다 작으면 충분히 좋은 추측값이라고 판정하는 것이다.

이런 종류의 반복 종료 판정 방식을 사용하는 제곱근 함수를 설계하라.

작은 수와 큰 수에 대해 그 함수가 본문의 함수보다 더 잘 작동하는가?

나의 풀이

//실험 기존 코드
function sqrt(x){ return sqrt_iter(1, x); }
function sqrt_iter(guess, x){ 
	return is_good(guess, x) ? guess:sqrt_iter(improve(guess, x), x);
}
function is_good(guess, x){ return abs(square(guess) - x) < 0.001; }
function improve(guess, x){ return average(guess, x/guess); }
function abs(x){ return x < 0 ? -x : x; }
function square(x){ return x*x; }
function average(a, b){ return (a+b)/2; }

//작은 경우
sqrt(0.01); //기대값: 0.1 -> 실험값: 0.10032578510960605
sqrt(0.0001); //기대값: 0.01 -> 실험값: 0.03230844833048122 //고장시작
sqrt(0.00001); //기대값: 0.001 -> 실험값: 0.031260655525445276 //완전고장
sqrt(0.001); //실험값: 0.04124542607499115 //이미 많이 벗어남.
//결론: is_good에서 x가 0.001보다 작게 되면 적당히 제곱해서 0.001보다 작은수랑 x빼서 실제로 작으면 트루로 반환하는게 문제. 즉 0.001로 비교하기엔 x값이 0.001에 가까워질수록 정확한 계산 불가

//큰 경우
sqrt(10000) //기대값: 100 -> 실험값: 100
sqrt(1000000) //기대값: 1000 -> 실험값: 1000
sqrt(900000000) //기대값: 30000 -> 실험값: 30000
sqrt(987654321987654321); //Maximum call stack error
sqrt(9000000000000000000); //실험값: 3000000000 -> 오히려 큰수여도 딱 떨어지는 숫자를 주면 콜스택 에러가 안난다.
//결론: 0.001이라는 고정된 팩터가 근사값을 측정하기까지 너무 많은 연산을 해야되기 때문에 제대로된 연산 결과에 도달을 못한다.
//새로 제시된 방식: guess의 변화량이 guess의 아주 작은 비율보다 작으면 충분히 좋은값
function sqrt(x){ return sqrt_iter(x, 1, x); }
function sqrt_iter(pre, guess, x){ 
	return is_good(pre, guess) ? guess:sqrt_iter(guess, improve(guess, x), x);
}
function is_good(pre, guess){ return abs(pre - guess) < (guess*0.00001); }
function improve(guess, x){ return average(guess, x/guess); }
function abs(x){ return x < 0 ? -x : x; }
function square(x){ return x*x; }
function average(a, b){ return (a+b)/2; }

sqrt(987654321987654321);//993807990.5030237 -> 이제 에러가 안난다. 기대값과도 일치



■ 연습문제 1.8

세제곱근을 위한 뉴턴 방법에서는, y가 x의 세제곱근을 근사하는 값이라고 할 때 다음 공식으로 디 나은 근삿값을 구한다.
x/y2+2y3\frac{{x/y^2+2y}}{3}
이 공식을 이용해서 제곱근 함수와 비슷한 형태의 세제곱근 함수를 구현하라. ($1.3.4에서는
이 제곱근 함수와 세제곱근 함수를 추상화한 것에 해당하는, 일반적인 뉴턴 방법의 구현을 이
야기한다.)

나의 풀이
	function th_sqrt(x){ return th_sqrt_iter(x, 1, x); }
	function th_sqrt_iter(pre, guess, x){
		return th_is_good(pre, guess) ? guess :
			th_sqrt_iter(guess, th_improve(guess,x), x);
	}
	function th_is_good(pre, guess){ return abs(pre-guess)< guess*0.0001;}
	function th_improve(guess, x){
		return (x/(guess*guess) + 2*guess)/3;
	}
	function abs(x){return x < 0 ? -x : x; }
	th_sqrt(27); //실행값: 3
	th_sqrt(987654321); //이론값: 995.86772 실행값: 995.8677216529971
	th_sqrt(0.0000123456789); //실행값:0.023112042410213075 이론값:0.02311...



1.1.8 블랙박스 추상으로서의 함수

함수의 군집을 만들때 필요한 함수를 분해하는 전략
- 각 함수들이 각자 식별 가능한 과제를 수행: 확실하게 구분되어야함. 비슷한 일을 하는거면 의미 없음
- 그 함수들을 모듈로서 사용해서 다른 함수의 정의에 사용할 수 있게 하는 것
-정리: "모듈(module)"이란?

"하나의 특정한 역할을 수행하는 독립적인 함수 또는 코드 단위"
"다른 함수에서 재사용 가능하며, 조립할 수 있는 작은 구성 요소"

square가 구체적인 함수가 아니라 함수의 추상이다?"

좋아, 이걸 제대로 이해하려면 "구체적인 함수(concrete function)" vs "함수적 추상(functional abstraction)" 의 차이를 먼저 알아야 한다. 관념적인 내용이다. 똑같은 함수인데도 구체적인 함수가 될수도 있고 함수적 추상이 될수도 있다는 거야

function square(x) {
    return x * x;
}

이거는 함수의 정의, 구현부이다. 이렇게 바라보는게 구체적인 함수야.

function is_good_enough(guess, x) {
    return Math.abs(square(guess) - x) < 0.01;
}

이제 이렇게 바라보는게 함수적 추상이다.

무슨 차이냐면. 함수적 추상은 안에 세부적인 내용은 모르겠고(연산, 구현, 부품의 원리, 요소가 어떻게 이루어져서 뭐시기 이런거) 그냥 얘를 이용하면 제곱이 되겠거니 생각하겠다 이거야.

그래서 책에서 말하는 is_good_enough 입장에서 square는 하나의 함수 추상이다. 라는 말이 그뜻이야.

그래서 그걸 블랙박스 추상으로서의 함수라고 말하는거야

local name(지역이름)

  • 함수의 사용자는 함수의 구체적인 내용을 몰라도 된다. : 함수적 추상
  • 그 말은 즉 함수의 매개변수의 이름을 몰라도 된다는 얘기이다. 이런 함수의 매개변수들을 함수 본문 안에서만 유효한 local name 이라고 함.
  • 만약 local name을 지원하지 않았다면? -> 변수 선언 대혼란의 시대가 도래함
  • local name을 가능하게 하는 원리? -> bounding을 지원하기 때문. 함수안에서 종속되게 만든는거
  • 주어진 이름의 유효한 범위를 scope라고함. 함수의 매개변수는 함수 구현부가 scope이다.
  • bounding, bound의 개념은 함수 안에서만 있는건 아니고 넓게 더 퍼져있다. 그러나 여기서는 이정도만

내부 선언과 블록 구조

  • isolation: 격리.

  • 지금까지 배운 격리 방식: local name 매개변수

  • 그런데 지금까지 배운 함수들이 더 대형 시스템 안에서 공존한다고 상상

  • 프로그램 개발할때마다 이런 함수가 있나? 저런함수가 있나? 계속 isolation 시키기위한 고민을 끊임없이 해야함.

  • 그것을 편하게 해주는게 복합함수 안에 보조함수를 넣는 방식.

  • 복합함수: 여러 함수들을 조합하여 동작하는 함수

  • 보조함수: 복합함수 연산에 참여하여 필요한 부품들

//여러 보조함수들을 {} 중괄호 쌍에 포함시켜 버리면서 해당 함수들은 이제 sqrt()의 local name이 되었다!!
function sqrt(x){
	function is_good_enough(guess, x){
		return abs(square(guess) - x) < 0.001;
	}
	function improve(guess, x) {
		return average(guess, x / guess);
	}
	function sqrt_iter(guess, x) {
		return is_good_enough(guess, x)
			? guess
			: sqrt_iter(improve(guess, x), x);
	}
	return sqrt_iter(1, x);
}

//-> 더 간편화된 방법. 애초에 x를 여러번 매개변수로 쓸 필요가 없다. 왜? x는 sqrt 선언에 대한 바인딩 name 이므로, 같은 바인딩된 function들은 x를 자유자재로 사용가능하다.
// 이러한 방식을 어휘순 범위 적용(lexical scoping)이라고 한다.
// 대형 시스템을 편하게 해준다.
// 어차피 사용자 혹은 응용프로그래머가 알아야될 내용은 sqrt(x)라는 함수명 하나일 뿐이다.
profile
난민

0개의 댓글