함수 타입과 익명 함수

Function type and anonymous function

함수에서는 기본적인 함수 선언 및 호출 방법에 대해서 다뤘다. 해당 글을 시작할 때 함수를 타입으로 사용할 수도 있다고 언급했는데, 이 내용은 구조체 기초에서도 나온다.

여기서 얘기하는 함수는 앞서 살펴본 선언하고 정의한 함수와는 차이가 있다. 기본적인 내용은 구조체 기초에서 이미 일부 다뤘지만 좀 더 상세히 알아보도록 하자.

함수 타입

함수 타입에 대한 규정은 다음과 같다.

FunctionType   = "func" Signature .
Signature      = Parameters [ Result ] .
Result         = Parameters | Type .
Parameters     = "(" [ ParameterList [ "," ] ] ")" .
ParameterList  = ParameterDecl { "," ParameterDecl } .
ParameterDecl  = [ IdentifierList ] [ "..." ] Type .

다른 것들은 같으니, FunctionType과 함수에서 다룬 FunctionDecl만 자세히 보자.

FunctionType   = "func" Signature .
FunctionDecl   = "func" FunctionName Signature [ FunctionBody ] .

함수 타입은 이름(FunctionName)이 없고, 구현부(FunctionBody)도 없다.

사실 함수 타입, 시그니쳐 등은 C 개발자에게는 이미 너무나 익숙한 용어일 것이다. Java는 class가 중심이 되는 언어이기 때문에 유사한 기능을 위해 인터페이스(interface)를 정의하지만, C는 함수의 모양—signature라 불리는—을 타입으로 정의할 수 있고, 이 타입의 변수에 함수 구현을 저장할 수 있다.1 다음 C 함수를 보자.

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                   void *(*start_routine) (void *), void *arg);

void *를 인수로 받고, void *를 반환하는 형태의 함수를 start_routine이라는 이름의 인수로 받는 pthread_create 함수의 프로토타입이다.

Go의 함수 타입 역시 C와 동일하게 다양한 함수 구현체들을 변수에 담을 수 있도록 함수의 모양을 정의하는 역할을 한다.

int를 예로 들어보자. var i int라 하면 정수를 담기 위한 i라는 이름의 변수를 만든다. 마찬가지로 var ft 이러이러한-모양의-함수라 하면 이러이러한-모양의-함수를 담기 위한 ft라는 이름의 변수를 만들 수 있다. 그리고 이러이러한-모양의-함수를 정의하는 것이 바로 함수 타입이다.

함수 타입 사용법

C의 경우에서 이미 힌트를 얻었겠지만 함수를 이러이러한-모양이라고 할 때 가장 중요한 것은 바로 그 함수의 인수, 반환값의 타입, 개수 및 순서이다. 이를 signature라고 부른다.

타입이 들어가는 자리에 사용

구조체의 경우처럼 함수 타입도 타입이 들어가는 자리에 사용할 수 있다.

var ft func(a, b int) int

이렇게 만든 변수 ft에는 두 개의 int를 인수로 받고, int 값을 반환하는 함수를 저장할 수 있다. 아래 예를 보자.

package main

import "fmt"

func sum(a, b int) int {
	return a + b
}

func sub(a, b int) int {
	return a - b
}

func main() {
	var ft func(a, b int) int
	
	ft = sum
	fmt.Println(ft(2, 1))
	
	ft = sub
	fmt.Println(ft(2, 1))
}

타입이 들어가는 자리에 사용할 수 있으니 당연히 함수의 인수나 반환값으로도 사용할 수 있다.

package main

import "fmt"

func sum(a, b int) int {
	return a + b
}

func sub(a, b int) int {
	return a - b
}

func calc(ft func(x, y int) int, a, b int) int {
	return ft(a, b)
}

func main() {
	fmt.Println(calc(sum, 2, 1))
	fmt.Println(calc(sub, 2, 1))
}

위는 함수 타입을 함수의 인수로 사용한 경우이며, 아래는 반환값으로 사용한 경우이다.

package main

import "fmt"

func sum(a, b int) int {
	return a + b
}

func sub(a, b int) int {
	return a - b
}

func getFunc(i int) func(a, b int) int {
	if i == 0 {
		return sum
	} else {
		return sub
	}
}

func main() {
	fmt.Println(getFunc(0)(2, 1))
	fmt.Println(getFunc(1)(2, 1))
}

main 함수에서 쓴 구문이 어색할 수 있는데, getFunc(0) 또는 getFunc(1)을 호출해 반환받은 결과는 함수이고, 그 함수에 (2, 1)을 인자로 넣어 호출한 것이다.

타입 선언 방식으로 사용

함수 타입도 위와 같은 방식으로 사용하면 재사용성이 떨어진다. 구조체처럼 타입 선언 방식으로 사용하는 것이 일반적이다.

type ii_i func(a, b int) int
// or
type ii_i = func(a, b int) int
package main

import "fmt"

func sum(a, b int) int {
	return a + b
}

func sub(a, b int) int {
	return a - b
}

type ii_i func(a, b int) int

func main() {
	var f1 ii_i = sum
	var f2 ii_i = sub

	fmt.Println(f1(2, 1))
	fmt.Println(f2(2, 1))
}

익명 함수 (Anonymous function)

함수 타입이 C에는 있고 Java에는 없는 개념이었다면, 익명 함수는 Java에는 있고 C에는 없는 개념이다. 함수를 정의하되 이름을 붙이지 않는 것이다.

C 개발자들은 어떻게 호출하라고 이름도 없는 함수를 만드는지 의아해할 수 있을 것이다. 위 쓰래드를 만드는 pthread_create 함수를 다시 살펴보자.

pthread_create 함수에 들어갈 인수를 만들기 위해 C 개발자는 코드 어딘가에 void *를 인수로 받고 void *를 반환하는 함수를 정의해야 한다. 당연히 이름도 붙여야 한다. 하지만 이 함수는 생성되는 쓰래드의 시작점(entry point)으로 사용될 뿐 함수명을 이용해 직접 호출하는 경우는 거의 없다.

이런 용도로 사용할 수 있도록 Go에서는 이름이 없는 익명 함수를 만드는 기능을 제공한다. 함수 타입으로 어디서든 변수를 만들 수 있기 때문에 호출에 대한 것도 큰 문제가 아니다.

package main

import "fmt"

type ii_i func(a, b int) int

func calc(f ii_i, a, b int) int {
	return f(a, b)
}

func main() {
	var f1 ii_i = func(a, b int) int {
		return a + b
	}

	fmt.Println(f1(2, 1))

	fmt.Println(calc(func(a, b int) int {
		return a - b
	}, 2, 1))
}

익명 함수를 만들어 바로 그 자리에서 호출하는 것도 가능한데,

package main

import "fmt"

func main() {
	func(a, b int) {
		fmt.Println(a + b)
	}(2, 1)
}

지금 당장은 전혀 효용성이 없어 보이니 가능하다는 사실만 알아두자.


  1. 정확히 따지고 들면 함수의 주소, 즉 함수 포인터. [return]