Julia는 JIT 컴파일할 수 있는 함수 안에 가능한 한 많은 코드를 넣도록 권장해요. 작은 함수를 많이 만드는 것이 설계상 성능이 좋기도 하고요.
그러다 보면 작고 단순한 함수가 많이 남게 되는데, 이런 함수들은 사소하지 않은 작업을 수행하려면 서로 조합해야 해요.
한 가지 뻔한 방법은 함수 호출을 중첩하는 거예요. 다음 예제는 매우 억지스럽지만, 요점은 잘 보여줘요.
julia> first.(titlecase.(reverse.(["my", "test", "strings"])))
3-element Vector{Char}:
'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
이 방법의 단점은 중첩이 깊어질수록 가독성이 빠르게 떨어진다는 거예요.
더 간단하고 유연한 방법이 필요해요.
수학자들이 즐겨 쓰는 기법인데, Julia는 수학의 문법을 그대로 가져왔어요.
∘ 연산자로 임의의 개수 함수를 composed할 수 있어요 (\circ를 입력하고 탭을 눌러요).
그 결과는 하나의 함수처럼 사용할 수 있어요.
julia> compfunc = first ∘ titlecase ∘ reverse
first ∘ titlecase ∘ reverse
julia> compfunc.(["my", "test", "strings"])
3-element Vector{Char}:
'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
# alternative syntax, giving the same result
julia> (first ∘ titlecase ∘ reverse).(["my", "test", "strings"])
몇 가지 짚어둘 점이 있어요:
또 다른 방법은 _수학자_보다는 _프로그래머_의 접근 방식이라고 할 수 있어요.
Pipelines는 오래전부터 Unix 셸 스크립트에서 쓰여 왔고, 최근에는 주류 프로그래밍 언어에서도 인기를 얻었어요 (F#이 이 방식을 처음 도입한 것으로 종종 언급돼요).
기본 개념은 어떤 데이터로 시작해서, 일련의 함수들을 파이프로 흘려보내 결과를 얻는 거예요.
파이프 연산자는 |>예요 (F#과 최근 버전의 R에서처럼요). Julia에는 브로드캐스트 버전인 .|>도 있어요.
julia> ["my", "test", "strings"] .|> reverse .|> titlecase .|> first
3-element Vector{Char}:
'Y': ASCII/Unicode U+0059 (category Lu: Letter, uppercase)
'T': ASCII/Unicode U+0054 (category Lu: Letter, uppercase)
'S': ASCII/Unicode U+0053 (category Lu: Letter, uppercase)
이제 실행은 엄격하게 왼쪽에서 오른쪽으로 진행되고, 각 함수의 출력은 화살표 방향으로 흘러 다음 함수의 입력이 돼요.
합성과 파이프라이닝을 설명할 때 쓴 함수들이 모두 하나의 인자만 받는다는 건 우연이 아니에요.
일부 순수 함수형 언어들은 첫 번째 인자를 함수로 파이프로 넘기면서 다른 인자들도 함께 포함할 수 있게 해요.
반대로 Julia는 파이프라인에 함수 이름(또는 이에 준하는 것)만 기대하고, 추가 인자는 받지 않아요.
여기에는 중요한 기술적 이유가 있어요 (currying이 언어 설계의 표준적인 부분이 아니라는 점과 관련이 있어요).
커링을 잘 모르는 많은 사람들은 이 제약이 부주의한 실수가 아니며, 앞으로의 Julia 버전에서도 바뀔 가능성이 낮다는 점을 그냥 받아들이면 돼요.
필요한 일을 해 주는 단일 인자 함수가 필요해요. 다행히 Julia에서 새 함수를 정의하는 건 쉬워요.
가장 간단하게는 anonymous function을 쓸 수 있어요.
예를 들어 입력 문자열 하나가 있고 밑줄을 기준으로 나누고 싶다면:
julia> "my_test_strings" |> (s -> split(s, '_'))
3-element Vector{SubString{String}}:
"my"
"test"
"strings"
그러면 그 벡터를 이전처럼 다른 함수로 파이프로 넘길 수 있어요.
이 경우 익명 함수를 괄호로 감싸는 건 선택 사항이지만, 일반적으로는 모호함을 줄이는 유용한 방법이에요.
마찬가지로 프로그램 앞부분에서 이름 있는 함수를 만들어 두고, 필요할 때 재사용할 수도 있어요.
Closures는 이 개념의 범위를 벗어나지만, 다른 언어에서 클로저에 익숙한 사람이라면 클로저가 단일 인자 함수를 만드는 더 유연한 방법을 제공한다는 걸 알아볼 수 있을 거예요.
julia> function makesplit(sep)
fs(str) = split(str, sep)
fs
end
makesplit (generic function with 1 method)
julia> f_us = makesplit('_')
(::var"#fs#32"{Char}) (generic function with 1 method)
julia> "my_test_strings" |> f_us
3-element Vector{SubString{String}}:
"my"
"test"
"strings"
# alternatively:
julia> "my_test_strings" |> makesplit('_')
3-element Vector{SubString{String}}:
"my"
"test"
"strings"
makesplit()을 한 번 정의하면 어떤 구분자에도 쓸 수 있어요.
makesplit('_')는 _함수 호출_이며, 다른 함수로 평가되고, 그 함수가 파이프로부터 입력을 받는다는 점에 유의해요.
헷갈려 보여도 처음에는 당연한 거예요 (연습하다 보면 더 명확해져요).
기본 Julia에서 파이프를 더 다용도로 만들자는 논의가 오래 이어졌지만, 여러 제안이 서로 호환되지 않아 합의에 이르지 못했어요.
그동안 사용자들은 특정 필요를 해결하는 다양한 설치형 패키지를 만드는, 늘 하던 방식을 택했어요. 어느 것도 Exercism 안에서는 동작하지 않지만, 관심 있다면 한번 살펴봐요: