Mojo 튜토리얼
Mojo는 Python의 완전한 Superset을 지향하며, Python의 단점인 느린 속도 등을 보완한 새로운 프로그래밍 언어이다. Python과는 달리, Mojo는 컴파일 언어이다.
소스 코드 파일의 확장자는 .mojo
, 또는 특이하게도 이모지인 .🔥
를 사용하고 있다.
아직 미완성인 언어이니 실제로는 아직 Python의 완전한 Superset도 아니고, 바로 설치하여 로컬 환경에서 Mojo로 작성한 프로그램을 실행하는 것도 불가능하다.
우선 공식 홈페이지에서 회원가입 후 사용 신청을 하면 이메일로 약 2~3일 후 링크가 온다.
Mojo로 작성한 프로그램을 실행하려면 해당 링크에서 제공하는 Jupyter Notebook인 Mojo Playground를 이용해야만 한다.
Mojo 문법
대부분은 Python과 공유한다. Python과 마찬가지로 들여쓰기로 블록을 구분하며, if
, for
과 같은 구문도 같은 방식으로 동작한다.
이하 내용에서는 Python과 구별되는 Mojo 문법과 작동 방식 등을 간략히 소개한다.
fn
으로 선언하는 함수
Mojo에서는 Python에서 사용하던 def
키워드로 함수를 정의할 수도, 새 키워드인 fn
을 사용하여 정의할 수도 있다.
def
키워드로 정의한 함수는 Python에서와 마찬가지로 Dynamic한 동작 방식을 보인다. 반면 fn
키워드로 작성한 함수는 Static하며 Type-strict하고, memory-safe한 동작 방식이 강제된다. 두 키워드 모두 상황에 따라 적절히 병행 사용하는 것이 권장되고 있으나, 여기서는 Python과 특별히 구분되는 fn
에 대해서만 다룬다.
Mojo는 다른 컴파일 언어들과 마찬가지로, entry point로써 main()
함수 정의가 필요하다. 단 Mojo는 Python 등과 마찬가지로 REPL 환경을 지원하므로 REPL 환경으로 프로그램을 구동하고 싶으면 main()
함수가 필요 없으나, .mojo
(.🔥
) 파일로 하나의 프로그램을 작성할 때는 필요하다.
fn
키워드로 작성한 main
함수는 다음과 같이 시작할 것이다.
fn main():
...
변수 선언
var
키워드로 일반적인 변수를, let
키워드로 수정 불가능한 변수(C의 const
와 비슷)를 선언할 수 있다.
각 변수는 다음과 같이 타입을 지정해 줄 수 있다.
var a: Int = 1
단, 타입 지정이 필수는 아니다. 타입 지정을 생략하면 Mojo가 타입을 추론(Type Inference)할 것이다. 그러나, 이하 서술할 함수 argument의 경우 반드시 타입을 지정해 줘야 한다.
함수 Arguments
앞서 언급했듯이, argument의 타입 지정은 필수이다. 그리고 리턴값의 타입 지정도 필수이다.
리턴값의 타입은 화살표 기호 ->
를 이용해서 명시 가능하다.
argument와 리턴값이 있는 함수는 다음과 같이 작성하면 된다.
fn add(x: Int, y: Int) -> Int:
return x + y
Argument Ownership
Mojo는 함수 argument를 정의할 때 ownership이라는 꽤 독특한 개념을 도입한다.
borrowed
기본적으로는 C에서의 함수 호출과 비슷하게 immutable reference로, argument로 전달된 변수의 값을 읽기만 할 뿐, 해당 변수의 값을 함수가 변경할 수는 없다.
이 경우의 ownership은 borrowed
라고 하는데, 이 ownership은 argument 이름 앞에 명시 가능하다.
fn add(borrowed x: Int, borrowed y: Int) -> Int:
return x + y
이건 위에서 정의의 함수와 완전히 동일하게 동작하는 함수이다. 생략된 borrowed
를 표시했을 뿐이다.
inout
inout
이라는 ownership
을 지정하면, argument로 전달된 변수를 변경하는 것이 가능하다.
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)
print(b)
print(c)
이 경우 변수 a
와 b
에는 add_inout()
함수에 의해 각각 1이 더해져 출력은 2 3 5
가 된다.
owned
이 ownership
을 지정하면 Mojo가 함수 호출 시 argument 대신 argument의 복사본을 만들어서 넘겨 준다. 이제 해당 함수는 그렇게 넘겨받은 argument에 대한 소유권(ownership
)을 완전히 갖게 된다.
함수 내부에서 변경될 수 있는 값은 원래 변수의 복사본이기 때문에, borrowed
와 비슷하게 원래 변수의 값은 변경되지 않는다. 내부적으로는, borrowed
보다도 C에서의 호출 방식과 흡사하다고 할 수 있다.
단, 함수 호출 시 argument 뒤에 ^
를 붙여서 전달하면 복사본을 만들지 않고 아예 해당 argument를 함수 내부로 가져와 버린다. 이 순간 argument는 함수 내부 블록에 속하기 때문에, 함수가 종료되면 해당 변수는 사라진다.
from String import String
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()
위 코드를 실행하면 print(a)
에서 컴파일 에러가 난다. 변수 a
는 이미 set_fire()
함수가 먹어버렸기 때문이다.
이런 behavior는 복사본을 만드는 것이 성능적 부담이 될 수 있으니 개발자가 원할 때 변수를 복사하지 않도록 함으로써 성능 향상을 꾀할 수 있도록 하기 위해 구현되었다고 한다.
Structures
struct
로 구조체를 생성할 수 있다. C의 구조체보다는 Python의 class
와 비슷하나, 완전히 compile-time에 bound 되며, dynamic dispatch가 불가능하며 runtime에 구조체를 변형시킬 수 없다.
struct MyPair:
var first: Int
var second: Int
# This "initializer" behaves like a constructor in other languages
fn __init__(inout self, first: Int, second: Int):
self.first = first
self.second = second
fn dump(inout self):
print(self.first)
print(self.second)
기본적인 구조체의 형태는 위와 같다. __init__
함수 정의부터 self
의 사용까지, Python의 class
와 매우 비슷하다.
주의할 점은 __init__
함수 정의 전 argument에 해당하는 변수들을 미리 선언하고 있다는 점이다.
선언한 구조체는 다음과 같이 사용 가능하다.
def pair_test() -> Bool:
let p = MyPair(1, 2)
# Uncomment to see an error:
# return p < 4 # gives a compile time error
return True
이렇게 우리는 p라는 MyPair
오브젝트를 얻을 수 있었다.
Python 모듈 가져오기
from PythonInterface import Python
let np = Python.import_module("numpy")
ar = np.arange(15).reshape(3, 5)
print(ar)
print(ar.shape)
위와 같이 Python
이라는 Mojo 모듈을 가져온 후에, Python.import_module()
메소드를 이용하여 Python 모듈을 가져올 수 있다.
참고 문헌
- Hello, Mojo🔥