0%

2. 함수

글을 시작하기에 앞서 해당 시리즈는 Allen Downey, Ben Lauwens의 저서인 Think Julia: How to Think Like a Computer Scientist를 바탕으로 작성된 글임을 알려드립니다.

이 포스트는 Functions를 한글로 요약 정리한 글입니다.

함수

프로그래밍에서 함수는 특정 계산을 수행하는 일련의 명령문이다. 함수를 정의할 때에는 이름과 명령문의 순서를 정하며, 이후 해당 이름을 통해서 함수를 호출할 수 있다.

함수 호출

우리는 이미 함수를 호출해 본 경험이 있다.

1
2
julia> println("Hello, World!")
Hello, World!

이 함수의 이름은 println()이다. 또한 괄호 안에 들어가는 표현식은 함수의 인수(argument)라고 부른다.
함수가 작동하는 과정은 일반적으로 인수를 받은 후 결과를 반환한다고 설명하며, 이런 결과를 반환 값이라고 한다.

줄리아는 데이터 타입을 바꿔주는 함수도 제공한다. 예로 parse()는 숫자로 구성된 문자열을 인수로 받은 후, 정수나 소수로 변환한다.

1
2
3
4
5
6
julia> parse(Int64, "32")
32
julia> parse(Float64, "3.14159")
3.14159
julia> parse(Int64, "Hello")
ERROR: ArgumentError: invalid base 10 digit 'H' in "Hello"

또한 trunc()는 소수를 인수로 받아 나머지들을 제거한 후, 남은 정수만을 반환한다.

1
2
3
4
julia> trunc(Int64, 3.99999)
3
julia> trunc(Int64, -2.3)
-2

float()는 정수를 소수로 변환한다.

1
2
julia> float(32)
32.0

마지막으로, string()는 인수들을 모두 문자로 변환한다.

1
2
3
4
julia> string(32)
"32"
julia> string(3.14159)
"3.14159"

수학 함수

줄리아는 대부분의 기본적인 수학 함수들이 내장되어 있다.

1
2
ratio = signal_power / noise_power
decibels = 10 * log10(ratio)

위의 예시에서는 log10을 사용하여 신호와 잡음의 비율을 데시벨 단위로 계산한다. (신호 변수인 signal_power과 잡음 변수인noise_power가 정의되었다는 전제 하에) 또한 자연로그를 계산하는 log 또한 제공된다.

1
2
radians = 0.7
height = sin(radians)

다음의 예시는 해당 radianssin()에 적용한다. 위의 예시에서 볼 수 있듯이 줄리아는 sin() 및 기타 삼각 함수 (cos, tan 등) 또한 제공한다.

1
2
3
4
5
6
julia> degrees = 45
45
julia> radians = degrees / 180 * π
0.7853981633974483
julia> sin(radians)
0.7071067811865475

각도(degree)를 통해 radians을 구하기 위해서는 각도를 180으로 나누고 π를 곱해야 한다. 줄리아에서는 π도 바로 사용할 수 있으며, 소수점 16자리까지 정확하다.

컴포지션(composition)

지금까지 우리는 변수, 표현식, 명령문과 같은 프로그램의 요소들이 어떻게 결합하는지에 대해서 이야기하고자 한다.
프로그래밍 언어의 가장 유용한 특징 중 하나는 작은 요소들을 가져와서 프로그램을 구성할 수 있다는 것이다. 예를 들어 함수의 인수로 산술연산자를 포함한 모든 종류의 표현식들을 사용할 수 있다.

1
2
x = sin(degrees / 360 * 2 * π)
x = exp(log(x+1))

할당문의 왼쪽은 무조건 변수 이름이 와야 한다. 이 한 가지의 규칙만 어기지 않는다면, 표현식 대부분에 값을 넣을 수 있다.

1
2
3
4
julia> minutes = hours * 60 # right
120
julia> hours * 60 = minutes # wrong!
ERROR: syntax: "60" is not a valid function argument name

새로운 함수 만들기

지금까지 우리는 줄리아가 제공하는 함수들을 사용해왔다. 하지만 그외에도 새로운 함수를 만들어 사용할 수 있다. 새로운 함수를 만드는 방법은 새로운 함수의 이름을 설정하고 실행될 일련의 명령문들의 순서를 설정하는 것이다. 예를 들면 아래와 같다.

1
2
3
4
function printlyrics()
println("I'm a lumberjack, and I'm okay.")
println("I sleep all night and I work all day.")
end

function은 함수를 정의하는 키워드이다. 위의 예시에서 함수의 이름은 printlyrics이다. 함수 이름에 대한 규칙은 변수 이름 만드는 규칙과 동일하다. (거의 모든 유니코드는 사용가능하지만 숫자를 이름의 첫 번째 문자로 사용할 수는 없다. 키워드 이름은 사용할 수 없다.)
이름 뒤의 빈 괄호는 함수가 인수를 가지지 않는다는 것을 나타낸다.
지금까지 설명한 함수의 첫 번째 줄은 헤더(header)라고 부르며, 그 외 나머지는 본문(body)라고 부른다.
본문에는 여러 명령문들이 포함되어 있으며, 함수는 키워드 end로 종료된다.
또한 가독성을 위해서 본문은 들여 쓰기로 작성해야 한다.

대화식 모드에서 함수를 정의한다면 헤더부터 본문까지 한 줄씩 작성하면 된다. 본문 작성이 끝나고 난 후에는 end를 입력하여 함수 정의를 끝내주어야 한다. 새로 만든 함수를 사용하는 방법은 기존에 있던 함수와 같이 이름을 사용하여 호출하는 것이다.

1
2
3
julia> printlyrics()
I'm a lumberjack, and I'm okay.
I sleep all night and I work all day.

새로 만든 함수는 다른 함수 본문에 넣어서 사용할 수도 있다. 아래의 예시를 보자.

1
2
3
4
function repeatlyrics()
printlyrics()
printlyrics()
end

repeatlyrics()는 앞서 만들었던 printlyrics()를 두 번 실행하도록 정의하였다.
이후 repeatlyrics()를 호출한다면 다음과 같은 결과를 얻을 수 있다.

1
2
3
4
5
julia> repeatlyrics()
I'm a lumberjack, and I'm okay.
I sleep all night and I work all day.
I'm a lumberjack, and I'm okay.
I sleep all night and I work all day.

함수 정의와 사용

앞 장에서 만들었던 함수 정의와 사용 코드를 결합한 전체 프로그램은 다음과 같다.

1
2
3
4
5
6
7
8
9
10
11
function printlyrics()
println("I'm a lumberjack, and I'm okay.")
println("I sleep all night and I work all day.")
end

function repeatlyrics()
printlyrics()
printlyrics()
end

repeatlyrics()

위의 프로그램은 repeatlyrics()printlyrics()를 정의한다. 함수를 정의하는 코드는 오로지 함수 객체를 만드는 역할만 하며, 함수가 호출되기 전까지는 함수 본문의 코드가 진행되지 않는다. 따라서 특정 함수를 사용하고 싶다면, 함수를 먼저 정의한 후에 함수를 호출해야 한다.

실행 과정

함수의 정의와 사용을 올바르게 사용하기 위해서는 실행 과정을 정확히 알고 있어야 한다.
실행은 항상 첫 번째 문장에서 시작하며, 명령문은 위에서 아래로 한 번씩만 작동한다.

1
2
3
4
5
6
7
8
9
10
11
function printlyrics()
println("I'm a lumberjack, and I'm okay.")
println("I sleep all night and I work all day.")
end

function repeatlyrics()
printlyrics()
printlyrics()
end

repeatlyrics()

이 예시를 통해 실행과정을 본다면, 줄리아는 먼저 printlyrics()를 정의한다. 그 후 repeatlyrics()를 정의한 후 호출한다. 호출된 repeatlyrics()는 위에 정의된 본문으로 돌아가 printlyrics()를 호출하는 명령문을 수행한다. 다음으로 호출된 printlyrics()는 해당 함수의 정의된 본문으로 돌아가 명령문을 수행한다. 그 결과가 repeatlyrics()를 호출한 결과로 도출되는 것이다.

매개 변수와 인수

몇 몇의 함수들은 인수를 필요로 한다. 예를 들어 sin()를 호출하기 위해서는 숫자 한 개를 인수로 입력해야 하며, parse()의 경우 숫자 유형과 문자열 두 개를 인수로 입력해야 한다.

함수 내부에서 인수는 매개 변수라는 변수에 할당된다. 다음 예시는 인수를 취하는 함수에 대한 정의이다.

1
2
3
4
function printtwice(bruce)
println(bruce)
println(bruce)
end

이 함수는 인수를 bruce라는 매개 변수에 지정하였기 때문에 함수가 호출되면 매개 변수 값을 두 번 인쇄한다.
또한 위의 함수는 인수에 어떤 값이 들어가든 인쇄한다.

1
2
3
4
5
6
7
8
9
julia> printtwice("Spam")
Spam
Spam
julia> printtwice(42)
42
42
julia> printtwice(π)
π = 3.1415926535897...
π = 3.1415926535897...

printtwice()에 내장되어 있는 println()는 모든 표현식들을 인수로 받으므로 printtwice() 또한 모든 표현식들을 인수로 사용할 수 있다.

1
2
3
4
5
6
julia> printtwice("Spam "^4)
Spam Spam Spam Spam
Spam Spam Spam Spam
julia> printtwice(cos(π))
-1.0
-1.0

인수는 함수가 호출되기 전에 우선적으로 값을 도출된다. 따라서 위의 예시에서 인수로 쓰인 표현식들은 먼저 값으로 정리 된 후 함수에 적용된다. "Spam "^4인수의 경우, 먼저 Spam Spam Spam Spam으로 도출된 다음에 함수의 인자로 적용된다.

또한 변수도 함수의 인자로 사용할 수 있다.

1
2
3
4
5
julia> michael = "Eric, the half a bee."
"Eric, the half a bee."
julia> printtwice(michael)
Eric, the half a bee.
Eric, the half a bee.

로컬 변수

로컬 변수(local)는 함수 정의 안에서 만들어진 변수를 의미한다. 예를 들면,

1
2
3
4
function cattwice(part1, part2)
concat = part1 * part2
printtwice(concat)
end

위의 함수는 두 개의 인수를 사용하여 곱한 다음에 그 결과를 두 번 인쇄한다. 이 함수에서 conat은 함수 내부에서 만들어진 로컬 변수이다.
위의 함수를 사용한 예시는 다음과 같다.

1
2
3
4
5
6
7
julia> line1 = "Bing tiddle "
"Bing tiddle "
julia> line2 = "tiddle bang."
"tiddle bang."
julia> cattwice(line1, line2)
Bing tiddle tiddle bang.
Bing tiddle tiddle bang.

위의 실행에서 cattwice()가 종료되는 순간 로컬 변수 conat은 삭제된다. 그렇기 때문에 conat을 인쇄하려고 하면 아래와 같은 오류를 만날 수 있다.

1
2
julia> println(concat)
ERROR: UndefVarError: concat not defined

매개 변수 또한 로컬 변수이다. 즉, printtwice() 정의에 사용된 매개 변수 bruce 또한 인쇄할 수 없다.

스택 다이어그램

어디에서 어떤 변수를 사용했는지 추척하려면 스택 다이어그램을 그리는 것이 유용하다. 스택 다이어그램에는 각 변수의 값과 그 값이 속한 함수도 표시된다.
각 함수는 프레임 단위로 그리며, 프레임 안에는 함수의 매개 변수와 로컬 변수를 쓴다.

예시는 다음과 같다.

stack diagram

프레임은 함수가 호출되는 순서대로 배열한다. 위의 예에서는 Maincattwice()를 호출하며, cattwice()printtwice()를 호출한다. 함수 외부에서 변수를 만들면, 이는 Main에 속한다.

각 매개 변수는 해당 인수와 동일한 값을 나타낸다. 따라서 part1line1과 같고 part2line2와 같으며, bruceconat과 같다.

함수 호출 도중에 오류가 발생가면 줄리아는 해당 오류의 위치를 알려 준다.
예를 들어, printtwice()에서 concat에 액세스하려고하면 UndefVarError가 발생한다.

1
2
3
4
ERROR: UndefVarError: concat not defined
Stacktrace:
[1] printtwice at ./REPL[1]:2 [inlined]
[2] cattwice(::String, ::String) at ./REPL[2]:3

위처럼 함수 목록들을 통해 오류가 발생한 프로그램 파일과 함수를 알려주는 것을 스택 추적이라고 한다
스택 추척의 함수 목록 순서는 스택 다이어그램의 프레임 순서와 반대이다.

결과 있는 함수와 결과 없는 함수

우리가 사용하는 어떤 함수는 결괏값을 반환하지만 어떤 함수는 결과를 반환하지 않는다. 우리는 전자를 결과 있는 함수(fruitful function)이라고 하며, 후자를 결과 없는 함수(void function)이라고 한다.

예로 결과 있는 함수를 먼저 본다면,

1
golden = (sqrt(5) + 1) / 2

위의 예시의 sqrt()는 유익한 함수로서 반환 값을 가지고 있기 때문에 바로 계산이 가능하다.
대화식 모드에서 함수를 호출하면, 다음과 같은 결과를 도출한다.

1
2
julia> sqrt(5)
2.23606797749979

하지만 스크립트 모드에서는 위의 예시처럼 함수를 호출하면 반환 값은 손실된다.

1
sqrt(5)

위의 스크립트는 아래의 값은 산출만 한다. (저장하지 않는다.)

1
2.23606797749979

따라서 스크립트 모드에서는 그다지 유익한 함수로서 작동하지 않는다.

결과 없는 함수는 화면에 값을 표시하거나 다른 영향을 줄 수는 있지만 반환 값은 없다. 아래의 예시를 보자.

1
2
3
4
5
julia> result = printtwice("Bing")
Bing
Bing
julia> show(result)
nothing

위의 예시는 변수 result에 함수의 값을 할당하였지만, 밑에 결과를 보면 아무것도 없는 것을 확인할 수 있다. 두번째 코드의 결과인 nothing은 그 자체로 특수한 데이터 타입이며, 문자열인 ”nothing”과는 구분된다.

1
2
julia> typeof(nothing)
Nothing

왜 함수인가

프로그램을 만드는데 있어 함수가 왜 유용한지 궁금할 것이다. 이에 대한 몇 가지 이유를 보자.

  • 함수는 명령문들을 묶어서 사용할 수 있으므로, 이후 프로그램을 쉽게 파악하고 디버깅을 할 수 있다.
  • 함수는 반복적인 코드들을 대체하여 프로그램 코드를 더 짧게 만들어준다.
  • 한번에 긴 프로그램을 설계하는 것보다 함수 단위로 나누어 설계하고 조립하는 것이 더 쉽다.
  • 잘 설계된 함수는 여러 프로그램에서 사용할 수 있다. (재사용 가능)
  • 줄리아에서는 함수들이 성능을 크게 향상시킬 수 있다.

디버깅

디버깅은 가장 중요한 기술 중 하나이다. 그 이유는 프로그램 진행과정을 완벽히 알고 있어야 가능한 기술이기 때문이다. 프로그램에 문제가 생긴다면 개발자는 프로그램의 진행과정에서 문제와 관련된 단서를 찾고, 해결방안을 모색해야 한다. 이런 과정들은 매우 어렵고 힘들지만, 꼭 필요한 능력이다.

어떤 사람들에게는 프로그래밍과 디버깅은 동일하다. 즉, 프로그래밍은 원하는 작업이 수행될 때까지 코드들을 점검하고 디버깅하는 과정이기 때문이다.