글을 시작하기에 앞서 해당 시리즈는 Allen Downey, Ben Lauwens의 저서인 Think Julia: How to Think Like a Computer Scientist를 바탕으로 작성된 글임을 알려드립니다.
이 포스트는 Functions를 한글로 요약 정리한 글입니다.
함수
프로그래밍에서 함수는 특정 계산을 수행하는 일련의 명령문이다. 함수를 정의할 때에는 이름과 명령문의 순서를 정하며, 이후 해당 이름을 통해서 함수를 호출할 수 있다.
함수 호출
우리는 이미 함수를 호출해 본 경험이 있다.
1 | julia> println("Hello, World!") |
이 함수의 이름은 println()
이다. 또한 괄호 안에 들어가는 표현식은 함수의 인수(argument)라고 부른다.
함수가 작동하는 과정은 일반적으로 인수를 받은 후 결과를 반환한다고 설명하며, 이런 결과를 반환 값이라고 한다.
줄리아는 데이터 타입을 바꿔주는 함수도 제공한다. 예로 parse()
는 숫자로 구성된 문자열을 인수로 받은 후, 정수나 소수로 변환한다.
1 | julia> parse(Int64, "32") |
또한 trunc()
는 소수를 인수로 받아 나머지들을 제거한 후, 남은 정수만을 반환한다.
1 | julia> trunc(Int64, 3.99999) |
float()
는 정수를 소수로 변환한다.
1 | julia> float(32) |
마지막으로, string()
는 인수들을 모두 문자로 변환한다.
1 | julia> string(32) |
수학 함수
줄리아는 대부분의 기본적인 수학 함수들이 내장되어 있다.
1 | ratio = signal_power / noise_power |
위의 예시에서는 log10
을 사용하여 신호와 잡음의 비율을 데시벨 단위로 계산한다. (신호 변수인 signal_power
과 잡음 변수인noise_power
가 정의되었다는 전제 하에) 또한 자연로그를 계산하는 log
또한 제공된다.
1 | radians = 0.7 |
다음의 예시는 해당 radians
을 sin()
에 적용한다. 위의 예시에서 볼 수 있듯이 줄리아는 sin()
및 기타 삼각 함수 (cos, tan 등) 또한 제공한다.
1 | julia> degrees = 45 |
각도(degree)를 통해 radians
을 구하기 위해서는 각도를 180으로 나누고 π
를 곱해야 한다. 줄리아에서는 π
도 바로 사용할 수 있으며, 소수점 16자리까지 정확하다.
컴포지션(composition)
지금까지 우리는 변수, 표현식, 명령문과 같은 프로그램의 요소들이 어떻게 결합하는지에 대해서 이야기하고자 한다.
프로그래밍 언어의 가장 유용한 특징 중 하나는 작은 요소들을 가져와서 프로그램을 구성할 수 있다는 것이다. 예를 들어 함수의 인수로 산술연산자를 포함한 모든 종류의 표현식들을 사용할 수 있다.
1 | x = sin(degrees / 360 * 2 * π) |
할당문의 왼쪽은 무조건 변수 이름이 와야 한다. 이 한 가지의 규칙만 어기지 않는다면, 표현식 대부분에 값을 넣을 수 있다.
1 | julia> minutes = hours * 60 # right |
새로운 함수 만들기
지금까지 우리는 줄리아가 제공하는 함수들을 사용해왔다. 하지만 그외에도 새로운 함수를 만들어 사용할 수 있다. 새로운 함수를 만드는 방법은 새로운 함수의 이름을 설정하고 실행될 일련의 명령문들의 순서를 설정하는 것이다. 예를 들면 아래와 같다.
1 | function printlyrics() |
function
은 함수를 정의하는 키워드이다. 위의 예시에서 함수의 이름은 printlyrics
이다. 함수 이름에 대한 규칙은 변수 이름 만드는 규칙과 동일하다. (거의 모든 유니코드는 사용가능하지만 숫자를 이름의 첫 번째 문자로 사용할 수는 없다. 키워드 이름은 사용할 수 없다.)
이름 뒤의 빈 괄호는 함수가 인수를 가지지 않는다는 것을 나타낸다.
지금까지 설명한 함수의 첫 번째 줄은 헤더(header)라고 부르며, 그 외 나머지는 본문(body)라고 부른다.
본문에는 여러 명령문들이 포함되어 있으며, 함수는 키워드 end
로 종료된다.
또한 가독성을 위해서 본문은 들여 쓰기로 작성해야 한다.
대화식 모드에서 함수를 정의한다면 헤더부터 본문까지 한 줄씩 작성하면 된다. 본문 작성이 끝나고 난 후에는 end
를 입력하여 함수 정의를 끝내주어야 한다. 새로 만든 함수를 사용하는 방법은 기존에 있던 함수와 같이 이름을 사용하여 호출하는 것이다.
1 | julia> printlyrics() |
새로 만든 함수는 다른 함수 본문에 넣어서 사용할 수도 있다. 아래의 예시를 보자.
1 | function repeatlyrics() |
repeatlyrics()
는 앞서 만들었던 printlyrics()
를 두 번 실행하도록 정의하였다.
이후 repeatlyrics()
를 호출한다면 다음과 같은 결과를 얻을 수 있다.
1 | julia> repeatlyrics() |
함수 정의와 사용
앞 장에서 만들었던 함수 정의와 사용 코드를 결합한 전체 프로그램은 다음과 같다.
1 | function printlyrics() |
위의 프로그램은 repeatlyrics()
와 printlyrics()
를 정의한다. 함수를 정의하는 코드는 오로지 함수 객체를 만드는 역할만 하며, 함수가 호출되기 전까지는 함수 본문의 코드가 진행되지 않는다. 따라서 특정 함수를 사용하고 싶다면, 함수를 먼저 정의한 후에 함수를 호출해야 한다.
실행 과정
함수의 정의와 사용을 올바르게 사용하기 위해서는 실행 과정을 정확히 알고 있어야 한다.
실행은 항상 첫 번째 문장에서 시작하며, 명령문은 위에서 아래로 한 번씩만 작동한다.
1 | function printlyrics() |
이 예시를 통해 실행과정을 본다면, 줄리아는 먼저 printlyrics()
를 정의한다. 그 후 repeatlyrics()
를 정의한 후 호출한다. 호출된 repeatlyrics()
는 위에 정의된 본문으로 돌아가 printlyrics()
를 호출하는 명령문을 수행한다. 다음으로 호출된 printlyrics()
는 해당 함수의 정의된 본문으로 돌아가 명령문을 수행한다. 그 결과가 repeatlyrics()
를 호출한 결과로 도출되는 것이다.
매개 변수와 인수
몇 몇의 함수들은 인수를 필요로 한다. 예를 들어 sin()
를 호출하기 위해서는 숫자 한 개를 인수로 입력해야 하며, parse()
의 경우 숫자 유형과 문자열 두 개를 인수로 입력해야 한다.
함수 내부에서 인수는 매개 변수라는 변수에 할당된다. 다음 예시는 인수를 취하는 함수에 대한 정의이다.
1 | function printtwice(bruce) |
이 함수는 인수를 bruce
라는 매개 변수에 지정하였기 때문에 함수가 호출되면 매개 변수 값을 두 번 인쇄한다.
또한 위의 함수는 인수에 어떤 값이 들어가든 인쇄한다.
1 | julia> printtwice("Spam") |
printtwice()
에 내장되어 있는 println()
는 모든 표현식들을 인수로 받으므로 printtwice()
또한 모든 표현식들을 인수로 사용할 수 있다.
1 | julia> printtwice("Spam "^4) |
인수는 함수가 호출되기 전에 우선적으로 값을 도출된다. 따라서 위의 예시에서 인수로 쓰인 표현식들은 먼저 값으로 정리 된 후 함수에 적용된다. "Spam "^4
인수의 경우, 먼저 Spam Spam Spam Spam
으로 도출된 다음에 함수의 인자로 적용된다.
또한 변수도 함수의 인자로 사용할 수 있다.
1 | julia> michael = "Eric, the half a bee." |
로컬 변수
로컬 변수(local)는 함수 정의 안에서 만들어진 변수를 의미한다. 예를 들면,
1 | function cattwice(part1, part2) |
위의 함수는 두 개의 인수를 사용하여 곱한 다음에 그 결과를 두 번 인쇄한다. 이 함수에서 conat
은 함수 내부에서 만들어진 로컬 변수이다.
위의 함수를 사용한 예시는 다음과 같다.
1 | julia> line1 = "Bing tiddle " |
위의 실행에서 cattwice()
가 종료되는 순간 로컬 변수 conat
은 삭제된다. 그렇기 때문에 conat
을 인쇄하려고 하면 아래와 같은 오류를 만날 수 있다.
1 | julia> println(concat) |
매개 변수 또한 로컬 변수이다. 즉, printtwice()
정의에 사용된 매개 변수 bruce
또한 인쇄할 수 없다.
스택 다이어그램
어디에서 어떤 변수를 사용했는지 추척하려면 스택 다이어그램을 그리는 것이 유용하다. 스택 다이어그램에는 각 변수의 값과 그 값이 속한 함수도 표시된다.
각 함수는 프레임 단위로 그리며, 프레임 안에는 함수의 매개 변수와 로컬 변수를 쓴다.
예시는 다음과 같다.
프레임은 함수가 호출되는 순서대로 배열한다. 위의 예에서는 Main
이 cattwice()
를 호출하며, cattwice()
는 printtwice()
를 호출한다. 함수 외부에서 변수를 만들면, 이는 Main
에 속한다.
각 매개 변수는 해당 인수와 동일한 값을 나타낸다. 따라서 part1
은 line1
과 같고 part2
는 line2
와 같으며, bruce
는 conat
과 같다.
함수 호출 도중에 오류가 발생가면 줄리아는 해당 오류의 위치를 알려 준다.
예를 들어, printtwice()
에서 concat
에 액세스하려고하면 UndefVarError
가 발생한다.
1 | ERROR: UndefVarError: concat not defined |
위처럼 함수 목록들을 통해 오류가 발생한 프로그램 파일과 함수를 알려주는 것을 스택 추적이라고 한다
스택 추척의 함수 목록 순서는 스택 다이어그램의 프레임 순서와 반대이다.
결과 있는 함수와 결과 없는 함수
우리가 사용하는 어떤 함수는 결괏값을 반환하지만 어떤 함수는 결과를 반환하지 않는다. 우리는 전자를 결과 있는 함수(fruitful function)이라고 하며, 후자를 결과 없는 함수(void function)이라고 한다.
예로 결과 있는 함수를 먼저 본다면,
1 | golden = (sqrt(5) + 1) / 2 |
위의 예시의 sqrt()
는 유익한 함수로서 반환 값을 가지고 있기 때문에 바로 계산이 가능하다.
대화식 모드에서 함수를 호출하면, 다음과 같은 결과를 도출한다.
1 | julia> sqrt(5) |
하지만 스크립트 모드에서는 위의 예시처럼 함수를 호출하면 반환 값은 손실된다.
1 | sqrt(5) |
위의 스크립트는 아래의 값은 산출만 한다. (저장하지 않는다.)
1 | 2.23606797749979 |
따라서 스크립트 모드에서는 그다지 유익한 함수로서 작동하지 않는다.
결과 없는 함수는 화면에 값을 표시하거나 다른 영향을 줄 수는 있지만 반환 값은 없다. 아래의 예시를 보자.
1 | julia> result = printtwice("Bing") |
위의 예시는 변수 result
에 함수의 값을 할당하였지만, 밑에 결과를 보면 아무것도 없는 것을 확인할 수 있다. 두번째 코드의 결과인 nothing
은 그 자체로 특수한 데이터 타입이며, 문자열인 ”nothing”
과는 구분된다.
1 | julia> typeof(nothing) |
왜 함수인가
프로그램을 만드는데 있어 함수가 왜 유용한지 궁금할 것이다. 이에 대한 몇 가지 이유를 보자.
- 함수는 명령문들을 묶어서 사용할 수 있으므로, 이후 프로그램을 쉽게 파악하고 디버깅을 할 수 있다.
- 함수는 반복적인 코드들을 대체하여 프로그램 코드를 더 짧게 만들어준다.
- 한번에 긴 프로그램을 설계하는 것보다 함수 단위로 나누어 설계하고 조립하는 것이 더 쉽다.
- 잘 설계된 함수는 여러 프로그램에서 사용할 수 있다. (재사용 가능)
- 줄리아에서는 함수들이 성능을 크게 향상시킬 수 있다.
디버깅
디버깅은 가장 중요한 기술 중 하나이다. 그 이유는 프로그램 진행과정을 완벽히 알고 있어야 가능한 기술이기 때문이다. 프로그램에 문제가 생긴다면 개발자는 프로그램의 진행과정에서 문제와 관련된 단서를 찾고, 해결방안을 모색해야 한다. 이런 과정들은 매우 어렵고 힘들지만, 꼭 필요한 능력이다.
어떤 사람들에게는 프로그래밍과 디버깅은 동일하다. 즉, 프로그래밍은 원하는 작업이 수행될 때까지 코드들을 점검하고 디버깅하는 과정이기 때문이다.