기타

프로그래밍 스킴 Scheme 3 "3두개 7두개로 24를 만들려면?" 프로그래밍으로 풀기 1

by 정체불명 posted Oct 03, 2010
?

단축키

Prev이전 문서

Next다음 문서

ESC닫기

크게 작게 위로 아래로 댓글로 가기 인쇄
3두개 7두개로 24를 만들려면?을 스킴으로 풀기

3두개 7두개로 24를 만들려면?을 스킴으로 풀기
이번 강좌는 어느 카페에서 올라온 3두개 7두개로 24를 어떻게 만드나?하는 퀴즈에서 시작되었다.
사칙연산으로 3두개와 7두개로 24를 만들려면 어떻게 해야하나?
아마 대부분 머릿속에서 일일이 대입하고 있는 사람이 많을것이다.
원리를 꿰뚫어서 푼다면 당신은 천재 -_-;;;

나의경우는 스킴이라는 언어를 배우고 있을때여서 스킴으로 풀어볼까 했다.
그런데 방법을 몰라서 멍때리고 있다가 카페에서 순열을 구하는 코드를 득템해서 풀어보게 되었다. 물론 처음 하는거라서 실수도 있었지만 원하는 값을 찾을 수 있었다.

자 그럼 어떻게 만들지 구상을 해보자.
나의 경우는 
(+ (+ (+ (+ 3) (+ 7)) 3) 7)
부터
(/ (/ (/ (- 3) (- 7)) 3) 7)
까지
그냥 일일이 다 대입해서
파일에 한 줄 한 줄저장했다.
그중에 맞는게 있지 않겠는가? 후후..
그리고 그 데이터들을 읽어와서 실행해봤다.
엄청난 장점 중 하나로 스킴은 데이터와 코드의 구분이 없다! 물론 리습도 'ㅅ'
그래서 글자들을 읽어와서 그대로 실행할 수 있는 것이다.
(가령 (+ 1 2)를 파일에서 읽어와서 실행하면 3이 나온다.)
실행해서 24와 같으면 그 식을 출력하고 아니면 넘어간다.

후훗.. 실행해본 결과
(+ (+ (+ (+ 7) (+ 7)) 7) 3)
(+ (+ (+ (+ 7) (+ 7)) 7) 3)
(+ (+ (+ (+ 7) (+ 3)) 7) 7)
(+ (+ (+ (+ 7) (+ 3)) 7) 7)
(+ (+ (- (+ 7) (- 7)) 7) 3)
(+ (+ (- (+ 7) (- 7)) 7) 3)
(+ (+ (- (+ 7) (- 3)) 7) 7)
(+ (+ (- (+ 7) (- 3)) 7) 7)
(- (* (* (+ 3) (+ 3)) 3) 3)
(- (* (* (+ 3) (+ 3)) 3) 3)
(- (* (* (- 3) (- 3)) 3) 3)
(- (* (* (- 3) (- 3)) 3) 3)
(* (+ (/ (+ 3) (+ 7)) 3) 7)
(* (+ (/ (+ 3) (+ 7)) 3) 7)
(* (+ (/ (+ 7) (+ 7)) 7) 3)
(* (+ (/ (+ 7) (+ 7)) 7) 3)
(* (+ (/ (- 3) (- 7)) 3) 7)
(* (+ (/ (- 3) (- 7)) 3) 7)
(* (+ (/ (- 7) (- 7)) 7) 3)
(* (+ (/ (- 7) (- 7)) 7) 3)
이렇게 나왔다 
내 코드가 잘 못 되어서 이렇게 나온거다;;
우선 같은게 두개씩 출력되었고 
3두개 7두개가아니라 3 네개나 3 세개 + 7한개 같은거도나와버렸다. 
아무래도 숫자 넣는 칸에 3과7로 만들수 있는 모든 경우의 수를 계산해버린거같다.
그럼 뭐 어떤가? 답이 나왔는데...-_-;;
3두개 7두개인것을 추려보고 두개인것을 하나로 추려보면
(* (+ (/ (+ 3) (+ 7)) 3) 7)과 
(* (+ (/ (- 3) (- 7)) 3) 7)가 나온다!
자 그럼 계산기로 계산해보자 3나누기 7한것에 3을 더한뒤 7을 곱하면 24가 나온다 후훗...!
음수 3을 음수 7로 나누고 3더하고 7곱해도 역시나 24가 나온다. 

자 코드를 살펴보자

핵심 함수는 다음과 같다.
우선 눈에 익혀두자 아래에 설명할 것이다.

==모든 경우의 수===
for
for*/list
=================================

===데이터 변환===
list->string 
=================

===파일에 출력(기록)(파일을 열어야 기록할 수 있다.)===
open-output-file
display
===================

===파일에서 읽어오기===
open-input-file
read
========================

===파일에서 읽어온 식을 실행===
make-base-namespace
eval
===============================

자 이제 설명을 해보겠다.

==모든 경우의 수===
===for===
변수 먼저 적고 그뒤에 몸체가 있다는 함수 선언 하는것과 비슷하다.
(for () ())
for뒤에 오는 첫번째 괄호에는 [변수 리스트]를 넣고 
팁>> []가 여러개 넣어도 된다.
팁>> []는 ()과같다. 그런데 계속 ()만 쓰면 눈아프니까 'ㅅ';;
두번째 괄호에는 그 변수들을 가지고 연산을 하는 칸이다. 
비워두면 오류나므로 뭔가 적어두자.
(for ([i '(1 2 3)]
      [j "abc"]
      [k '(j k l)])
  (display (list i j k)))
for는 리스트중에 
팁>> "abc"는 '(#a #b #c)와 같다고보면 된다. #는 뒤에 문자가 온다는 뜻.
팁>> #space는 공백을 뜻한다.
for가 첫번째 돌때는 각각 리스트의 첫번째 원소 값이 각각 변수 i j k에 저장되고 
두번째 돌때는 두번째 세번째돌때는 세번째 원소값이 변수에 저장된다.
(1 a j)(2 b k)(3 c l)가 나온다.
그럼 다음과 같은 코드는 어떻게 출력될까?
(for ([i '(2 3 4 5 6 7 8 9)])
  (printf "2 * ~a = ~an" i (* 2 i)))
다음과 같이 출력된다.
2 * 2 = 4
2 * 3 = 6
2 * 4 = 8
2 * 5 = 10
2 * 6 = 12
2 * 7 = 14
2 * 8 = 16
2 * 9 = 18
========= 

===for*/list===
(for*/list ([변수1 리스트1] [변수2 리스트2] [변수3 리스트3])
  (변수사용하는 함수))
이 함수 역시 for와 마찬가지로 for*/list뒤에 첫째 괄호안에 [변수 리스트]를 여러개 적고 
(리스트를 하나만 적으면 for나 for*/list나 차이가 없다. 여기서는 3개를 적었다.)
그뒤의 괄호안에는 그 변수를 사용하는 계산이 온다.
(for*/list ([i '(1 2 3)]
            [j "abc"]
            [k '(j k l)])
  (list i j k))
그럼 이 코드의 결과는 어떻게 나올까?
한마디로 말하자면 모든 경우의 수가 리스트로 나온다.
다음은 위에 적어둔 for*/list의 결과이다
'((1 #a j) >>첫번째 돌아서 (list i j k)를 실행시킨 결과.
  (1 #a k)
  (1 #a l)
  (1 #b j)
  (1 #b k)
  (1 #b l)
  (1 #c j)
  (1 #c k)
  (1 #c l)
  (2 #a j)
  (2 #a k)
  (2 #a l)
  (2 #b j)
  (2 #b k)
  (2 #b l)
  (2 #c j)
  (2 #c k)
  (2 #c l)
  (3 #a j)
  (3 #a k)
  (3 #a l)
  (3 #b j)
  (3 #b k)
  (3 #b l)
  (3 #c j)
  (3 #c k)
  (3 #c l))

===for*===
for*라는 함수도있는데 for*와 for*/list의 차이점은 for*/list는 결과들을 리스트로 묶는다는 것이다. for*/list의 결과를 보면 가장 가장자리에 '()가 있는 것을 볼수있다.
for*를 연습해보고 싶다면
다음과 같이 해보라. display를 추가했다.
(for* ([i '(1 2 3)]
       [j "abc"]
       [k '(j k l)])
  (display (list i j k)))
==========

==여기서 잠깐.연습문제!===
for*/list를 사용해서 구구단을 출력해 보아라.
힌트>>#lang scheme 포함하고 들여쓰기 해서 4줄이면 된다.
==========================
===============
====================
무언가 감이 오지 않는가?
for*/list를 사용해서 모든 경우의 식들을 만들고
for를 사용해서 그 식들하나하나를 문자열로 바꿔서 저장하게 될것이다.
저장 용량은 뭐 200KB안에서 해결되더라.

===데이터 변환===
===list->string===
'(1 2 3)같은 리스트를 "1 2 3"과 같은 문자열로 바꿔준다.
파일에 리스트가 아닌 문자열을 출력하기 때문에 사용한것임.
(list->string (list 변수1 변수2 변수3 등등))는 
변수1 변수2 변수3 등등을 리스트로 만들고 그 리스트를 문자열로 바꾼다.
여기서는 변수자리에 #( 같은게 들어갔는데 #는 뒤에 따라오는 문자 하나를 뜻한다.
#)는 닫은 괄호를 뜻하고 #a은 문자 a를 뜻한다.
즉 "(+ (+ (+ (+ 3) (+ 3)) 3) 3)" 이런걸 만드라는 의미이다.
팁>> '(1 (+ 1 1) 3)은 문자 그대로 '(1 (+ 1 1) 3)인 리스트이다.
     하지만 (list 1 (+ 1 1) 3)은 '(1 2 3)이 된다. 
     즉 안의 계산을 먼저하고 그걸 리스트로 만드는 것이다.
=================

===입력과 출력이 헛갈리는 분들을 위한 팁===
input은 입력 output은 출력이다. 
입력은 파일에서 받을수도 사용자에게 받을수도 있다.
출력은 화면에 출력할수도있고 파일에 출력할수도 있다.
입력이라고 해서 파일에다가 입력하라는 뜻이 아니다.
입력은 받는거 출력은 뿌리는 것이다.
===========================================

===파일에 출력(기록)(파일을 열어야 기록할 수 있다.)===
팁>> 출력이라는 말이 헛갈릴수 있는데 파일에 출력이라함은 파일에 기록하는것이다.
팁>> 경로를 따로 지정안하고 
     이름만 적어두었으니 소스가 저장된 곳에 test.txt가 생길것이다.
     (소스를 저장 안 했으면 Racket설치 폴더에 test.txt가 생길것임.)
(define out (open-output-file "test.txt"))
=> 파일열고 out에 대입한다.
=> 이제 out에다가 출력하면 test.txt에 글자가 써진다.
(display strings out)
=> strings안에 든 내용을 out(즉 test.txt)에 출력한다.
(display "rn" out)
=> r은 커서를 가장 앞으로 옮기고 n은 다음줄로 넘어간다는 뜻이다. 
=> n만 쓰면 메모장으로 열때 깨진 문자를 볼수 있을것이다.
팁>> 이렇게 string따로 rn따로 나누어서 파일에 출력 하고싶지 않다면 
     (fprintf o "~arn" strings)처럼 쓴 후에 
     o문자열을 한번에 파일에 출력하는 방법이 있다.
     o에 출력하기 전에 (define o (open-output-string))이런 코드를 적어둬야 된다.
열었던 파일을 (close-output-port out)로 닫아주는 센스!!
================================================

===파일에서 읽어오기===
(define in (open-input-file "test.txt"))
=> 파일을 열고 in에 대입했다. 
=> 이제 in을 읽어서 출력하면 파일에서 한줄씩 읽어와서 출력된다.
(display (read in))
=> read로 in을 읽어서 출력했다.
팁>> display를 위에서는 파일에 출력할때 썼는데 
     뒤에 아무것도 안 적어주면 화면에 출력하라는 뜻이다.
=> 파일을 열어야 파일에서 읽어올 수 있다.
주의>> (open-input-file "test.txt")을 실행하면 파일의 처음부터 읽기시작한다.
       만약 (read (open-input-file "test.txt"))을 
       10번실행하면 10번 모두 첫줄만 나올것이다.
       하지만 (read in)으로 적고 10번 실행하면 한줄씩 넘어가서 10줄이 나올것이다.
       즉 읽을때 가르키는 곳이 한번 읽을때마다 다음 줄로 넘어간다.
=======================

===파일에서 읽어온 식을 실행===
(이 namespace와 eval 부분은 필자도 자세히 모르므로 틀려도 양해 바란다 'ㅅ';;)
===make-base-namespace===
namespace는 기호들과 기호와 관련된 정보를 연결해 놓은 자료이다.
eval로 불러온 식을 실행할때 기호들이 무슨 역할을 하는지 여기서 참조한다.
그냥 간단하게 eval 쓰기 전에 
(define ns (make-base-namespace)) 이런식으로 선언해준다고 생각하자.
=========================
===eval===
eval은 evaluation의 약자다. 평가라는 뜻이다.(수학에서 값을 구한다라는 뜻이다.)
필자는 실행이라는 단어를 많이 썼는데 사실 평가라는 단어가 정석이다.
eval은 뒤에 오는 리스트 데이터를 코드라고 생각하고 실행한다.
실행할때 기호들을 어떻게 실행할지는 마지막에 오는 namespace 를 참조한다.
'(+ 1 2 3)이 있다면 +가 기호/함수 이다. 
(eval '(+ 1 2 3) ns) 이렇게 적고 실행하면 6이 나올것이다.
여기에서는'(+ 1 2 3)대신에 위에서 for*/list로 만든 식이 들어올것이다.
다음과 같이 적을것이다.
(eval found ns)
found에는 다음과 같은 값이 들어갈것이다. 
주의>> ??는 생략했다는 뜻. ??를 적는다는의미가 아니다 -_-;;
(let ((found (read in))) (??))
found에다가 in 파일에서 read한 식을 한줄씩을 저장하므로 
'(+ (+ (+ (+ 3) (+ 7)) 3) 3) 이런 데이터가 저장 될것이다.
스킴은 데이터와 코드의 구분이 없으므로 이걸 실행할수 있다!!
왜 found에다가 저장한 다음에 
24인지 비교해서 맞으면 found를 출력했냐는 사람들이 있을것이다.
그건 (= (eval (read in) ns) 24)와 같이하면 24일때 그 식을 출력하기 어렵기 때문이다.
(필자가 모르는 것일수도 -_-;;)
==========
===============================

다음 편에 계속된다.