[Scala] 흐름 제어 추상화

smlee·2023년 8월 10일
0

Scala

목록 보기
9/37
post-thumbnail

이 글은 Programming in Scala 4/e Chapter 9을 읽고 작성한 글입니다.


고차 함수

모든 함수는 비공통 부분과 호출과 관계없이 일정한 공통 부분으로 나뉜다. 공통 부분은 보통 함수의 본문이며, 비공통 부분은 반드시 인자로 주어져야 한다. 함숫값을 인자로 전달하면, 어떤 알고리즘은 다른 알고리즘의 비공통 부분으로 만들 수 있다.

이렇게 함수를 인자로 받는 함수고차 함수라고 한다. 이러한 고차함수는 코드를 간단하게 압축할 수 있는 더 많은 기회를 제공한다.

이러한 고차함수는 자신만의 추상화한 흐름 제어를 작성할 수 있어 코드 중복을 줄일 수 있다는 점이다. 따라서 인자로 들어오는 함수에 따라 다른 기능을 할 수 있도록 하여 코드를 최소화할 수 있는 스칼라의 장점이 있다.


우리는 파일 브라우저에서 사용자가 원하는 파일을 찾아주는 API들을 설계하고 싶다고 가정하다.

(1) 파일명이 query로 끝나는 파일 목록 (2) 파일명에 query가 들어가는 목록 (3) query Regex를 가지는 파일 목록 3가지 API들을 설계하고 싶다.

그리고, 공통인 부분과 비공통 부분으로 나누면 다음과 같을 것이다.

  • 공통 부분 - 파일 목록을 불러오는 부분
  • 비공통 부분 - API들 마다 다른 필터링 기준 및 리턴 부분

위와 같이 설계하면 다음과 같이 코드가 될 것이다.

object FileMatcher{
	private def filesHere = (new java.io.File(".")).listFiles
   
   def filesEnding(query:String) = 
   	for (file <- filesHere; filesMatching(file.getName.endsWith(query))
        		yield file
   def filesContaining(query:String) = 
   	for (file <- filesHere; filesMatching(file.getName.contains(query)))
        	yield file
   def filesRegex(query:String) =
   	for (file <- filesHere; filesMatching(file.getName.matches(query)))
        	yield file
}

filesEnding, filesContaining, filesRegex라는 메서드들로 API마다 필터링 기준 및 리턴 부분이 있고, filesHere로 공통으로 불러왔다.

하지만, 여기서 눈에 띄는 부분이 있다. 필터링을 거는 부분의 조건절을 제외하고는 유사한 동작을 하는 것이다. 그렇다면, 조건절을 받아서 공통적으로 묶을 수 있다는 생각을 할 수 있다.

object FileMatcher{
	private def filesHere = (new java.io.File(".")).listFiles
    
    private def filesMatching(matcher:String => Boolean) =
    	for (file <- filesHere; if(matcher(file.getName))
        	yield file
   
   def filesEnding(query:String) = filesMatching(_.endsWith(query))
   def filesContaining(query:String) = filesMatching(_.contains(query))
   def filesRegex(query:String) = filesMatching(_.matches(query))
}

즉 위의 코드처럼 필터링하는 부분을 공통적으로 묶고, 조건절을 메서드를 파라미터로 받아서 실행시키면 최대한 공통 부분을 묶을 수 있다. 즉, 고차함수를 사용하면 코드의 중복을 제거할 수 있다.

커링 (currying)

커링currying한 함수는 인자 목록이 하나가 아니고, 여럿인 함수이다. 일단 커링하지 않은 밑의 함수를 살펴보자.

위의 함수는 커링을 하지 않은 함수이다. 그렇다면, 인자 목록이 하나가 아니고 여럿인 커링한 함수는 어떤 것일까?

위는 커링한 함수이다. curriedSum이라는 커링한 함수에서 인자가 하나인 소괄호 2개가 연달아 붙어 있는 것이다. 이는 실제로는 2개의 전통적인 함수를 연달아 호출하는 것이다.

새로운 제어 구조 작성

함수가 1급 계층인 언어에서는 언어의 문법이 고정되어 있더라도 새로운 제어 구조를 작성할 수 있다. 함수를 인자로 받는 메서드만 작성하면 된다.

위와 같이 op라는 함수를 받아 twice라는 새로운 제어 구조를 작성하는 것이다.
위와 같이 고차함수를 사용하여 새로운 제어 구조를 작성할 수 있다.

이름에 의한 호출 파라미터

중괄호 사이에 값을 전달하는 내용이 없는 형태로 구현하고 싶다면, 이름에 의한 호출 파라미터(by-name parameter)를 사용하면 된다.

밑의 코드는 이름에 의한 호출 파라미터가 존재하지 않는 코드이다.

var assertionsEnabled = true

def myAssert(predicate: () => Boolean) =
	if(assertionsEnabled && !predicate())
    	throw new AssertionError

위의 코드를 사용하려면 다음과 같이 실행시켜야 한다.

myAssert(() => 5 > 3)

보통은 ()=>을 생략하고 사용할텐데, 이렇게 되면 불편하므로 이름에 의한 호출 파라미터를 사용한다.

def byNameAssert(predicate: => Boolean) =
	if(assertionEnabled && !predicate)
    	throw new AssertionError

위와 같이 사용하면 제어 구조와 똑같이 사용할 수 있다.

byNameAssert(5 > 3)

호출 파라미터 타입은 파라미터에서만 사용할 수 있다. 이름으로 전달하는 변수나 이름으로 전달하는 필드는 존재하지 않는다.

def boolAssert(predicate: Boolean) =
	if(assertionEnabled && !predicate)
    	throew new AssertionError

그러면 위와 같은 인자 타입으로 사용하면 간단하지 않을까 생각할 수도 있다.

boolAssert(5 > 3)

역시 동작하기 때문이다. 하지만, 이 두 접근법에는 중요한 차이가 존재한다.
boolAssert의 인자 타입이 Boolean이므로, 괄호 안에 위치한 표현식을 boolAssert 호출 직전에 계산한다.
반면, byNameAssert에서는 => Boolean 타입이므로 byNameAssert를 호출하기 전에 괄호 안에 있는 표현식을 계산하지 않는다. 대신, 5 > 3을 계산하는 내용의 apply 메서드가 들어간 함숫값을 만들어 byNameAssert로 넘긴다.

📚 Reference

  • Programming in Scala 4/e - Chapter 9

0개의 댓글