0%

18. 기타 문법들

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

이 포스트는 The Goodies: Syntax를 한글로 요약 정리한 글입니다.

기타 문법들 (Syntax)

이 책의 목적 중 하나는 가능한 적게 줄리아를 사용하는 법을 알려주는 것이었다. 만약 동일한 어떤 일을 하는 두 가지의 방법이 있다면, 그 중 하나만 이 책에 적었으며 나머지는 언급하지 않았다. 때로는 나머지 방법을 연습문제에 포함시켰다.

지금부터는 언급하지 않았던 것들 중에 알아두면 좋은 것들을 소개할 것이다. 줄리아는 실제로 필수적이지는 않은 여러 가지 기능들을 제공한다. 이런 기능 없이도 좋은 코드를 작성할 수 있지만, 기능을 포함하면 더 간결하고 읽기 쉽고 효율적인 코드를 작성할 수 있다.

이 장과 다음 장에서는 이전 장에서 생략한 아래의 사항들에 대해 설명할 것이다.

  • 문법 보충
  • Base에서 직접 사용 가능한 함수, 데이터 타입 및 매크로
  • 표준 라이브러리에 있는 함수, 데이터 타입 및 매크로

네임드 튜플 (Named Tuples)

네임드 튜플을 만들어 튜플의 구성요소 이름을 지정할 수 있다.

1
2
3
4
julia> x = (a=1, b=1+1)
(a = 1, b = 2)
julia> x.a
1

네임드 튜플에서는 점 문법인 (x.a)을 사용하여 필드에 접근할 수 있다.

함수

줄리아에서 함수는 간결한 문법으로 정의할 수 있다.

1
2
julia> f(x,y) = x + y
f (generic function with 1 method)

익명 함수 (Anonymous Functions)

이름 설정 없이 함수를 정의할 수 있다.

1
2
3
4
5
6
julia> x -> x^2 + 2x - 1
#1 (generic function with 1 method)
julia> function (x)
x^2 + 2x - 1
end
#3 (generic function with 1 method)

위의 코드는 익명 함수의 예시이다. 익명 함수는 종종 다른 함수의 인수로 사용된다.

1
2
3
julia> using Plots

julia> plot(x -> x^2 + 2x - 1, 0, 10, xlabel="x", ylabel="y")

Plot

위의 그래프는 Plot 명령어의 결과이다.

키워드 인수 (Keyword Arguments)

함수 인수 또한 아래와 같이 명명될 수 있다.

1
2
3
4
5
ulia> function myplot(x, y; style="solid", width=1, color="black")
###
end
myplot (generic function with 1 method)
julia> myplot(0:10, 0:10, style="dotted", color="blue")

함수에서 키워드 인수는 세미콜론(;) 뒤에 지정되며, 쉼표로 호출된다.

클로저 (Closures)

클로저는 함수가 함수의 호출 범위 밖에 정의된 변수를 캡쳐할 수 있게 하는 기술이다.

1
2
3
4
5
6
7
8
julia> foo(x) = ()->x
foo (generic function with 1 method)

julia> bar = foo(1)
#1 (generic function with 1 method)

julia> bar()
1

이 예시에서 함수 foo()foo()의 인수x에 접근하는 익명 함수를 반환한다. bar는 해당 익명함수를 지정하고 foo()의 인수인 값을 반환한다.

블럭 (Blocks)

블럭은 여러 명령문들을 그룹화하는 방법이다. 블럭은 키워드 begin로 시작하고 end로 끝난다.

인터페이스 디자인에서 @svg를 소개했다.

1
2
3
4
5
6
🐢 = Turtle()
@svg begin
forward(🐢, 100)
turn(🐢, -90)
forward(🐢, 100)
end

이 예시에서 @svg는 3개의 함수 호출을 그룹화한 블록인 단일 인수가 있다.

let 블럭

let 블럭은 값을 참조할 수 있는 위치와 같은 새로운 바인딩(binding)을 생성하는 데 유용하다.

1
2
3
4
5
6
7
8
9
10
11
12
julia> x, y, z = -1, -1, -1;

julia> let x = 1, z
@show x y z;
end
x = 1
y = -1
ERROR: UndefVarError: z not defined
julia> @show x y z;
x = -1
y = -1
z = -1

위 예시에서 첫 번째 @show매크로는 로컬 변수 x와 글로벌 변수 y와 정의되지 않은 z를 보여준다. 글로벌 변수는 그대로 유지된다.

do 블럭

14장의 읽고 쓰기 파트에서 우리는 작성이 끝난 파일을 닫아야 했다. 이것은 do 블럭을 사용하여 자동화할 수 있다.

1
2
3
4
5
6
julia> data = "This here's the wattle,\nthe emblem of our land.\n"
"This here's the wattle,\nthe emblem of our land.\n"
julia> open("output.txt", "w") do fout
write(fout, data)
end
48

위 예시에서 fout는 결과를 위해 사용된 파일 스트림이다.

이것은 기능적으로 아래의 코드와 같다.

1
2
3
4
5
6
julia> f = fout -> begin
write(fout, data)
end
#3 (generic function with 1 method)
julia> open(f, "output.txt", "w")
48

익명 함수는 open()의 첫 번째 인수로서 사용된다.

1
2
3
4
5
6
7
8
function open(f::Function, args...)
io = open(args...)
try
f(io)
finally
close(io)
end
end

do 블럭은 둘러싸는 범위에서 변수를 캡처할 수 있다. 예를 들어 위의 예시였던 open...do에 있는 변수 데이터는 외부 범위에서 캡처된다.

제어 흐름 (Control Flow)

삼항 연산자 (Ternary Operator)

삼항 연산자 ?:는 단일 표현식 값들 중에서 선택해야 할 때 사용되는 if-elseif문의 대안이다.

1
2
3
4
julia> a = 150
150
julia> a % 2 == 0 ? println("even") : println("odd")
even

?앞의 표현식은 조건을 나타낸 것이다. 만약 조건이 true: 이전의 표현식을 실행하며, false:뒤의 표현식을 실행한다.

단락 평가 (Short-Circuit Evaluation)

연산자 &&||는 단락 평가를 수행한다. 즉, 다음 인수는 오직 최종값을 결정할 때만 평가된다.

예를 들어 재귀적인 팩토리얼 함수 정의는 아래와 같다.

1
2
3
4
5
function fact(n::Integer)
n >= 0 || error("n must be non-negative")
n == 0 && return 1
n * fact(n-1)
end

테스크 (일명 Coroutines)

테스크(task)는 반환 없이 협력적으로 제어할 수 있는 제어 구조이다. 줄리아에서, 테스크는 첫 번째 인수로 channel 객체를 가지는 함수로 구현될 수 있다. channel은 함수에서 수신자에게 값을 전달하는 데 사용된다.

피보나치 수열은 테스크를 사용하여 생성할 수 있다.

1
2
3
4
5
6
7
8
9
function fib(c::Channel)
a = 0
b = 1
put!(c, a)
while true
put!(c, b)
(a, b) = (b, a+b)
end
end

put!channel객체에 값을 저장하며, take!은 객체로부터 값을 읽는다.

1
2
3
4
5
6
7
8
9
10
11
12
julia> fib_gen = Channel(fib);

julia> take!(fib_gen)
0
julia> take!(fib_gen)
1
julia> take!(fib_gen)
1
julia> take!(fib_gen)
2
julia> take!(fib_gen)
3

생성자 Channel은 테스크를 만든다. fib()put!을 호출할 때마다 일시 중지되고 take!이후에 다시 시작된다. 성능상의 이유로 시퀀스의 여러 값은 재개와 일시정지 동안에 Channel 객체에 저장된다.

또한 Channel 객체는 이터레이터(iterator)로써도 사용된다.

1
2
3
4
5
julia> for val in Channel(fib)
print(val, " ")
val > 20 && break
end
0 1 1 2 3 5 8 13 21

데이터 타입

기본 데이터 타입 (Primitive Types)

기본적으로 사용되는 구체적 데이터 타입들을 기본 데이터 타입이라고 한다. 다른 언어들과 달리 줄리아에서는 고유한 기본 데이터 타입을 선언할 수 있다. 표준 기본 데이터 타입도 같은 방식으로 정의된다.

1
2
3
4
primitive type Float64 <: AbstractFloat 64 end
primitive type Bool <: Integer 8 end
primitive type Char <: AbstractChar 32 end
primitive type Int64 <: Signed 64 end

위의 명령문에 있는 숫자들은 얼마나 많은 비트가 요구되는지 나타낸다.

아래의 예시는 기본 데이터 타입인 Byte와 생성자를 만든다.

1
2
3
4
5
6
julia> primitive type Byte 8 end

julia> Byte(val::UInt8) = reinterpret(Byte, val)
Byte
julia> b = Byte(0x01)
Byte(0x01)

reinterpret()은 8비트의 부호없는 정수 비트를 바이트에 저장하는 데 사용된다.

파라메트릭 데이터 타입 (Parametric Types)

줄리아의 데이터 타입 시스템은 파라메트릭, 즉 데이터 타입이 매개 변수를 가질 수 있다.

데이터 타입 매개 변수는 데이터 타입 이름 뒤에 중괄호로 묶여 있다.

1
2
3
4
struct Point{T<:Real}
x::T
y::T
end

위의 코드는 Real을 supertype으로 갖는 모든 데이터 타입인 T의 좌표를 가진 새로운 파라메트릭 타입 Point{T<:Real} 정의한다.

1
2
julia> Point(0.0, 0.0)
Point{Float64}(0.0, 0.0)

복합 데이터 타입 외에도 추상 데이터 타입이나 기본 데이터 타입에도 매개 변수가 있을 수 있다.

Tip
성능상의 이유로 구조체 필드에 구체적 데이터 타입을 사용하는 것이 절대적으로 권장되며, 그것이 Point를 빠르고 유연하게 만드는 좋은 방법이다.

데이터 타입 유니언 (Type Unions)

데이터 타입 유니언은 임의의 인수 타입으로 작용할 수 있는 추상적인 파라메트릭 데이터 타입이다.

1
2
3
4
5
6
julia> IntOrString = Union{Int64, String}
Union{Int64, String}
julia> 150 :: IntOrString
150
julia> "Julia" :: IntOrString
"Julia"

대부분의 컴퓨터 언어에서 데이터 타입 유니언은 데이터 타입을 추론하기 위한 내부 구조이다. 하지만 줄리아에서는 데이터 타입 유니언이 적을 때 효율적인 코드를 작성할 수 있기 때문에 이 기능을 사용자에게 노출한다. 이 기능은 줄리아를 사용하는 개발자에게 디스패치를 제어할 수 있는 유연성을 제공한다.

메서드 (Methods)

파라메트릭 메서드 (Parametric Methods)

메서드 정의 또한 데이터 타입 매개 변수를 가질 수 있다.

1
2
3
4
5
6
julia> isintpoint(p::Point{T}) where {T} = (T === Int64)
isintpoint (generic function with 1 method)
julia> p = Point(1, 2)
Point{Int64}(1, 2)
julia> isintpoint(p)
true

함수형 객체 (Function-like Objects)

줄리아에서는 어떤 임의의 객체도 “호출 가능”하게 만들 수 있다. 이런 “호출 가능”한 객체를 펑터(functor)라고도 부른다.

1
2
3
4
5
6
7
8
9
10
11
struct Polynomial{R}
coeff::Vector{R}
end

function (p::Polynomial)(x)
val = p.coeff[end]
for coeff in p.coeff[end-1:-1:1]
val = val * x + coeff
end
val
end

위의 Polynomial을 평가하기 위해서는 간단하게 호출하면 된다.

1
2
3
4
julia> p = Polynomial([1,10,100])
Polynomial{Int64}([1, 10, 100])
julia> p(3)
931

생성자 (Constructors)

파라메트릭 데이터 타입은 명시적 또는 암시적으로 생성될 수 있다.

1
2
3
4
5
6
julia> Point(1,2)         # implicit T
Point{Int64}(1, 2)
julia> Point{Int64}(1, 2) # explicit T
Point{Int64}(1, 2)
julia> Point(1,2.5) # implicit T
ERROR: MethodError: no method matching Point(::Int64, ::Float64)

기본 내부 생성자 및 외부 생성자는 각 T에서 생성된다.

1
2
3
4
5
6
7
struct Point{T<:Real}
x::T
y::T
Point{T}(x,y) where {T<:Real} = new(x,y)
end

Point(x::T, y::T) where {T<:Real} = Point{T}(x,y);

그리고 xy는 같은 데이터 타입이어야 한다.

xy가 다른 데이터 타입을 가지면, 외부 생성자를 아래와 같이 정의될 수 있다.

1
Point(x::Real, y::Real) = Point(promote(x,y)...);

promote()의 세부사항은 아래에서 다룰 것이다.

전환 및 프로모션 (Conversion and Promotion)

줄리아는 인수를 공동 데이터 타입으로 승격시키는 시스탬을 가지고 잇다. 이 작업은 자동으로 수행되지는 않지만 쉽게 확장할 수 있다.

전환 (Conversion)

값은 다른 데이터 타입으로 변환될 수 있다.

1
2
3
4
5
6
7
8
julia> x = 12
12
julia> typeof(x)
Int64
julia> convert(UInt8, x)
0x0c
julia> typeof(ans)
UInt8

또한 고유한 convert메서드도 추가할 수 있다.

1
2
3
4
julia> Base.convert(::Type{Point{T}}, x::Array{T, 1}) where {T<:Real} = Point(x...)

julia> convert(Point{Int64}, [1, 2])
Point{Int64}(1, 2)

프로모션 (Promotion)

프로모션은 혼합 데이터 타입의 값을 단일 공통 데이터 타입으로 변환하는 것이다.

1
2
julia> promote(1, 2.5, 3)
(1.0, 2.5, 3.0)

promote()의 메서드 일반적으로 직접 정의되지는 않지만, 보조 함수인 promote_rule()은 프로모션의 규칙을 나타내는 데 사용된다.

1
promote_rule(::Type{Float64}, ::Type{Int32}) = Float64

메타프로그래밍 (Metaprogramming)

줄리아 코드는 언어 자체의 데이터 구조로서 표현할 수 있다. 이는 프로그램이 자체적으로 코드를 변환하고 생성할 수 있도록 허락한다.

표현식

모든 줄리아 프로그램은 문자열로서 시작한다.

1
2
julia> prog = "1 + 2"
"1 + 2"

다음 단계는 줄리아 문자열의 문자 각각을 분석하여 Expr라는 줄리아 데이터 타입의 표현식 객체로 나타내는 것이다.

1
2
3
4
5
6
7
8
9
10
11
julia> ex = Meta.parse(prog)
:(1 + 2)
julia> typeof(ex)
Expr
julia> dump(ex)
Expr
head: Symbol call
args: Array{Any}((3,))
1: Symbol +
2: Int64 1
3: Int64 2

dump()은 expr 객체의 주석을 보여준다.

괄호 안에 :을 쓰거나 또는 quote 블록을 사용하여 표현식을 직접 구성할 수 있다.

1
2
3
julia> ex = quote
1 + 2
end;

eval

줄리아는 eval을 사용하여 표현식 객체를 평가할 수 있다.

1
2
julia> eval(ex)
3

모든 모듈은 범위 내에서 표현식을 평가하는 고유한 eval()을 가진다.

WARNING
eval()을 과도하게 사용하는 것은 사실 무언가가 잘못되었다는 것을 의미한다.

매크로 (Macros)

매크로는 프로그램에서 생성된 코드를 포함할 수 있다. 매크로는 Expr객체의 튜플을 컴파일된 표현식에 직접 매핑한다.

아래의 코드는 간단한 매크로이다.

1
2
3
macro containervariable(container, element)
return esc(:($(Symbol(container,element)) = $container[$element]))
end

매크로는 이름 앞에 @(at-sign)을 붙인다. @containervariable letters 1 매크로 호출은 아래의 코드로 대체된다.

1
:(letters1 = letters[1])

@macroexpand @containervariable letters 1은 디버깅에 매우 유용한 표현식을 반환한다.

이 예제는 매크로가 어떻게 인수 이름에 접근하는지 보여준다. 반환 표현식은 매크로 호출 환경에서 분석되어야 하므로 esc를 사용하여 “escaped” 처리를 해야한다.

NOTE
왜 매크로인가? 전체 프로그램이 실행되기 전에 매크로는 분석 시간 동안 맟춤형 코드의 조각들을 생성하고 포함한다.

생성 함수 (Generated Functions)

매크로 @generated는 인수의 타입에 따라 메서드에 대한 특수 코드를 생성한다.

1
2
3
4
@generated function square(x)
println(x)
:(x * x)
end

함수를 호출할 경우, 생성 함수는 일반 함수처럼 작동한다.

1
2
3
4
5
6
7
8
julia> x = square(2); # note: output is from println() statement in the body
Int64
julia> x # now we print x
4
julia> y = square("spam");
String
julia> y
"spamspam"

결측값 (Missing Values)

결측값은 Missing유형의 싱글톤(singleton) 인스턴스인 missing객체를 통해 표현될 수 있다.

배열은 결측값을 포함할 수 있다.

1
2
3
4
julia> a = [1, missing]
2-element Array{Union{Missing, Int64},1}:
1
missing

이런 배열 요소의 데이터 타입은 Union{Missing, T} 이며, 결측값이 아닌 T 데이터 타입이다.

결측값이 포함된 배열을 호출하면 리듀스 함수가 결측값을 반환한다.

1
2
julia> sum(a)
missing

이 상황에서는 결측값을 넘겨주는 skipmissing()을 사용하면 된다.

1
2
julia> sum(skipmissing([1, missing]))
1

C와 포트란 코드 호출 (Calling C and Fortran Code)

많은 코드들이 C 또는 포트란으로 작성된다. 테스트된 코드를 다시 사용하는 것이 직접 만든 알고리즘 버전을 코드로 작성하는 것보다 더 낫다. 줄리아는 ccall 문법을 사용하여 기존 C 또는 포트란 라이브러리를 직접 호출할 수 있다.

우리는 데이터베이스 파트에서 데이터베이스 함수의 GDBM 라이브러리에 줄리아 인터페이스를 도입하였다. 라이브러리는 C로 작성되며, 데이터베이스를 닫으려면 close (db)에 대한 함수를 호출해야 한다.

1
2
3
4
5
Base.close(dbm::DBM) = gdbm_close(dbm.handle)

function gdbm_close(handle::Ptr{Cvoid})
ccall((:gdbm_close, "libgdbm"), Cvoid, (Ptr{Cvoid},), handle)
end

dbm 객체는 Ptr{Cvoid}데이터 타입의 필드 handle을 가지고 있다. 이 필드에는 데이터베이스를 참조하는 C 포인터가 있다. 데이터베이스를 닫으려면 C 함수인 gdbm_close()는 데이터베이스를 가리키는 C 포인터만 반환 값 없이 호출해야 한다. 줄리아는 인수를 가진 ccall()을 사용하여 직접 수행한다.

  • 우리가 호출하고자하는 함수의 이름을 요소로 가지는 튜플: gdbm_close 및 문자열로 지정된 공유 라이브러리 "libgdm"
  • 반환 데이터 타입: Cvoid
  • 인수 데이터 타입의 튜플: (Ptr{Cvoid})
  • 인수 값: handle

GDBM 라이브러리의 완전한 맵핑은 ThinkJulia 소스에서 예제로 찾을 수 있다.