풀스택 웹🌐 개발자 지망생 🧑🏽‍💻
➕ 인공지능 관심 🤖


Categories


Recent views

  • 1
  • 2
  • 3
  • 4
  • 5

어떻게 코딩할 것인가-추상화

  1. 추상화 설계
  2. 추상 함수 이용
    • 함수 템플릿과 고차원 함수를 이용한 틀 함수

      어떻게 코딩할 것인가-추상화

      추상화(Abstraction)은 반복적인 코드를 리팩토링을 통해 추출하여 간단하고 재사용 가능한 일반적으로 표현된 추상 함수로 바꾸는 기술이다.

      프로그램의 복잡도를 관리하고 기능과 함수의 목적을 더욱 세세하게 나눌 수 있게 해준다.

      추상화 설계

      추상화는 기존의 함수 설계의 역순으로 따라가는 것이 특징이다.
      즉, 함수를 먼저 정의하고, 테스트한 뒤, 설명하고, 마지막에 시그니처를 얻어낸다.
      다음은 추상 함수 설계 과정이다.

      1. 추상 함수 정의

      🧾️ 추상화 1단계 예시
      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)))]))
      
      🧾️ 추상화 2,3단계 예시
      1. 하나의 함수를 복사하여 추상 함수로 삼는다.
      2. 추상 함수를 더 일반적인 이름으로 바꾼다.
        • contains-ubc?를 복사해 추상함수로 삼는다.
        • contains?로 이름을 바꿈(재귀 부분도 바꿈)
      (define (contains? los)
        (cond [(empty? los) false]
              [else
               (if (string=? (first los) "UBC")
                   true
                   (contains? (rest los)))]))
      
      🧾️ 추상화 4, 5단계 예시
      1. 함수 간 서로 다른 부분을 추상 함수의 인자로 추가한다.
      2. 추상 함수의 서로 다른 부분을 함수의 인자로 치환한다.
        • 두 함수는 “UBC”, “Mcgill” 부분이 다르므로, 이 부분을 인자 s로 치환한다.
       (define (contains? s los)
        (cond [(empty? los) false]
              [else
               (if (string=? (first los) s)
                   true
                   (contains? (rest los)))]))
      

      title: 추상화 완성 예시

      1. 완성된 추상 함수로 기존의 두 함수를 교체한다.
        ~~~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단계 예시

      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))
      
      🧾️ 3단계 예시
      1. 중복된 테스트, 쓸모없는 테스트는 지우고, 필요한 테스트를 추가한다.
        (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.
        
      🧾️ 4단계 예시
      1. 교체된 두 함수의 함수 해석을 가져온다.
        ;; produce list of sqr of every number in lon
        ;; produce list of sqrt of every number in lon
        
      🧾️ 5단계 예시
      1. 이를 이용해 추상 함수의 해석을 유도한다.
        ;; 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. 추상 함수 시그니처 작성

      추상 함수의 시그니처를 작성하는 것은 어려운일일 수 있다.
      완성된 추상 함수를 통해 시그니처를 유추해야 하며, 더 많은 인자, 타입 인자 등이 사용될 수 있기 때문이다.

      🧾️ 1 시그니처 템플릿 유추

      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)))]))
      
      🧾️ example

      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
      
      🧾️ 3 2번째 인자 유추

      los 인자에 firstrest 함수가 사용되는 것으로 보아 무언가의 배열임을 유추할 수 있다. 이 무언가를 미지의 타입인자 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
      
      🧾️ 4 1번째 인자 유추 및 타입인자 X 유추

      string=? 함수는 두 인자로 String 두개를 받으므로 첫번째 인자 s와 미지의 타입인자 XString임을 유추할 수 있고, 시그니처가 완성되었다.

      ;; 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)))]))
      
      사실, 이렇게 함수로만 유추하지않고 작성된 테스트들, 기존 함수들의 시그니처와 설명 등을 보고 직감적으로 시그니처를 작성할 수 있겠지만, 복잡하고 중요한 함수의 경우 이러한 과정이 필요할 것이다.

      타입인자가 사용되는 경우의 시그니처 설계

      추상 함수는 다양한 함수의 공통된 로직을 처리하기 위해 만드므로 아래 예시와 같이 타입과 관계없는 인자가 존재할 수 있다.

      타입 인자?(Type parameter)

      간혹, 함수의 한 인자의 타입이 다양하게 들어올 수 있다.

      예를 들어 예시와 같은 함수의 경우, 인자의 타입과 관계없이 처리가 가능한데, 이때 시그니처를 작성하기 위해 미지의 타입을 의미하는 타입 인자를 사용할 수 있다.(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)))]))
      

      다음은 타입인자가 사용된 경우의 시그니처 설계 예시이다.

      🧾️ 1 시그니처 템플릿 유추

      추상 함수의 인자가 2개이며, 리턴값은 언제나 하나이므로 위와 같이 시그니처 템플릿을 생성한다.

      ;; ____ ____ -> ____
      (define (map2 fn lon) ; Two parameter, output is always one.
        (cond [(empty? lon) empty]
              [else
               (cons (fn (first lon)) 
                     (map2 fn (rest lon)))]))
      
      🧾️ 2 결과값 타입 유추

      결과값이 emptycons에 사용되므로 무언가의 배열임을 유추한다.이 무언가를 미지의 타입인자 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)))]))
      
      🧾️ 3 2번째 인자 유추

      2번째 인자 lonempty?, 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.
      
      🧾️ 4 1번째 인자 함수 유추

      첫번째 인자 fn(first lon)의 결과값, 즉 X 타입을 인자로 받고 있다. 즉 fnX 타입 인자를 하나 받는 함수로 유추한다.

      ;; (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)))]))
      
      🧾️ 5 유추 완성

      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-1. 이때, 보통 추상 함수의 함수 입력 파라미터를 제외하면 얼추 찾을 수 있다.
      2. 설계 함수 내부에 추상함수를 집어넣고, 추상 함수를 원하는 로직으로 동작하기 위한 인자를 넣어준다.
        • 추상 함수의 입력 파라미터의 타입 또한 시그니처와 비교하여 알 수 있다.
        • 이때 고차원 추상 함수의 함수 파라미터를 위해 추가적인 함수를 구현해야할 수 있으며, 이때 클로저 함수를 이용할 수 있다.
      🧾️ 클로저와 고차원 추상 함수를 이용한 새로운 함수 설계

      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))
      
      클로저(closure)?

      함수 생성 시의 자신의 코드 블록(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)))
      

      다만, 상당히 성능상 비효율적인 경우가 많으므로, 대략적인 템플릿을 잡는데만 쓰고 추가로 리팩토링하는게 좋다.