트랙
/
Julia
Julia
/
학습 과정
/
함수 합성
함수

함수 합성 에서 Julia

1개의 연습 문제

함수 합성 소개

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 안에서는 동작하지 않지만, 관심 있다면 한번 살펴봐요:

GitHub에서 편집 링크가 새 창이나 탭에서 열려요

함수 합성 배우기