문서화

문서화 (Documentation)

Julia에는 함수, 타입, 그 밖의 객체를 쉽게 문서화할 수 있도록 설계된 내장 문서 시스템이 있어요. 이번 절에서는 REPL에서 문서에 접근하는 방법부터, 함수 시그니처 표기, doctest, @doc 매크로의 고급 사용법까지 문서화에 필요한 내용을 하나씩 살펴볼게요. 코드뿐 아니라 문서도 잘 쓰는 습관을 길러 두면, 패키지를 만들 때나 남이 쓴 코드를 이해할 때 모두 큰 도움이 돼요.

출처: Julia 공식 문서: 문서화

문서 접근하기 (Accessing Documentation)

REPL이나 IJulia에서 ? 다음에 함수나 매크로의 이름을 쓰고 Enter를 누르면 문서에 접근할 수 있어요. 예를 들어

?cos
?@time
?r""

다음 각각은 해당하는 함수, 매크로, 문자열 매크로에 대한 문서를 보여줍니다. 대부분의 Julia 환경은 문서에 직접 접근하는 방법을 제공해요.

  • VS Code는 함수 이름 위에 마우스를 올리면 문서를 표시합니다. 사이드바의 Julia 패널에서 문서를 검색할 수도 있어요.
  • Pluto에서는 오른쪽 아래의 "Live Docs" 패널을 여세요.
  • Juno에서는 Ctrl-J, Ctrl-D로 커서 아래 객체의 문서를 보여줍니다.
  • Docs.hasdoc(module, name)::Bool은 이름에 docstring이 있는지 알려 줍니다. Docs.undocumented_names(module; all)은 모듈에서 문서화되지 않은 이름들을 반환해요.

문서 작성하기 (Writing Documentation)

Julia는 패키지 개발자와 사용자가 함수, 타입, 그 밖의 객체를 내장 문서 시스템을 통해 쉽게 문서화할 수 있게 해 줍니다.

기본 문법은 간단해요. 객체(함수, 매크로, 타입, 인스턴스) 바로 앞에 나타나는 모든 문자열은 그 객체를 문서화하는 것으로 해석됩니다. 이런 문자열을 docstring이라고 불러요. docstring과 문서화되는 객체 사이에 빈 줄이나 주석이 끼어들면 안 된다는 점을 기억하세요. 기본 예시를 보겠습니다.

"Tell whether there are too many foo items in the array."
foo(xs::Array) = ...

docstring과 문서화되는 객체 사이에 빈 줄이 있으면 둘을 분리시켜서 docstring이 효력을 잃게 됩니다.

문서는 Markdown으로 해석되므로, 들여쓰기와 코드 펜스를 이용해 코드 예시와 본문을 구분할 수 있어요. 엄밀히 말하면 어떤 객체든 다른 객체의 메타데이터로 연결될 수 있습니다. Markdown이 기본일 뿐이고, 다른 문자열 매크로를 만들어서 @doc 매크로에 넘기는 것도 가능해요.

Markdown 지원은 Markdown 표준 라이브러리에 구현되어 있고, 지원되는 문법의 전체 목록은 해당 문서에서 확인할 수 있어요.

여전히 Markdown을 쓰는 더 복잡한 예시를 보겠습니다.

"""
bar(x[, y])

Compute the Bar index between `x` and `y`.

If `y` is unspecified, compute the Bar index between all pairs of columns of `x`.

# Examples
```julia-repl
julia> bar([1, 2], [1, 2])
1

""" function bar(x, y) ...


위 예시처럼, 문서를 작성할 때 몇 가지 간단한 관례를 따르는 걸 권장합니다.

- **함수의 시그니처를 문서 맨 위에 네 칸 들여쓰기로 항상 표시**하세요. 그러면 Julia 코드로 출력됩니다.

이 시그니처는 Julia 코드에 있는 시그니처(`mean(x::AbstractArray)` 같은)와 동일하거나 더 단순한 형태일 수 있어요. 선택 인자는 가능하면 실제 Julia 문법에 맞춰 기본값과 함께(`f(x, y=1)`처럼) 표현하고, 기본값이 없는 선택 인자는 대괄호 안에 넣어요. `f(x[, y])`나 `f(x[, y[, z]])`처럼요. 대안으로 여러 줄을 쓰는 방법도 있어요. 선택 인자가 없는 줄 하나와, 있는 줄들로요. 이 방법은 주어진 함수의 관련 메서드 여러 개를 문서화할 때도 쓸 수 있습니다. 함수가 키워드 인자를 많이 받는다면, 시그니처에는 `<keyword arguments>` 자리 표시자만 넣고(`f(x; <keyword arguments>)`처럼), 전체 목록은 `# Arguments` 절(아래 4번 참고)에 적어 주세요.

반환 타입을 문서화하거나 반환 값에 이름을 줄 때는 이런 스타일을 씁니다.

```julia-repl
# Naming the return value or its type is not necessary (this is the most common case)
"""
sum(itr; [init])

...
"""

# The return type is easily documented and critical to the semantics of this function
"""
vec(x::AbstractArray)::AbstractVector

...
"""

# Naming and/or destructuring the return value clarifies the semantics of this function
"""
splitdir(path::AbstractString) -> (dir::AbstractString, file::AbstractString)
...
"""

반환 타입을 포함할 때는 시그니처 뒤에 ::로 구분해서 쓰고, 이름 있는 반환 값은 양쪽에 공백을 둔 ->로 구분해서 써요. 반환 타입과 반환 값은 가능하면 유효한 Julia 표현식이어야 해요. 반환 타입이나 반환 값을 주석으로 다는 매크로 docstring 시그니처는, 매크로 인자가 끝나는 지점과 반환 타입·반환 값이 시작되는 지점을 명확히 하기 위해 괄호를 사용해야 합니다.

  • 단순화된 시그니처 블록 뒤에, 함수가 무엇을 하는지 또는 객체가 무엇을 나타내는지 설명하는 한 줄짜리 문장을 하나 포함하세요. 필요하면 빈 줄 뒤의 두 번째 문단에서 더 자세히 설명할 수 있어요.

함수를 문서화할 때 한 줄짜리 문장은 3인칭이 아니라 명령형("이걸 해라", "저걸 반환해라")을 써야 합니다. "Returns the length..."처럼 쓰지 말라는 뜻이에요. 문장은 마침표로 끝나야 합니다. 함수의 의미를 쉽게 요약할 수 없다면, 여러 개의 조합 가능한 조각으로 나누는 게 도움이 될 수 있어요(다만 모든 경우에 반드시 그래야 하는 절대 요건으로 받아들이진 마세요).

  • 반복하지 마세요.

함수 이름은 시그니처가 알려 주므로, 문서를 "The function bar..."로 시작할 필요가 없어요. 바로 본론으로 들어가세요. 마찬가지로 시그니처가 인자의 타입을 명시한다면, 설명에서 그걸 다시 언급하는 건 중복이에요.

  • 꼭 필요할 때만 인자 목록을 제공하세요.

단순한 함수는 인자의 역할을 함수 목적 설명에서 직접 언급하는 편이 더 명확한 경우가 많아요. 인자 목록은 이미 다른 곳에서 제공된 정보를 반복할 뿐이니까요. 하지만 인자가 많은 복잡한 함수(특히 키워드 인자)에게는 인자 목록을 제공하는 게 좋은 생각일 수 있어요. 그럴 때는 함수의 일반적인 설명 뒤에 # Arguments 제목 아래에 놓고, 인자마다 - 불릿 하나씩 씁니다. 목록은 인자의 타입과 (있다면) 기본값을 언급해야 해요.

"""
...
# Arguments
- `n::Integer`: the number of elements to compute.
- `dim::Integer=1`: the dimensions along which to perform the computation.
...
"""
  • 관련 함수에 대한 힌트를 제공하세요.

때로는 관련 기능을 가진 함수들이 있죠. 발견성을 높이기 위해 이런 함수들의 짧은 목록을 See also 문단에 제공해 주세요.

See also [`bar!`](@ref), [`baz`](@ref), [`baaz`](@ref).
  • 코드 예시는 # Examples 절에 포함하세요.

예시는 가능하면 언제나 doctest로 작성해야 해요. doctest는 ````jldoctest로 시작하는 펜스 코드 블록으로, Julia REPL을 흉내 내는 julia>` 프롬프트 여러 개와 입력·예상 출력을 담고 있습니다.

doctest는 Documenter.jl이 활성화해 줍니다. 더 자세한 문서는 Documenter의 매뉴얼을 참고하세요.

예를 들어 다음 docstring에서는 변수 a가 정의되고, 그 뒤에 Julia REPL에 출력되는 대로의 예상 결과가 나옵니다.

"""
Some nice documentation here.

# Examples
```jldoctest
julia> a = [1 2; 3 4]
2×2 Matrix{Int64}:
1 2
3 4

"""


doctest에서는 `rand` 같은 RNG 관련 함수 호출을 피해야 합니다. Julia 세션마다 일관된 출력이 나오지 않으니까요. 난수 생성 관련 기능을 보여주고 싶다면, 자기 자신의 RNG 객체를 명시적으로 만들고 시드를 설정한 다음(`Random`을 참고), doctest하는 함수들에 그 객체를 넘기는 방법이 있어요.

운영체제 워드 크기(`Int32` 또는 `Int64`)와 경로 구분자 차이(`/` 또는 `\`)도 일부 doctest의 재현성에 영향을 줍니다.

doctest에서 공백은 중요하다는 점을 기억하세요! 예를 들어 배열의 pretty-printing 출력을 어긋나게 정렬하면 doctest가 실패해요.

그런 다음 `make -C doc doctest=true`를 실행해서 Julia 매뉴얼과 API 문서의 모든 doctest를 돌리고, 예시가 제대로 동작하는지 확인할 수 있어요.

출력 결과가 잘렸음을 나타내려면, 검사를 멈춰야 하는 줄에 `[...]`라고 쓰면 됩니다. 이는 특히 doctest에서 스택트레이스(julia 코드의 줄에 대한 비영구적 참조를 담고 있는)를 감추는 데 유용해요. 예외가 던져지는 경우를 보여줄 때요.

```julia-repl
```jldoctest
julia> div(1, 0)
ERROR: DivideError: integer division error
[...]

테스트할 수 없는 예시는 ````julia`로 시작하는 펜스 코드 블록 안에 작성하면, 생성된 문서에서 하이라이팅이 제대로 됩니다.

가능하면 어디서든 예시는 자급자족적이고 실행 가능해야 해요. 독자가 의존성 없이 바로 시도해 볼 수 있도록요.

- **코드와 수식을 식별하기 위해 백틱을 사용**하세요.

Julia 식별자와 코드 발췌는 항상 백틱 `` ` `` 사이에 넣어 하이라이팅을 활성화하세요. LaTeX 문법의 수식은 이중 백틱 ```` ` `` ```` 사이에 넣을 수 있어요. LaTeX 이스케이프 시퀀스보다는 유니코드 문자를 사용하세요. 즉 `\alpha = 1` 대신 `α = 1`을 쓰는 거예요.

- **시작과 끝의 `"""` 문자는 각각 자기 줄에 두세요.**

즉, 이렇게 쓰는 게 아니라

```julia-repl
"""...
...
..."""
f(x, y) = ...

이렇게 쓰세요.

"""
...
...
"""
f(x, y) = ...

이렇게 하면 docstring이 어디서 시작하고 끝나는지 더 명확해져요.

  • 주변 코드에 쓰인 줄 길이 제한을 지키세요.

docstring은 코드와 같은 도구로 편집됩니다. 따라서 같은 관례를 따라야 해요. 줄은 최대 92자 너비로 쓰는 게 권장됩니다.

  • 커스텀 타입이 함수를 구현할 수 있도록 하는 정보를 # Implementation 절에 제공하세요. 이런 구현 세부 사항은 사용자보다는 개발자를 위한 것이라, 예를 들어 어떤 함수를 오버라이드해야 하고 어떤 함수가 자동으로 적절한 폴백을 쓰는지 설명해요. 이런 내용은 함수 동작의 주된 설명과 분리해 두는 편이 좋아요.

  • 긴 docstring은 # Extended help 제목으로 문서를 나누는 걸 고려하세요. 일반적인 도움말 모드는 제목 위의 내용만 보여 주고, 표현식 앞에 '?'를 추가하면(즉 "?foo" 대신 "??foo") 전체 도움말에 접근할 수 있어요.

함수와 메서드 (Functions & Methods)

Julia의 함수는 여러 구현, 즉 메서드를 가질 수 있어요. 일반 함수가 단일 목적을 갖는 게 좋은 관행이지만, Julia는 필요하다면 메서드를 각각 문서화하는 것도 허용합니다. 일반적으로는 가장 일반적인 메서드만, 아니면 함수 자체만 문서화해야 해요. function bar end처럼 메서드 없이 만들어진 객체 말이죠. 특정 메서드는 일반적인 메서드와 동작이 다를 때만 문서화해야 합니다. 어쨌든 다른 곳에서 제공된 정보를 반복해서는 안 돼요. 예를 들면

"""
*(x, y, z...)

Multiplication operator. `x * y * z *...` calls this function with multiple
arguments, i.e. `*(x, y, z...)`.
"""
function *(x, y, z...)
# ... [implementation sold separately] ...
end

"""
*(x::AbstractString, y::AbstractString, z::AbstractString...)

When applied to strings, concatenates them.
"""
function *(x::AbstractString, y::AbstractString, z::AbstractString...)
# ... [insert secret sauce here] ...
end

help?> *
search: * .*

*(x, y, z...)

Multiplication operator. x * y * z *... calls this function with multiple
arguments, i.e. *(x,y,z...).

*(x::AbstractString, y::AbstractString, z::AbstractString...)

When applied to strings, concatenates them.

일반 함수의 문서를 가져올 때는 각 메서드의 메타데이터를 catdoc 함수로 이어 붙여요. 이 함수는 물론 커스텀 타입을 위해 오버라이드할 수 있습니다.

고급 사용법 (Advanced Usage)

@doc 매크로는 첫 번째 인자를 두 번째 인자와 META라는 모듈별 사전에 연결해요.

문서 작성을 쉽게 만들기 위해 파서는 @doc라는 매크로 이름을 특별하게 처리합니다. @doc 호출에 인자가 하나인데, 한 줄 바꿈 뒤에 다른 표현식이 나온다면 그 추가 표현식이 매크로의 인자로 추가돼요. 따라서 다음 문법은 @doc에 대한 2-인자 호출로 파싱됩니다.

@doc raw"""
...
"""
f(x) = x

이렇게 하면 일반 문자열 리터럴이 아닌 다른 표현식(raw"" 문자열 매크로 같은)도 docstring으로 쓸 수 있어요.

문서를 가져올 때 @doc 매크로(또는 동등하게 doc 함수)는 모든 META 사전에서 주어진 객체와 관련된 메타데이터를 찾아 반환합니다. 반환된 객체(Markdown 콘텐츠 같은)는 기본적으로 지능적으로 스스로 표시돼요. 이 설계 덕분에 문서 시스템을 프로그래밍 방식으로 사용하기도 쉽습니다. 예를 들어 함수의 다른 버전들 사이에서 문서를 재사용할 때요.

@doc "..." foo!
@doc (@doc foo!) foo

Julia 1.11 이후에서는 @doc 매크로로 문서를 가져오려면 REPL stdlib이 로드되어 있어야 해요.

또는 Julia의 메타프로그래밍 기능과 함께 쓸 수도 있습니다.

for (f, op) in ((:add, :+), (:subtract, :-), (:multiply, :*), (:divide, :/))
@eval begin
$f(a, b) = $op(a, b)
end
end
@doc "`add(a, b)` adds `a` and `b` together" add
@doc "`subtract(a, b)` subtracts `b` from `a`" subtract

begin, if, for, let, 내부 생성자 같은 최상위가 아닌 블록의 문서도 @doc를 통해 문서 시스템에 추가해야 해요. 예를 들면

if condition()
@doc "..."
f(x) = x
end

이것은 condition()true일 때 f(x)에 문서를 추가합니다. f(x)가 블록 끝에서 스코프를 벗어나더라도 그 문서는 남아 있다는 점을 기억하세요.

메타프로그래밍을 활용해 문서 생성을 도울 수도 있어요. docstring 안에서 문자열 보간을 사용할 때는 $($name)처럼 $를 하나 더 써야 합니다.

for func in (:day, :dayofmonth)
name = string(func)
@eval begin
@doc """
$($name)(dt::TimeType) -> Int64

The day of month of a `Date` or `DateTime` as an `Int64`.

""" $func(dt::Dates.TimeType)
end
end

동적 문서 (Dynamic documentation)

때로는 타입의 인스턴스에 적절한 문서가 타입 자체가 아니라 그 인스턴스의 필드 값에 달려 있을 수 있어요. 이런 경우에는 커스텀 타입에 Docs.getdoc 메서드를 추가해서 인스턴스별로 문서를 반환하게 할 수 있습니다. 예를 들어

struct MyType
value::Int
end

Docs.getdoc(t::MyType) = "Documentation for MyType with value $(t.value)"

x = MyType(1)
y = MyType(2)

?x는 "Documentation for MyType with value 1"을 표시하고, ?y는 "Documentation for MyType with value 2"를 표시할 거예요.

문법 가이드 (Syntax Guide)

이 가이드는 문서화가 가능한 모든 Julia 문법 구조에 문서를 다는 방법을 종합적으로 보여 줍니다.

다음 예시에서 "..."는 임의의 docstring을 나타내는 데 쓰입니다.

$\ 문자

$\ 문자는 docstring에서도 여전히 문자열 보간이나 이스케이프 시퀀스의 시작으로 파싱됩니다. raw"" 문자열 매크로를 @doc 매크로와 함께 쓰면 이런 문자를 이스케이프하지 않아도 됩니다. docstring에 LaTeX나 보간이 포함된 Julia 소스 코드 예시가 있을 때 유용해요.

@doc raw"""
```math
\LaTeX

""" function f end


### 함수와 메서드

```julia-repl
"..."
function f end

"..."
f

함수 f에 docstring "..."를 추가합니다. 첫 번째 버전이 권장 문법이지만, 둘 다 동등해요.

"..."
f(x) = x

"..."
function f(x)
return x
end

"..."
f(x)

메서드 f(::Any)에 docstring "..."를 추가합니다.

"..."
f(x, y = 1) = x + y

두 개의 Method, 즉 f(::Any)f(::Any, ::Any)에 docstring "..."를 추가합니다.

매크로

"..."
macro m(x) end

@m(::Any) 매크로 정의에 docstring "..."를 추가합니다.

"..."
:(@m1)

"..."
macro m2 end

@m1@m2라는 이름의 매크로에 docstring "..."를 추가합니다.

타입

"..."
abstract type T1 end

"..."
mutable struct T2
...
end

"..."
struct T3
...
end

타입 T1, T2, T3에 docstring "..."를 추가합니다.

"..."
T1

"..."
T2

"..."
T3

타입 T1, T2, T3에 docstring "..."를 추가합니다. 이전 버전이 권장 문법이지만, 둘 다 동등해요.

"..."
struct T
"x"
x
"y"
y

@doc "Inner constructor"
function T()
new(...)
end
end

타입 T에 docstring "..."를, 필드 T.x"x"를, 필드 T.y"y"를, 내부 생성자 T()"Inner constructor"를 추가합니다. mutable struct 타입에도 적용됩니다.

모듈

"..."
module M end

module M

"..."
M

end

Module M에 docstring "..."를 추가합니다. Module 위에 docstring을 다는 게 권장 문법이지만, 둘 다 동등해요.

모듈 docstring은 모듈의 스코프 안에서 평가되어, 모듈에 정의되고 모듈로 가져온 모든 심볼에 접근할 수 있습니다.

"The magic number is $(MAGIC)."
module DocStringEval
const MAGIC = 42
end

표현식 위에 docstring을 놓아 baremodule을 문서화하면 그 모듈로 @doc가 자동으로 가져와집니다. 모듈 표현식이 문서화되지 않을 때는 이런 가져오기를 수동으로 해야 해요.

"..."
baremodule M
# ...
end

baremodule M

import Base: @doc

"..."
f(x) = x

end

전역 변수

"..."
const a = 1

"..."
b = 2

"..."
global c = 3

Binding a, b, c에 docstring "..."를 추가합니다.

BindingModule에서 특정 Symbol에 대한 참조를, 그 참조된 값 자체를 저장하지 않고 보관하는 데 쓰입니다.

const 정의가 다른 정의의 별칭을 만드는 데만 쓰이는 경우, 예를 들어 Base의 함수 div와 그 별칭 ÷처럼, 별칭을 문서화하지 말고 실제 함수를 문서화하세요. 별칭이 문서화되고 실제 정의는 문서화되지 않으면, 실제 정의를 검색할 때 문서 시스템(? 모드)이 별칭에 붙은 docstring을 반환하지 않아요.

예를 들어 이렇게 쓰는 게 아니라

f(x) = x + 1
"..."
const alias = f

이렇게 써야 합니다.

"..."
f(x) = x + 1
const alias = f
"..."
sym

sym과 연결된 값에 docstring "..."를 추가합니다. 다만 sym이 정의된 곳에서 문서화하는 게 더 권장됩니다.

여러 객체

"..."
a, b

문서화 가능한 표현식인 ab 각각에 docstring "..."를 추가합니다. 이 문법은 다음과 동등해요.

"..."
a

"..."
b

이런 식으로 문서화가 가능한 표현식은 몇 개든 함께 문서화할 수 있어요. 이 문법은 두 함수가 서로 관련되어 있을 때, 예를 들어 비-변형과 변형 버전인 ff!처럼, 유용할 수 있습니다.

매크로가 생성한 코드

"..."
@m expression

@m expression을 확장해서 생성된 표현식에 docstring "..."를 추가합니다. 이 덕분에 @inline, @noinline, @generated, 그 밖의 매크로로 장식된 표현식도 장식되지 않은 표현식과 같은 방식으로 문서화할 수 있어요.

매크로 작성자가 주의할 점으로, 단일 표현식만 생성하는 매크로만 자동으로 docstring을 지원합니다. 매크로가 여러 하위 표현식을 담은 블록을 반환한다면, 문서화해야 할 하위 표현식을 @__doc__ 매크로로 표시해야 해요.

@enum 매크로는 Enum을 문서화할 수 있도록 @__doc__를 사용합니다. 그 정의를 살펴보면 @__doc__를 올바르게 사용하는 방법의 예시가 될 거예요.

Core.@__doc__ — 매크로```julia-repl @doc(ex)


매크로가 반환한 표현식 중 문서화해야 할 것을 표시하는 데 쓰는 저수준 매크로예요. 둘 이상의 표현식이 표시되면 같은 docstring이 각 표현식에 적용됩니다.

```julia-repl
macro example(f)
quote
$(f)() = 0
@__doc__ $(f)(x) = 1
$(f)(x, y) = 2
end |> esc
end

@__doc__는 그것을 사용하는 매크로가 문서화되지 않으면 아무 효과가 없습니다.

이 절은 아주 미묘한 모서리 경우를 다루는데, 이는 매크로를 정의하면서 동시에 같은 확장 안에서 그 매크로를 사용하려 시도하는 매크로에만 관련돼요. 이런 매크로는 Julia 1.12 이전에는 작성할 수 없었고 지금도 아주 드뭅니다. 그런 매크로를 작성하는 게 아니라면 이 내용은 무시해도 돼요.

Julia 1.12 이전 버전에서는 매크로 확장이 Expr(:toplevel) 블록을 재귀적으로 확장했습니다. 이 동작은 1.12에서 매크로가 다른 매크로를 재귀적으로 정의하고 같은 반환 표현식 안에서 사용할 수 있게 바뀌었어요. 하지만 @__doc__의 기존 사용법과의 하위 호환성을 유지하기 위해, 문서 시스템은 @__doc__ 표시를 찾을 때 여전히 Expr(:toplevel) 블록을 확장합니다. 그 결과, 매크로-정의-매크로는 docstring이 달려 있을 때 관찰 가능한 동작 차이를 보입니다.

julia> macro macroception()
Expr(:toplevel, :(macro foo() 1 end), :(@foo))
end

julia> @macroception
1

julia> "Docstring" @macroception
ERROR: LoadError: UndefVarError: `@foo` not defined in `Main`

지원되는 해결 방법은 매크로를 정의하는 매크로 안에서 @__doc__ 매크로를 수동으로 확장하는 것입니다. 그러면 문서 시스템이 그 재귀 확장을 인식하고 억제해요.

julia> macro macroception()
Expr(:toplevel,
macroexpand(__module__, :(@__doc__ macro foo() 1 end); recursive=false),
:(@foo))
end

julia> @macroception
1

julia> "Docstring" @macroception
1

더 알아보기 (Learn more)