AI/기타

[Mojo] Mojo 기초 문법

sangwonYoon 2023. 9. 22. 14:24

Mojo는 파이썬의 문법과 동적인 특징을 그대로 사용할 수 있기 때문에 파이썬 패키지에서 코드를 가져와 실행할 수 있다. 그러나 Mojo는 파이썬에 단순히 syntax sugar(코드를 읽거나 작성하기 편하도록 디자인 된 문법)를 추가한 언어가 아닌, 파이썬과 비교했을 때 시스템 프로그래밍 기능, 타입 검사, 메모리 안전성, 차세대 컴파일러 기술 측면에서 한층 발전한 언어라고 소개하고 있다.

Mojo는 아직 개발이 진행중인 언어이기 때문에 아직 모든 파이썬의 기능을 지원하지는 않지만, 대부분의 기능을 지원한다. 따라서 Mojo의 문법은 파이썬의 문법과 닮은 부분이 매우 많다. 이번 포스팅에서는 파이썬과 동일한 Mojo의 문법을 제외하고, Mojo 고유의 문법에 대해서 알아보자.

이 포스트는 Mojo v0.2.1(23년 9월 20일) 기준으로 작성되었음을 알립니다.

기초 문법을 다루기 앞서, Mojo 코드는 AOT(ahead-of-time) 또는 JIT(just-in-time)으로 컴파일될 수 있다. 즉, 다시 말해 소스 코드를 미리 컴파일 한 뒤, 컴파일된 파일을 실행시킬 수 있고, 실행 시점에 소스 코드를 컴파일하여 실행시킬 수 있다.

 

함수

main 함수

다른 컴파일 언어들과 마찬가지로 Mojo 프로그램을 실행하기 위해서는 main() 함수가 반드시 필요하다.

fn main():
    var x: Int = 1
    x += 1
    print(x)

물론 프로그램이 아닌, 모듈이나 API 라이브러리를 작성할 때에는 main() 함수를 생략해도 된다.

 

fn 키워드

파이썬에서 사용되는 def 키워드 또한 Mojo에서 사용 가능하지만, fn 키워드는 def 키워드와 조금 다른 방식으로 사용된다.

def 키워드는 파이썬 스타일로 함수를 동적으로 선언할 수 있다.

반면, fn 키워드로 함수를 선언하게 되면 강력한 타입 검사와 메모리 안전성을 강화할 수 있다.

 

변수

var과 let 키워드

fn main():
    var x: Int = 1
    x += 1
    print(x)

이 main() 함수에서 변수 x를 var 키워드로 선언하게 되면 가변 변수(값을 바꿀 수 있는 변수)로, let 키워드로 선언하게 되면 불변 변수(값을 바꿀 수 없는 변수)로 생성할 수 있다.

따라서 위 main() 함수의 변수 x를 let 키워드로 선언하게 되면, 다음과 같은 컴파일 오류가 발생할 것이다. 

fn main():
    let x: Int = 1
    x += 1
    print(x)
error: Expression [15]:7:5: expression must be mutable for in-place operator destination
    x += 1
    ^

 

만약, main() 함수에서 var 키워드를 제거하게 되면 에러가 발생한다. 왜냐하면 fn 키워드로 생성된 함수 내부에서 선언된 변수는 반드시 명시적 변수 선언이 필요하기 때문이다. (이 부분에서 def 키워드로 생성된 함수와 차이점이 있다.)

 

타입 선언

변수의 타입에 대한 명시적인 선언은 함수의 body에서는 필수적이지 않다. 타입 선언을 생략할 경우, Mojo가 타입을 추론한다.

fn do_math():
    let x: Int = 1
    let y = 2
    print(x + y)

do_math() 
# 출력: 3

 

그러나, fn 함수의 인자반환 값에는 타입을 명시적으로 선언해야 한다.

fn add(x: Int, y: Int) -> Int:
    return x + y

 

함수의 인자

borrowed 키워드

fn 함수의 인자들은 기본적으로 값을 변경할 수 없다. 즉, 인자를 “빌린” 상태이며, 이를 명시적으로 선언하기 위해서는 borrowed 키워드를 사용하여 선언할 수 있다.

# 이 함수는 바로 위에 있는 add() 함수와 동일하게 동작한다.
fn add(borrowed x: Int, borrowed y: Int) -> Int:
    return x + y

 

inout 키워드

인자의 값을 수정하고 싶다면, inout 키워드를 사용해야 한다. 함수 내부(in)의 변화를 함수 외부(out)에서도 볼 수 있다는 의미이다.

fn add_inout(inout x: Int, inout y: Int) -> Int:
    x += 1
    y += 1
    return x + y

var a = 1
var b = 2
c = add_inout(a, b)
print(a) # 2
print(b) # 3
print(c) # 5

함수 내부에서 바뀐 변수의 값이 함수가 종료된 이후에도 유지되는 것을 확인할 수 있다.

 

owned 키워드

또 다른 옵션으로 owned 키워드가 있다. 함수의 인자를 owned 키워드로 선언할 경우, 함수 내에서 변수의 값을 수정하더라도 함수 외부에서는 영향받지 않는다.

fn set_fire(owned text: String) -> String:
    text += "🔥"
    return text

fn mojo():
    let a: String = "mojo"
    let b = set_fire(a)
    print(a)
    print(b)

mojo()
# 출력:
# mojo
# mojo🔥

위 코드에서 변수 a의 값를 복사하여 인자 text에게 넘기기 때문에 a 변수의 값은 바뀌지 않는다.

 

^ 연산자

그런데 만약 함수의 인자를 owned로 선언한 상황에서 변수를 복사하여 넘겨주지 않고 싶다면(복사하는 연산의 비용이 매우 비쌀 수 있기 때문에) 어떻게 해야할까? 그럴 때는 ^ 연산자를 사용하여 변수를 복사하지 않고 함수에게 변수를 넘겨줄 수 있다.

이 연산자는 지역 변수를 함수에게 전달한 다음, 지역 변수를 제거한다.

예를 들어 아래와 같은 코드를 실행할 경우,

fn set_fire(owned text: String) -> String:
    text += "🔥"
    return text

fn mojo():
    let a: String = "mojo"
    let b = set_fire(a^) # ^ 연산자 사용
    print(a) # 에러 발생
    print(b)

mojo()

지역 변수 a는 set_fire() 함수에게 전달된 뒤 제거되었기 때문에 print(a)를 실행할 때, 선언되지 않은 변수를 호출하는 에러가 발생한다.

 

fn set_fire(owned text: String) -> String:
    text += "🔥"
    return text

fn mojo():
    let a: String = "mojo"
    let b = set_fire(a^) # ^ 연산자 사용
    print(b)

mojo() 
# 출력: mojo🔥

따라서 위와 같이 print(a)를 제거해야 정상적으로 코드가 실행된다.

 

이러한 함수 인자 규칙은 메모리 최적화와 안전한 변수 접근, 변수 메모리 해제를 보장하기 위해 설계되었다. Mojo 컴파일러는 두 개의 변수가 동시에 같은 값을 수정하지 않도록 보장하고, 각 값의 수명을 명확하게 정의함으로써 메모리를 해제한 뒤 사용하거나 중복 해제와 같은 메모리 에러를 방지한다.

함수의 반환값은 항상 값을 복사하여 반환한다.

 

구조체

구조체는 파이썬의 클래스와 유사한 개념으로, 메소드, 필드, 연산자 오버로딩, 데코레이터 등을 지원한다. 그러나, 파이썬의 클래스와 달리 구조체는 정적이기 때문에 컴파일 시점에 결정된다. 따라서 동적 디스패치나 런타임 시점에 구조체를 변경하는 것을 허용하지 않는다.

디스패치란 어떤 메소드를 호출할 것인지 결정하여 실행하는 과정이다. 동적 디스패치(dynamic dispatch)는 메소드 오버라이딩이 되어있는 경우 실행 시점에 어떤 메소드를 실행할 지 결정된다.
- 위키백과
struct MyPair:
    var first: Int
    var second: Int

    fn __init__(inout self, first: Int, second: Int):
        self.first = first
        self.second = second
    
    fn dump(self):
        print(self.first, self.second)
let mine = MyPair(2, 4)
mine.dump() 
# 출력: 2 4

 

클래스는 아직까지 지원하지 않는다.

 

파이썬 통합

Mojo는 파이썬 모듈을 그대로 가져와 사용할 수 있다. 파이썬 모듈을 실행하는 메커니즘은 파이썬에서 사용하는 CPython 인터프리터를 활용하기 때문에 모든 파이썬 모듈과 호환된다.

아래 코드는 NumPy 모듈을 import하여 사용하는 코드이다.

from python import Python

let np = Python.import_module("numpy")

ar = np.arange(15).reshape(3, 5)
print(ar)
print(ar.shape)

# 출력:
# [[ 0  1  2  3  4]
#  [ 5  6  7  8  9]
#  [10 11 12 13 14]]
# (3, 5)

 

Python.import_module()을 통해 파이썬 모듈을 가져와 사용하는 것에는 문제가 없지만, 아직 Mojo 언어가 파이썬의 모든 기능을 포함하지 않기 때문에 파이썬 코드를 그대로 복사해서 Mojo에서 실행하게 되면 문제가 발생할 수 있다.

 

Mojo를 설치할 때, 설치 프로그램이 Mojo에서 사용할 파이썬 버전을 찾아 modular.cfg 설정 파일에 경로를 추가한다. 따라서 만약 파이썬 버전을 변경하거나 가상 환경을 전환하는 경우 Mojo가 잘못된 파이썬 라이브러리를 참조하게 되어 문제가 발생할 수 있다.
이 문제를 해결하기 위해서는 MOJO_PYTHON_LIBRARY 환경 변수를 수정해야 한다. 관련 이슈