Open10

Mojo触れるようになった

みはなだみはなだ

とりあえずHelloMojo.ipynbをやっていく

変数の定義

Pythonと違い、imutableな変数宣言できる。

  • let: イミュータブル
  • var: ミュータブル

メリットとしては以下の2点

  1. システムプログラマが変更不可であることを担保するため
  2. 変数名のタイポによる代入ミスを防ぐ

2は雑にnotebookとか使っているとめっちゃあるので確かにわかる。
試したところ、宣言がなくても一応動いた。

def declare():
    let a = 1
    var b = 2
    c = 3
    
    print(a, b, c)
    # これはletに代入しているのでエラー
    # a = 3

declare()  # -> 1 2 3

変数宣言と同時に型の指定や後から変数を初期化することもできる。

def declare_with_type():
    let d: Int = 10
    let e: F32
    
    e = 0.5
    
    # 初期値を宣言した後なのでエラー
    # e = 0.1
    
    # 文字列型 -> String.Stringがある
    # 組み込みで以下の二つもあるけど違いが理解できず
    # StringLiteral: https://docs.modular.com/mojo/MojoBuiltin/StringLiteral.html
    # StringRef: https://docs.modular.com/mojo/MojoBuiltin/StringRef.html
    let f: String = "Hello"
    let g: String = "World"
    
    print(d, e, f, g)

declare_with_type()  # -> 10 0.500000 Hello World
みはなだみはなだ

String型にバグっぽいのがあるらしい。
letで宣言しても代入することができる。

from String import String

let a: String = "This is original text."  # -> This is original text.
print(a)

a = "asdf" # 変更できないはず
print(a) # -> asdf

https://github.com/modularml/mojo/issues/42

みはなだみはなだ

struct (構造体)

クラスとは違い、明示的にvarletでインスタンスの変数を宣言する必要がある。

struct MyStruct:
    
    var a: Int
    var b: F32
    var c: Int
    # let c: F32 = 2  # 今の所、letは使用できない
    
    fn __init__(self&, a: Int, b: F32, c: Int):
        self.a = a
        self.b = b
        self.c = c
    
    fn print_vars(self&):
        print("a: ", self.a)
        print("b: ", self.b)
        print("c: ", self.c)

var my_struct = MyStruct(1, 2, 3)
my_struct.print_vars()
# a:  1
# b:  2.000000
# c:  3

# 変数の変更
print("Change b 2 -> -2")
my_struct.b = -2
my_struct.print_vars()
# Change b 2 -> -2
# a:  1
# b:  -2.000000
# c:  3

書いてある通り、未定義の変数を宣言しようとするとエラーのなる。

var my_struct.d = 2

letで宣言

struct TestStruct:
    
    let c: F32
    
    fn __init__(self&, c: F32):
        self.c = c

The biggest difference compared to a class is that all instance properties in a struct must be explicitly declared with a var or let declaration.

var or letで宣言すると書いているけど、letはまだ利用できないみたい?やり方が違うのかもしれない

みはなだみはなだ

型チェック

Pythonと違い、結構型チェックがしっかりしてる。
動的型付けの便利さはあれど、システムかするときによくバグの元になるのでcompile時にやってくれるのは良いなと思う

# OK
def return_int() -> Int:
    return 1

# NG
# error: Expression [8]:6:12: 'Bool' value cannot be converted to 'Int' in return value
def return_int() -> Int:
    return True

# OK
# 書かなければ大丈夫
# 後述
def return_int():
    return True

# NG
# 型に実装されていない演算
# error: Expression [25]:6:14: 'String' does not implement the '__lt__' method
def is_negative(a: String) -> Bool:
    return a < 0

関数の定義をdefではなくfnですると明示的に変数や戻り値の型を記載する必要がある。

# NG
# error: Expression [26]:5:14: 'fn' parameter type must be specified
fn sample_fn(a):
    return a

# 記載しない場合、暗黙的に戻り値なしとなるみたい
# error: Expression [27]:6:12: 'Int' value cannot be converted to 'None' in return value
fn sample_fn(a: Int):
    return a
みはなだみはなだ

オーバーロード

Pythonだとtyping.overloadを使う.
ただ、型ヒントのためだけの実装で、処理をかき分けることはできない。

Mojoだと引数の異なる関数を複数定義すれば良いみたい。

struct MyStruct2:
    var name: String
    
    def __init__(self&, name: String):
        self.name = name
        
    def hello(self&):
        print("Hello", self.name)
        
    def hello(self&, greeting: String):
        print(greeting, self.name)

var my_struct2 = MyStruct2("Bob")

my_struct2.hello()  # -> Hello Bob
my_struct2.hello("Good morning")  # -> Good morning Bob

Python書いてて、結構コードがややこしくなるなと思うこともあるので便利な気がする。
pandasのDataFrameの初期化関数のようにいろんな引数を受け取って処理したいときはPythonよりスッキリ描けるかもしれない
https://github.com/pandas-dev/pandas/blob/607316c9b6f5839389161441da8cff2eff5eccd0/pandas/core/frame.py#L701-L855

みはなだみはなだ

fnについて

関数定義の方法としてdeffnの二つある。

  • def: Pythonのような柔軟な関数
  • fn: 型などに厳密な関数

fnではいくつか制限がある. 
とりあえず、次の二つは便利な気がする。

  1. 引数と型ヒントが必須
  2. 引数が不変であること
# OK
def add_def(a, b):
    return a + b
print(add_def(1, 2))  # -> 3

# OK
fn add_fn(a: Int, b: Int) -> Int:
    return a + b
print(add_fn(1, 2))  # -> 3

# NG
# error: Expression [17]:9:11: 'fn' parameter type must be specified
fn add_fn(a, b):
    return a + b

# NG
# error: Expression [15]:10:5: expression must be mutable in assignment
fn add_fn_ng(a: Int, b: Int) -> Int:
    a = a + 1
    return a + b
みはなだみはなだ

__copyinit__, __moveinit__

インスタンスを別の変数に渡す時の挙動を制御できる。
例えばnumpyのarrayは参照を渡すので以下のようなことになる

check_ndarray_behavior.py
import numpy as np
a = np.array([1, 2, 3])
b = a
print(a, b) # -> [1 2 3] [1 2 3]

b[2] = -3
print(a, b)  # -> [ 1  2 -3] [ 1  2 -3]

mojoではそもそも__copyinit____moveinit__を実装しないと代入できない

from String import String

struct CopyMoveTest:
    var name: String
    var value: Int
    
    fn __init__(self&, name: String, value: Int):
        self.name = name
        self.value = value
        
    fn dump(self):
        print("Name:", self.name)
        print("value:", self.value)

var copymove_test = CopyMoveTest("temp", 1)
copymove_test.dump()

# これはNG
# error: Expression [12]:32:15: value of type 'CopyMoveTest' cannot be copied into its destination
# var xxx = copymove_test

CopyMoveTestに再代入の関数を実装していないため

さては関数にも渡せない?かと思ったら、一部そんなことあった.

def call_dump(obj: CopyMoveTest):
    obj.dump()

&をつけると参照が渡されるので問題ないみたい

# OK
def call_dump(obj&: CopyMoveTest):
    obj.dump()

# -> Name: changed
# -> value: 1

fnで定義する場合はどちらもできた。

  • &なし: インスタンス変数変更不可
  • &あり: インスタンス変数変更可能
# OK
fn call_dump(obj&: CopyMoveTest):
    obj.name = 3
    obj.dump()
    
fn call_dump(obj: CopyMoveTest):
    # obj.name = 3  #NG 
    obj.dump()

他の言語をあまりやらず、普段書くのもPythonが基本的なPython畑の人なのでここら辺になってくると少し理解が追いつかなくなってくる。