ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [스칼라] 흐름 제어 추상화
    스칼라 2022. 2. 1. 23:03

    Programming in scala 4th edition 을 읽고 정리한 글입니다.

     

    1. 코드 중복 줄이기

    스트링으로 이루어진 어떤 리스트에서 끝 단어를 가지고 질의하면 그에 맞는 스트링만 리턴해주는 프로그램이 있다.

    object StringMatcher {
      private def getStrings() = List(
        "Ironman",
        "Kafka",
        "Korean man",
        "Son Heung min",
        "Korea drama"
      )
    
      def stringsEnding(query: String) =
        for(string <- getStrings; if string.endsWith(query))
          yield string
    }
    
    val endsWith = StringMatcher.stringsEnding("man")
    endsWith // 결과: ["Ironman", "Korean man"]

    여기에서 추가 구현사항으로 스트링에 임의의 부분에 특정 단어가 포함된 리스트를 반환하는 기능을 추가한다고 해보자.

    object StringMatcher {
      private def getStrings() = List(
        "Ironman",
        "Kafka",
        "Korean man",
        "Son Heung min",
        "Korea drama"
      )
    
      def stringsEnding(query: String) =
        for(string <- getStrings; if string.endsWith(query))
          yield string
    
      def stringsContaining(query: String) =
        for(string <- getStrings; if string.contains(query))
          yield string
    }
    
    val containing = StringMatcher.stringsContaining("ma")
    containing
    // 출력 결과: ["Ironman", "Korean man", "Korea drama"]

    stringsContaining 함수를 추가하여 요구되는 기능은 구현했다. 하지만 stringsEnding 함수와 stringsContaining 함수가 중복되는 부분이 많은 것이 기분이 영 안좋다.

    스칼라의 함숫값을 이용해서 중복되는 부분을 최대한 줄이려고 시도해본다.

    // 함숫 값 이용 중복 제거
    object StringMatcher {
      private def getStrings() = List(
        "Ironman",
        "Kafka",
        "Korean man",
        "Son Heung min",
        "Korea drama"
      )
    
      def stringsMatching(query: String,
                          matcher: (String, String) => Boolean) = {
        for (string <- getStrings; if matcher(string, query))
          yield string
      }
      
      def stringsEnding(query: String) =
        stringsMatching(query, _.endsWith(_))
    
    
      def stringsContaining(query: String) =
        stringsMatching(query, _.contains(_))
    }
    
    val endsWith = StringMatcher.stringsEnding("man")
    endsWith // ["Ironman", "Korean man"]
    val containing = StringMatcher.stringsContaining("ma")
    containing // ["Ironman", "Korean man", "Korea drama"]

    for 문에서 일어나는 반복처리를 공통 함수로 빼고 질의의 위치에 따라 변경되는 함수만 변경해서 stringsMathing 함수에 인자로 넘겨주도록 변경했다. stringsMatching 함수 내에서 query도 그냥 matcher에 전달되는 역할밖에 없으므로 아래와 같이 코드를 바꿔줄 수 있다.

    object StringMatcher {
      private def getStrings() = List(
        "Ironman",
        "Kafka",
        "Korean man",
        "Son Heung min",
        "Korea drama"
      )
    
      def stringsMatching(matcher: (String) => Boolean) = {
        for (string <- getStrings; if matcher(string))
          yield string
      }
    
      def stringsEnding(query: String) =
        stringsMatching(_.endsWith(query))
    
    
      def stringsContaining(query: String) =
        stringsMatching(_.contains(query))
    }
    
    val endsWith = StringMatcher.stringsEnding("man")
    endsWith
    val containing = StringMatcher.stringsContaining("ma")
    containing

    위의 예시로 스칼라의 first class function이 중복 코드를 제거하는데 유용하게 사용될 수 있음을 볼 수 있다. 함수 그 자체를 넘겨줌으로써 공통적으로 처리 될 부분과 특수하게 처리해야할 부분을 분리하기가 더 쉽다. 이렇게 함수를 인자로 받는 함수를 고차함수라고 한다.

     

    2. 클라이언트 코드 단순화하기

    고차함수를 활용하면 코드중복을 줄일수 있을 뿐만 아니라 코드를 좀 더 간결하게 만드는것도 쉽다. 스칼라 컬렉션 타입의 특별 루프 메서드를 가지고 살펴보자.

    def containsPos(nums: List[Int]): Boolean = {
      var exists = false
      for(num <- nums)
        if(num > 0)
          exists = true
      exists
    }
    
    println(containsPos(List(0, -1))) // false
    println(containsPos(List(1, 2, 3))) // true

    위의 코드는 정수형 리스트 내부에 양수가 있는지 없는지를 반환한다. 이것을 컬렉션의 고차함수인 exists로 구현하면 좀 더 간결하게 구현할 수 있다.

    def containsPos(nums: List[Int]) = nums.exists(_ > 0)
    
    println(containsPos(List(0, -1)))
    println(containsPos(List(1, 2, 3)))

    exists는 스칼라 라이브러리에서 제공하는 특수 루프 구조로 흐름 제어 추상화를 보여준다. 클라이언트는 스칼라가 제공하는 특수 루프 구조를 활용함으로써 프로그램의 흐름 제어를 간결하게 구현할 수 있다.

     

    3. 커링

    앞서 살펴본 예시들은 스칼라 자체적으로 제공하는 제어 추상화라기보다는 API나 구현차원에서 할 수 있는 제어 추상화로 보인다. 언어 차원에서 지원하는 제어 추상화 구문을 이해하려면 먼저 커링이라는 개념을 알아야한다.

    커링한 함수는 여러개의 인자목록을 갖는다.

    // 커링
    def plainSum(x: Int, y: Int): Int = x + y
    
    def curriedSum(x: Int)(y: Int): Int = x + y
    
    println(plainSum(1, 2)) // 3
    println(curriedSum(1)(2)) // 3

    위의 예시는 두 수를 받아 덧셈하는 함수를 일반적인 구현과 커링된 함수로 구현한 것이다. 위의 curriedSum을 이해하기 위해 아래 예시를 보자. 아래의 curriedSum은 근본적으로 위의 예시와 같은 일을 하는 함수이다.

    def curriedSum(x: Int) = (y: Int) =>  x + y
    
    println(curriedSum(1)(2))
    val plusOne = curriedSum(1) // 변수에 할당도 가능하다
    println(plusOne(2))

    다시 커링으로 구현된 curriedSum으로 돌아와서, 이 함수도 마찬가지로 인자를 일부만 준 상태에서 부분적용 함수로 활용 할 수 있다.

    // 커링 함수 재활용
    def curriedSum(x: Int)(y: Int) = x + y
    val plusOne = curriedSum(1)_
    println(plusOne(2)) // 3

     

    4. 새로운 제어 구조 작성

    함수가 1급 계층인 언어에서는 함수를 인자로 받는 메서드를 작성해서 새로운 제어 구조를 작성할 수 있다. 여러곳에서 제어 패턴의 반복되는 것을 발견했다면 새로운 제어 구조 구현을 고려해보는것이 좋다. 예를들어 자원을 열고, 조작사고, 닫아주는 구조를 생각해보자.

    def transaction(config: Config, op: Connector => Unit) = {
      val connector = new Connector(config)
      try {
        op(connector)
      } finally {
        connector.close()
      }
    }
    // 아래와 같이 활용할 수 있다.
    
    transaction(
      new Config("config.yml"),
      connector => connector.doSomething()
    )

    DB 커넥터를 연결하고 조작하고 커넥터를 다시 닫아주는 예시코드이다. 이제 사용자는 커넥터 연결/종료 부분은 신경쓰지 않고 DB에 접근해서 조작할 작업만 구현해서 사용하면 된다. 이런 방식을 빌려주기 패턴이라고 한다. 제어 추상화 함수가 자원을 열어 특정 함수에게 자원을 빌려주기 때문이다.

    코드 작성시 제어구조를 좀 더 강조하는 방법중 하나는 인자를 전달할 때 중괄호를 사용하는 것이다. 스칼라에서는 하나의 인자만 전달하는 경우에는 중괄호를 사용할 수 있다.

    println { "Hello, World!"}

    위의 빌려주기 패턴 예시를 커링과 중괄호를 사용해서 다시 작성하면 아래와 같다.

    def transaction(config: Config)(op: Connector => Unit) = {
      val connector = new Connector(config)
      try {
        op(connector)
      } finally {
        connector.close()
      }
    }
    
    transaction(new Config("config.yml")) {
      connector => connector.doSomethiong()
    }

     

    5. 이름에 의한 호출 파라미터

    예를들어 주어진 함수의 결과가 참인지 거짓인지를 판별하는 함수를 만든다고 하자.

    def myAssertion(predicate: () => Boolean): Unit = {
      if(!predicate())
        throw new AssertionError
    }
    
    myAssertion(() => 3 > 0)

    위의 코드는 아무 문제가 없지만 함수 호출시 빈 파라미터 목록과 => 표시를 없애면 더 좋을 것 같다. 이름에 의한 호출 파라미터는 이런 동작을 가능하게 한다. 이름에 의한 호출 파라미터를 이용하려면 함수 선언시 () => 대신에 => 를 사용하면 된다.

    // 이름에 의한 호출
    def myAssertion(predicate: => Boolean): Unit = {
      if(!predicate)
        throw new AssertionError
    }
    
    myAssertion(3 > 0)

     

    위의 코드가 아래와 무슨 차이가 있는지 알아보자.

    def myAssertion(predicate: Boolean)
    ...
    
    myAssertion(3 > 0)

    단순히 불리언 타입을 지정하면 predicate에 주어진 식이 myAssertion 호출 전에 계산되어 그 결과가 넘어간다. 이름에 의한 호출 파라미터는 결과를 계산하지 않고 3 > 0 을 계산하는 내용의 함수값을 만들어서 넘긴다.

    '스칼라' 카테고리의 다른 글

    [스칼라] 스칼라 계층구조  (0) 2022.02.13
    [스칼라] 상속과 구성  (0) 2022.02.03
    [스칼라] 함수와 클로저  (0) 2022.01.31
    [스칼라] 내장 제어 구문  (0) 2022.01.31
    [스칼라] 함수형 객체  (0) 2022.01.30

    댓글

Designed by Tistory.