풀스택 웹🌐 개발자 지망생 🧑🏽💻
➕ 인공지능 관심 🤖
Categories
-
┣
▶ COMPUTER_SCIENCE
📂: 7 -
┣
▶ WEB
📂: 3 -
┣
▶ ETC
📂: 3-
┃
┣
ETCS
📄: 10 -
┃
┣
SUBBRAIN 개발기
📄: 5 -
┃
┗
YOS 개발기
📄: 1
-
┃
┣
-
┗
▶ AI
📂: 9-
┣
AITOOLS
📄: 3 -
┣
CV
📄: 2 -
┣
DEEP_LEARNING
📄: 1 -
┣
DATA_VIS
📄: 2 -
┣
GRAPH
📄: 1 -
┣
LIGHTWEIGHT
📄: 1 -
┣
MATH
📄: 1 -
┣
NLP
📄: 3 -
┗
STRUCTURED_DATA
📄: 2
-
┣
어떻게 코딩할 것인가-추상화
어떻게 코딩할 것인가-추상화
추상화(Abstraction)은 반복적인 코드를 리팩토링을 통해 추출하여 간단하고 재사용 가능한 일반적으로 표현된 추상 함수로 바꾸는 기술이다.
프로그램의 복잡도를 관리하고 기능과 함수의 목적을 더욱 세세하게 나눌 수 있게 해준다.
추상화 설계
추상화는 기존의 함수 설계의 역순으로 따라가는 것이 특징이다.
즉, 함수를 먼저 정의하고, 테스트한 뒤, 설명하고, 마지막에 시그니처를 얻어낸다.
다음은 추상 함수 설계 과정이다.
1. 추상 함수 정의
- 구조와 기능이 상당히 유사한 두 함수를 찾는다.
- 두 함수의 구조와 기능이 상당히 유사하다.
- 테스트, 스텁, 시그니처 등은 생략
(define (contains-ubc? los)
(cond [(empty? los) false]
[else
(if (string=? (first los) "UBC")
true
(contains-ubc? (rest los)))]))
(define (contains-mcgill? los)
(cond [(empty? los) false]
[else
(if (string=? (first los) "McGill")
true
(contains-mcgill? (rest los)))]))
- 하나의 함수를 복사하여 추상 함수로 삼는다.
- 추상 함수를 더 일반적인 이름으로 바꾼다.
contains-ubc?
를 복사해 추상함수로 삼는다.contains?
로 이름을 바꿈(재귀 부분도 바꿈)
(define (contains? los)
(cond [(empty? los) false]
[else
(if (string=? (first los) "UBC")
true
(contains? (rest los)))]))
- 함수 간 서로 다른 부분을 추상 함수의 인자로 추가한다.
- 추상 함수의 서로 다른 부분을 함수의 인자로 치환한다.
- 두 함수는 “UBC”, “Mcgill” 부분이 다르므로, 이 부분을 인자 s로 치환한다.
(define (contains? s los)
(cond [(empty? los) false]
[else
(if (string=? (first los) s)
true
(contains? (rest los)))]))
title: 추상화 완성 예시
- 완성된 추상 함수로 기존의 두 함수를 교체한다.
~~~BSL
(define (contains-ubc? los) (contains? “UBC” los))
(define (contains-mcgill? los) (contains? “McGill” los))
(define (contains? s los)
(cond [(empty? los) false]
[else
(if (string=? (first los) s)
true
(contains? s (rest los)))]))
<!-- @#@-example@#@추상화 완성 예시@#@ -->
#### 고차 함수를 이용한 추상화
만약 서로 다른 부분이 함수라면 고차 함수를 지원하는 프로그래밍 언어라면 다음과 같이 고차함수로 추상 함수를 만들 수 있다.
<!-- #@#callout-example#@#고차 함수를 이용한 추상화 이전#@# -->
title: 고차 함수를 이용한 추상화 이전
두 함수는 각각 `sqr`, `sqrt`로 이용하는 함수 부분이 다르다.
고차 함수는 함수를 인자로 받을 수 있으므로 아래처럼 추상화할 수 있음
~~~BSL
(define (squares lon)
(cond [(empty? lon) empty]
[else
(cons (sqr (first lon))
(squares (rest lon)))]))
(define (square-roots lon)
(cond [(empty? lon) empty]
[else
(cons (sqrt (first lon))
(square-roots (rest lon)))]))
;;==========================
;;...After Abstraction.
;;==========================
(define (squares lon) (map2 sqr lon))
(define (square-roots lon) (map2 sqrt lon))
(define (map2 fn lon)
(cond [(empty? lon) empty]
[else
(cons (fn (first lon))
(map2 fn (rest lon)))]))
2. 추상 함수 테스팅 및 해석 작성
이 단계는 기존의 테스트와 해석을 재활용하여 아주 쉽게 진행할 수 있다.
title: 1단계 예시
- 교체된 두 함수의 테스트들을 가져온다.
~~~BSL
;; Tests from squares.
(check-expect (squares empty) empty)
(check-expect (squares (list 3 4)) (list 9 16))
;; Tests from square-roots.
(check-expect (square-roots empty) empty)
(check-expect (square-roots (list 9 16)) (list 3 4))
(define (map2 fn lon)
(cond [(empty? lon) empty]
[else
(cons (fn (first lon))
(map2 fn (rest lon)))]))
<!-- @#@-example@#@1단계 예시@#@ -->
<!-- #@#callout-example#@#2단계 예시#@# -->
title: 2단계 예시
2. **테스트 대상 함수를 추상함수로 교체하고, 인자를 추가한다.**
~~~BSL
(check-expect (map2 sqr empty) empty)
(check-expect (map2 sqr (list 3 4)) (list 9 16))
(check-expect (map2 sqrt empty) empty)
(check-expect (map2 sqrt (list 9 16)) (list 3 4))
- 중복된 테스트, 쓸모없는 테스트는 지우고, 필요한 테스트를 추가한다.
(check-expect (map2 sqr empty) empty) (check-expect (map2 sqr (list 3 4)) (list 9 16)) ; (check-expect (map2 sqrt empty) empty) ; duplicated empty test. (check-expect (map2 sqrt (list 9 16)) (list 3 4)) (check-expect (map2 abs (list 2 -3 4)) (list 2 3 4)) ; add a test for function parameter.
- 교체된 두 함수의 함수 해석을 가져온다.
;; produce list of sqr of every number in lon ;; produce list of sqrt of every number in lon
- 이를 이용해 추상 함수의 해석을 유도한다.
;; produce list of sqr of every number in lon ;; produce list of sqrt of every number in lon ;; produce (list (fn n0) (fn n1) ...) with given fn and (list n0 n1 ...).
기존 함수들의 테스트와 해석으로 부터 힌트를 얻을 수 있지만 속박될 필요없이 더 나은 방법이 있다면 바꾸어도 좋다.
3. 추상 함수 시그니처 작성
추상 함수의 시그니처를 작성하는 것은 어려운일일 수 있다.
완성된 추상 함수를 통해 시그니처를 유추해야 하며, 더 많은 인자, 타입 인자 등이 사용될 수 있기 때문이다.
contains?
함수의 필요인자를 보고 시그니처 템플릿을 유추
;; ____ ____ -> ____
(define (contains? s los) ; Two parameter, output is always one.
(cond [(empty? los) false]
[else
(if (string=? (first los) s)
true
(contains? s (rest los)))]))
title:2 결과값 타입 유추
contains?
함수의 결과값들이 전부 true
아니면 false
이므로 Boolean
임을 유추
;; ____ ____ -> Boolean
(define (contains? s los)
(cond [(empty? los) false] ; output is false
[else
(if (string=? (first los) s)
true ; output is true
(contains? s (rest los)))])) ; output is true or false
los
인자에 first
와 rest
함수가 사용되는 것으로 보아 무언가의 배열임을 유추할 수 있다. 이 무언가를 미지의 타입인자 X로 놓자.
;; _____ (listofX) -> Boolean
(define (contains? s los)
(cond [(empty? los) false]
[else
(if (string=? (first los) s) ; (first ) -> fn for list
true
(contains? s (rest los)))])) ; (rest )-> fn for list
string=?
함수는 두 인자로 String
두개를 받으므로 첫번째 인자 s
와 미지의 타입인자 X
는 String
임을 유추할 수 있고, 시그니처가 완성되었다.
;; String (listofString) -> Boolean ; Signature Completed!
(define (contains? s los)
(cond [(empty? los) false]
[else
(if (string=? (first los) s) ; (string=? String String)
true
(contains? s (rest los)))]))
타입인자가 사용되는 경우의 시그니처 설계
추상 함수는 다양한 함수의 공통된 로직을 처리하기 위해 만드므로 아래 예시와 같이 타입과 관계없는 인자가 존재할 수 있다.
간혹, 함수의 한 인자의 타입이 다양하게 들어올 수 있다.
예를 들어 예시와 같은 함수의 경우, 인자의 타입과 관계없이 처리가 가능한데, 이때 시그니처를 작성하기 위해 미지의 타입을 의미하는 타입 인자를 사용할 수 있다.(X, Y, Z, T… 등의 대문자로 표현)
;; given fn and (list n0 n1 ...) produce (list (fn n0) (fn n1) ...)
(check-expect (map2 sqr empty) empty)
(check-expect (map2 sqr (list 2 4)) (list 4 16))
(check-expect (map2 sqrt (list 16 9)) (list 4 3))
(check-expect (map2 abs (list 2 -3 4)) (list 2 3 4))
;; (X -> Y) (listof X) -> (listof Y) ; Using Type parameter
(define (map2 fn lon)
(cond [(empty? lon) empty]
[else
(cons (fn (first lon))
(map2 fn (rest lon)))]))
다음은 타입인자가 사용된 경우의 시그니처 설계 예시이다.
추상 함수의 인자가 2개이며, 리턴값은 언제나 하나이므로 위와 같이 시그니처 템플릿을 생성한다.
;; ____ ____ -> ____
(define (map2 fn lon) ; Two parameter, output is always one.
(cond [(empty? lon) empty]
[else
(cons (fn (first lon))
(map2 fn (rest lon)))]))
결과값이 empty
와 cons
에 사용되므로 무언가의 배열임을 유추한다.이 무언가를 미지의 타입인자 Y로 놓자.
;; ____ ____ -> (listof Y)
(define (map2 fn lon)
(cond [(empty? lon) empty] ; output is empty
[else
(cons (fn (first lon)) ; cons -> fn for list.
(map2 fn (rest lon)))]))
2번째 인자 lon
에 empty?
, first
, rest
등에 사용되므로 무언가의 배열임을 유추한다. 이 무언가를 미지의 타입인자 X로 놓자.
;; ____ (listof X) -> (listof Y)
(define (map2 fn lon)
(cond [(empty? lon) empty] -> empty? -> fn for list.
[else
(cons (fn (first lon)) ;; fisrt -> fn for list.
(map2 fn (rest lon)))])) ;; rest -> fn for list.
첫번째 인자 fn
이 (first lon)
의 결과값, 즉 X
타입을 인자로 받고 있다. 즉 fn
은 X
타입 인자를 하나 받는 함수로 유추한다.
;; (X -> ___) (listof X) -> (listof Y)
(define (map2 fn lon)
(cond [(empty? lon) empty]
[else
(cons (fn (first lon)) ; fn takes X as parameter.
(map2 fn (rest lon)))]))
fn
함수의 결과가 map2
함수의 결과값의 타입 Listof Y
의 원소이므로 fn
함수의 결과값의 타입은 Y
임을 알 수 있고, 시그니처가 완성되었다.
;; (X -> Y) (listof X) -> (listof Y) ;; Signature Comepleted!
(define (map2 fn lon)
(cond [(empty? lon) empty]
[else
(cons (fn (first lon))
(map2 fn (rest lon)))])) ;; Result's Type is ListOfY.
확실히, 타입인자가 필요한 추상 함수인지 확인하기 위해 시그니처에 맞는 테스트를 새로 작성해 테스트해봐도 좋다.
추상 함수 이용
여러 프로그래밍 언어에서 map
, filter
같은 추상 함수들을 지원하기도 한다.
빌트인 추상함수들을 이용하면 설계와 테스트가 간단해지고, 성능과 정확도 또한 보장할 수 있다.
추상 함수를 이용해 함수를 설계하려면 다음과 같다.
- 두개 이상의 추상함수가 필요할 수도 있다.
- 설계하려는 함수의 시그니처와 유사한 추상함수를 찾는다.
1-1. 이때, 보통 추상 함수의 함수 입력 파라미터를 제외하면 얼추 찾을 수 있다. - 설계 함수 내부에 추상함수를 집어넣고, 추상 함수를 원하는 로직으로 동작하기 위한 인자를 넣어준다.
- 추상 함수의 입력 파라미터의 타입 또한 시그니처와 비교하여 알 수 있다.
- 이때 고차원 추상 함수의 함수 파라미터를 위해 추가적인 함수를 구현해야할 수 있으며, 이때 클로저 함수를 이용할 수 있다.
map
추상 함수는 ((X -> Y) (listof X)) → (listof Y)
이므로 클로저 함수와 함께 사용한다면 시그니처가 일치한다.
;; String (listof String) -> (listof String)
;; produce list of all elements of los prefixed by p
(check-expect (prefix-all "accio " (list "portkey" "broom"))
(list "accio portkey" "accio broom"))
; (define (prefix-all p los) empty) ;stub
(define (prefix-all p los)
(map (local
[(define (prefix-p s) (string-append p s))]
prefix-p) los))
함수 생성 시의 자신의 코드 블록(lexical scope)의 정보를 기억하는 함수
이를 이용해 함수 설계시 이용하면,
- 전역변수 이용이 없어 외부에 주는 영향이 적고
- 재사용성 높으며,
- 가독성 좋게
구현할 수 있다.
아래 파이썬 예시를 보면, addNFunc
함수의 x인자 값은 addNFunc
함수가 종료되어도 add3
, add2
함수 내에서 남아있다.
코드 블록(lexical scope)이 종료되도 정보를 기억하는 것이다.
클로저 함수(아래의 람다 함수)를 이용하는 것이 그 아래의 전역변수를 이용하는 것보다 더 가독성과 재사용성, 부작용 면에서 낫다.
def addNFunc(x):
return lambda y : x + y
add3 = addNFunc(3)
add2 = addNFunc(2)
print(add3(1)) # 4
print(add2(1)) # 3
# 전역변수 버전
{: #전역변수-버전}
N = 3
def addNFunc2():
return lambda y : N + y
add = addNFunc2()
N = 3
print(add(1)) # 4
N = 2
print(add(1)) # 3
함수 템플릿과 고차원 함수를 이용한 틀 함수
함수 템플릿과 고차원 함수를 이용하면, 특정 데이터를 알맞게 처리하는 함수의 기본적인 틀 역할을 하는 함수를 만들 수 있다.
;; =================
;; Data definitions:
(define-struct dir (name sub-dirs images))
;; Dir is (make-dir String ListOfDir ListOfImage)
;; interp. An directory in the organizer, with a name, a list of sub-dirs and a list of images.
;; ListOfDir is one of:
;; - empty
;; - (cons Dir ListOfDir)
;; interp. A list of directories, this represents the sub-directories of a directory.
;; ListOfImage is one of:
;; - empty
;; - (cons Image ListOfImage)
;; interp. a list of images, this represents the sub-images of a directory.
;; NOTE: Image is a primitive type, but ListOfImage is not.
(define I1 (square 10 "solid" "red"))
(define I2 (square 12 "solid" "green"))
(define I3 (rectangle 13 14 "solid" "blue"))
(define D4 (make-dir "D4" empty (list I1 I2)))
(define D5 (make-dir "D5" empty (list I3)))
(define D6 (make-dir "D6" (list D4 D5) empty))
각 데이터의 템플릿을 가져오고 (...)
부분을 틀 함수의 인자로 집어넣자.
완성 후, 틀 함수의 시그니처를 유추한다.
;; =================
;; Functions:
;; fold-dir function
;; (String Y Z -> X) (X Y -> Y) (Image Z -> Z) Y Z Dir -> X
;; the abstract fold function for Dir
(check-expect (fold-dir make-dir cons cons empty empty D6) D6)
(check-expect (local [(define (c1 n rlod rloi) (+ rlod rloi))
(define (c2 rdir rlod) (+ 1 rdir))
(define (c3 img rloi) (+ 1 rloi))]
(fold-dir c1 c2 c3 0 0 D6))
3)
; <template from Dir>
(define (fold-dir c1 c2 c3 b1 b2 d)
(local [(define (fn-for-dir d) ; Dir -> X
(c1 (dir-name d)
(fn-for-lod (dir-sub-dirs d))
(fn-for-loi (dir-images d))))
(define (fn-for-lod lod) ; (listof Dir) -> Y
(cond [(empty? lod) b1]
[else
(c2 (fn-for-dir (first lod))
(fn-for-lod (rest lod)))]))
(define (fn-for-loi loi) ; (listof Image) -> Z
(cond [(empty? loi) b2]
[else
(c3 (first loi)
(fn-for-loi (rest loi)))]))]
(fn-for-dir d)))
지역 범위와 틀 함수를 이용해 처리함수를 넘겨주어 원하는 역할을 하는 함수를 만들 수 있다.
;; Dir -> Natural
;; count total number of Images in dir and all its subdirs
(check-expect (count-images D4) 2)
(check-expect (count-images D6) 3)
; <template as call to fold-dir>
(define (count-images d)
(local [(define (c1 n rlod rloi) (+ rlod rloi))
(define (c2 rdir rlod) (+ rdir rlod))
(define (c3 img rloi) (+ 1 rloi))]
(fold-dir c1 c2 c3 0 0 d)))
다만, 상당히 성능상 비효율적인 경우가 많으므로, 대략적인 템플릿을 잡는데만 쓰고 추가로 리팩토링하는게 좋다.
_articles/computer_science/OSSU/PL/HowToCode/어떻게 코딩할 것인가-추상화.md