🛀

チュートリアルと学ぶMojo🔥+Pixi

に公開

Mojo🔥チュートリアルで基礎を学ぶ

https://docs.modular.com/mojo/manual/get-started/

興味がなくMojo🔥のチュートリアルを避けていたが、徐々にMojo🔥の思想が分かってきた気がしたので、改めてこれを試す。しかし、記事にある通りそのまま踏み行うのではいといとつまらないので、記事とは少し異なる方法も試してみた。

ライフゲームの様子
このようなウィンドウが出来たら、チュートリアルは完成となる

全文はこちらから:

https://github.com/amenaruya/MojoTutorial/tree/main

事前準備

今回はWSLで試したものの、その他Macや、CUIでないLinuxであれば、同じように動作するものと思われる。Mojo🔥自体はCUIでも動くが、本チュートリアルではPygameを使って画面を作るため、GUIのものを使った方がよい。

WSLは基本的にCUI環境だが、「WSL2」であればGUIにも対応している。

https://learn.microsoft.com/ja-jp/windows/wsl/tutorials/gui-apps

Nautilusというファイルマネージャーがあるので、眉唾物と思われるならば試しにこれをインストールして確認してみるとよい。

sudo apt install nautilus -y

OSレベルの準備は既に済んだものとして省略する。なお、未だMojo🔥はWinodwsに対応していない。Winodwsユーザーは三つの択からどれか自由に選ぶと良い。

準備の流れ

Pixiについて

Pixiとはパッケージ管理ツールである。

以前記事にしたこともあるが、現在、「Mojo🔥をインストールする」という手続きは存在しない

Mojo🔥はModularという「AIプラットフォーム」に属する一員とでも言うべき立ち位置にあり、プログラミング言語として独立しているわけではない。よってModularをインストールすると、そこにMojo🔥が付いてくる。

その「Modularをインストールする」する方法として、公式が推奨しているものこそPixiである。

以前の記事から引用
  • 他のパッケージ管理ツールとの比較表
Pixi Conda Pip Poetry uv 備考
Pythonのインストール 特定バージョンのPythonがインストールできることを言うか
多言語対応 Python以外の言語への対応
Lockfile対応 Lockfile:依存関係を管理するファイル
タスク実行 パッケージをビルドする作業に於けるタスクを言う
プロジェクト管理 プロジェクト単位での管理ができることを言うか
  • 情報元

https://pixi.sh/latest/#what-is-the-difference-with-pixi

次世代とか新世代とか評する声もあり、今後一定の普及も期待できるのではなかろうか。


Pixiインストール

勿論既にインストールしてあれば、新たにインストールする必要はない。

install pixi
curl -fsSL https://pixi.sh/install.sh | sh

再起動

初めてPixiをインストールした場合、新たなターミナルを開くとpixiコマンドが使えるようになる。


Pixiでプロジェクト作成

これからチュートリアルを進める上で、プロジェクトを一つ作る。プロジェクト名は、記事ではlifeとなっている。自由に決めると良い。

initialize a new project
# プロジェクト作成
pixi init life -c https://conda.modular.com/max-nightly/ -c conda-forge
# プロジェクトに移動
cd life

今後の作業は全てここで作成したプロジェクトにて行う。


Modularインストール

プロジェクトにModularをインストールする。

install modular
pixi add modular

これ以降、プロジェクトの環境下でMojo🔥が使えるようになる。


Pythonインストール

現在、Linuxには初めからPythonが搭載されていることも珍しくないが、そのバージョンは環境によって一致せず、Python標準のパッケージ管理ツールであるPipも使えないようになっている。

馴染ない人には無駄に聞こえるかもしれないが、こうした問題を回避するべく、プロジェクト専用にPythonを一つインストールする。また、チュートリアルで用いるPygamePixiでインストールできる。これらはプロジェクト内に閉じた環境で行うため、外に影響を及ぼすことはない。

install python
pixi add "python>=3.11,<3.13" "pygame>=2.6.1,<3"

タスク作成

追記:タスクの基本的な話題を記事にしてまとめている。

https://zenn.dev/amenaruya/articles/d1b350697a4f2b

茲で言うタスクとは、Pixiに於けるものである。解釈としては、プログラムを実行するに当って行うタスクというものだろう。

https://github.com/amenaruya/MojoTutorial/blob/main/pixi.toml#L12-L17

例えばmake_build_dirタスクを実行する。

run make_build_dir task
pixi run make_build_dir

make_build_dir = "mkdir -p build"とある通り、これは次と同等である。

make directory
mkdir -p build

結局のところ、これらは次のような流れを演じる。

プログラムを実行したければexecuteタスクを実行する。

build and run program
pixi run execute

生成された実行ファイルを削除したければcleanタスクを実行する。

delete build folder
pixi run clean
各タスクについて

各タスクに概説を付する。

  • make_build_dir
    buildフォルダーを作るタスク。

    make_build_dir = "mkdir -p build"
    

    プロジェクト内の様子

    単に整理整頓のため、コンパイルによって生成される実行ファイルをbuildフォルダーに配置することにしたものである。

  • build
    実行ファイルを生成するタスク。Mojo🔥は、実行ファイルを生成しないJITコンパイルと、生成するコンパイルと、二つの方法を選ぶことができる。ここではコンパイルしている。

    build = { cmd = "mojo build src/main.mojo -o build/main", inputs = ["src/main.mojo"], outputs = ["build/main"], depends-on = ["make_build_dir"] }
    

    inputsoutputsにそれぞれファイルのパスを指定しているが、ここに指定したからと言って、cmdの方には省略できるというわけではなかった。本来の意味としては「キャッシュ」である。src/main.mojoに変更が無ければ、このタスクは省略され、無駄なコンパイルが行われなくなる(ものと思われる)。

    変更がない場合
     Pixi task (build): mojo build src/main.mojo -o build/main
    Task 'build' can be skipped (cache hit) 🚀
    

    また、depends-onはタスクの依存関係を示す。ここではmake_build_dirが指定されているため、「buildタスクが実行される前には必ずmake_build_dirタスクが実行される」。

https://pixi.sh/dev/workspace/advanced_tasks/#caching

  • exe_mojo
    プログラムを実行するタスク。build/mainが生成された実行ファイルのパスであるため、単にそこを指している。

    exe_mojo = "build/main"
    
  • execute
    buildタスクとexe_mojoタスクの別名(alias)であり、二つのタスクの依存関係を定義しているに過ぎない。

    execute = [{ task = "build" }, { task = "exe_mojo" }]
    

    executeを実行することは、buildの後にexe_mojoを実行することに等しい。

https://pixi.sh/dev/workspace/advanced_tasks/#depends-on

  • clean
    buildフォルダーを削除するタスク。存在しない場合への配慮は特にしていない。

    clean = "rm -rf build"
    

これらタスクはpixi.tomlファイルに記述されている。ファイルを直接編輯することは勿論、コマンドでもタスクを作成、削除することができる。

$ pixi task add make_build_dir "mkdir -p build"
 Added task `make_build_dir`: mkdir -p build
$ pixi task alias execute build exe_mojo
@ Added alias `execute`: , depends-on = [build,exe_mojo]
$ pixi task add clean "rm -rf build"
 Added task `clean`: rm -rf build

しかしながら、inputsoutputsをコマンドで指定する方法が分からなかった。現状はコマンドに固執するのでなく、直接記述するほうが確実に思う。

本題

ここから漸うプログラミングに取り掛かる。とは言え、全てはチュートリアルの記事にて説明されている通りであるため、本記事ではその内容について詳細に触れることはない。

プログラム全容

百聞は一見に如かず、とりあえず動く様子を見た方が理解も早いだろう。なにより、事前準備の段階で躓いていた場合、この先を読んでも身が入らないだろう。

https://github.com/amenaruya/MojoTutorial/blob/main/src/main.mojo

https://github.com/amenaruya/MojoTutorial/blob/main/src/grid.mojo

ここに全文を載せたため、以降は要所のみ引用する。

概説

内容に深く触れなければ何に触れるのかと言えば、Mojo🔥の基本的な書き方である。チュートリアルよりも情報量を減らし、見通しよくした。

なお、やや程度が高い、またはPythonとの乖離が大きいと思われたものは、コラムとして分けた。単にMojo🔥の基礎を知りたいのであれば、混乱を招くため寧ろ読まない方が良い。

1. エントリーポイントと標準入出力

プログラムに於いて、実行の端を発するところを「エントリーポイント」などと呼称する場合がある。多くの場合、main()と表記された箇所がエントリーポイントに当る。

Mojo🔥と深い関わりのあるPythonは、明確にエントリーポイントを定義する必要のない言語である。しかしMojo🔥は、次のようにエントリーポイントとしてmain()を定めなければならない。

# エントリーポイント
fn main() raises:
    # 標準入力
    var name: String = input("Enter your name: ")
    # 文字列のフォーマット
    # f"Hello, {name}!" と書くことはできない
    var greeting: String = String("Hello, {}!").format(name)
    # 標準出力
    print(greeting)
    # 指定時間の待機
    # time.sleep(1) と書く時は、小数を指定していないとしてエラーになる
    time.sleep(1.0)

Python同様、input()でキーボードによる文字入力を受け取ることができ、逆にprint()で文字を表示することができる。

コラム:変数定義

変数定義の方法

Mojo🔥では、Pythonのような変数定義方法に加え、varを用いた定義もできる。こちらはdeffnほど露骨ではないが、やはりその性質に異なるところがある。

手始めに簡単な例を見せよう。

初期化せず宣言のみ

Pythonにはできない芸当として、変数宣言のみしておくことができる。とは言えプログラミング言語としては何も珍しい機能ではない。

  1> value1: Int 
  2. var value2: Float64 
  3.  
(Int) value1 = 0
(SIMD[float64, 1]) value2 = {
  (f64) [0] = 0
}
  3> value1 = 15 
  4. value2 = 4.77 
  5.  
  5> print(value1, value2)
15 4.77

値による初期化

値を代入して初期化することもできる。

  1> value1 = -0.5 
  2. value2: Int8 = 16 
  3. var value3: List[Int] = [1, 3, 7]
(SIMD[float64, 1]) value1 = {
  (f64) [0] = -0.5
}
(SIMD[int8, 1]) value2 = {
  (si8) [0] = 16
}
(List[Int]) value3 = (size 3) {
  (Int) [0] = 1
  (Int) [1] = 3
  (Int) [2] = 7
}
  4> print(value1)
-0.5
  5> print(value2)
16

  7> for i in range(len(value3)):
  8.   print(value3[i]) 
1
3
7

スコープ

普通、変数には「使える場面」と「使えない場面」がある。

  1> fn F1(): 
  2.   if True: # 必ず分岐する
  3.     var x = 0 
  4.   print("undefined: ", x) 
  5.    
[User] error: Expression [0]:4:24: use of unknown declaration 'x'
  print("undefined: ", x)
                       ^

ifブロックの中でxが定義されているが、ifブロックの外では使えるわけがない。スコープとはいわばそうした性質である。

varによる再定義

 21> fn F2(): 
 22.   var x = 0 
 23.   if True: # 必ず分岐する 
 24.     print("outer the if block: ", x)
 25.     var x = "inner the if block" 
 26.     print(x) 
 27.   print("end the if block: ", x)
[User] warning: Expression [6]:3:6: if statement with constant condition 'if True'
  if True: # 必ず分岐する


 28> F2() 
 29.  
outer the if block:  0
inner the if block
end the if block:  0
  • F2()では当初、x0を代入した。ifブロックの中でもこれは健在であることがouter the if block: 0の結果から分かる。

  • ここで、ifブロックの中に改めてxを宣言し、文字列で初期化した。そのことがinner the if blockの結果から分かる。

  • ifブロックを抜けた後、xは文字列ではなく数値になっている。ifブロックの中で定義したxは既に事切れていることが、end the if block: 0の結果から分かる。

このように、「同名の変数を定義できる」のがvarの特徴である。とはいえ、これがメリットになる場面はそう多くないだろう。

既存の変数への代入

反対にvarを使わない場合、同名の変数を定義することはできず、既存の変数に代入することとなる。多くの場合、こちらの性質を使うことになるだろう。

 30> fn F3(): 
 31.   var x = 0 # var あり
 32.   if True: 
 33.     print("before update: ", x) 
 34.     x = 1000 
 35.   print("after if block: ", x)
[User] warning: Expression [8]:3:6: if statement with constant condition 'if True'
  if True:
     ^


 36> F3() 
 37.  
before update:  0
after if block:  1000


 37> fn F3(): 
 38.   x = 0 # var なし
 39.   if True: 
 40.     print("before update: ", x) 
 41.     x = 1000 
 42.   print("after if block: ", x) 
[User] warning: Expression [10]:3:6: if statement with constant condition 'if True'
  if True:
     ^


 43> F3() 
 44.  
before update:  0
after if block:  1000
定数はない

難:不変と可変

変数には、後から異なる値を代入できる場合と、そうでない場合とがある。こうした性質をそれぞれ「可変」、「不変」と言う。

Rustでは馴染みある概念

Rustでは、原則として変数の値を後から変更することは出来ない(不変)。後から変更したい場合は、定義や参照の際にmutを付けなければならない。

Rustに於ける不変と可変
fn main() {
    let immutable_value = 0; // 不変
    let mut mutable_value = 0; // 可変
    
    println!("immutable_value: {immutable_value}");
    println!("mutable_value: {mutable_value}");
    
    mutable_value = 1; // 可変のため後から代入できる
    
    println!("変更後: {}", mutable_value);
}

immutable_value: 0
mutable_value: 0
変更後: 1

0から1に変わっていることが分かるだろう。

しかしながら、immutable_value = 1;という一文を加えると、不変のものには後から代入できないとしてエラーになる。

error[E0384]: cannot assign twice to immutable variable `immutable_value`

Mojo🔥では可変が普通

Mojo🔥に於ける変数は基本的に\overset{\small \text{mutable}}{\text{可変}}である。

https://docs.modular.com/mojo/manual/variables

簡単に解釈すると、\overset{\small \text{immutable}}{\text{不変}}な定数が使えないと言え、この特徴はPythonと似ている。

艱難:不変なパラメーターと一般的な定義

しかし厳密に言えばMojo🔥にも不変なものは存在し、Parameterがこれに該当する。

https://docs.modular.com/mojo/manual/parameters/

但しその使い方は、敢えて定数と言うべきものとは言い難い。そのままパラメーターとでも呼んでおく方がよいだろう。チュートリアルとの関連が現れるまで、その使い方を深堀しておくこととする。

引数とパラメーターとの区別

パラメーターの用例として、繰り返し回数を指定するものが挙げられている。少し誇張した例を示す。

  1> fn repeat[count: Int](msg: String): 
  2.     @parameter 
  3.     for i in range(count): 
  4.         print(msg, end='') 
  5.     print() # これが無いと表示されない
  6.      
  7>  
  8> repeat[1]('*') 
  9.  
*
  9> repeat[2]('*') 
**
 10> repeat[5]('*') 
*****
 11> repeat[10]('*') 
**********
 12> repeat[100]('*') 
****************************************************************************************************

引数自体は'*'という文字であり、これを「定められた数」だけ表示するというものに過ぎない。

但し「定められた数」を定めているのがパラメーターである。1を指定すれば1回、100を指定すれば100回表示する。

このパラメーターは、しばしば「抽象化」に用いられる。

Generics(一般的)

ジェネリック医薬品などと言うが、あれは後発医薬品という意味合いが目立っており、原義の理解に障る。generic name(一般名)というような意味が原であり、プログラミングにおける「ジェネリクス」も其方に近い意味で用いられる。

https://www.sawai.co.jp/medicine/generic/

先の例も、繰り返す回数にジェネリクスを適用したものと言える。しかし、整数型に限定したものであったため、ジェネリクスの真価を発揮したものとは言えない。

物事は一般的に考える程、厳密な定義を要する。例えばデータを「表示する」ことを考えよう。

配列や構造体を表示できない問題

基本的には、データはprint()で表示できる。しかし、中にはできないものもある。

整数と配列の比較
  1> i = 10 
  2.  
(Int) i = 10
  2> print(i)
10
  3> l = [1, 2]
(List[Int]) l = (size 2) {
  (Int) [0] = 1
  (Int) [1] = 2
}
  4> print(l)
[User] error: Expression [3]:1:6: invalid call to 'print': could not deduce parameter 'Ts' of callee 'print'
print(l)
~~~~~^~~

[User] Expression [3]:1:7: failed to infer parameter 'Ts', argument type 'List[Int]' does not conform to trait 'Writable'
print(l)
      ^

Pythonと異なり、Mojo🔥では配列を表示できない。これはC++にも見られる不便である。このような問題は、ジェネリクスを使って「定義する」ことで対応することができる。

構造体を表示できるようにする

チュートリアルとしても後述されるため、構造体を例にするのが簡単であった。

fn main():
    # 整数
    generic_print(10)
    # 小数
    generic_print(-4.888)
    # 構造体
    s1 = S1(9998, 7.255)
    generic_print(s1)
    s2 = S2("Generics", -128)
    generic_print(s2)

# 文字列に変換できるデータを表示する
fn generic_print[DataTrait: Stringable](data: DataTrait):
    print( String(data) )

@fieldwise_init
struct S1(Copyable, Movable, Stringable):
    var value1: UInt64
    var value2: Float64

    # 文字列への変換の定義
    fn __str__(self) -> String:
        var str: String = "["+String(self.value1)+", "+String(self.value2)+"]"
        return str

@fieldwise_init
struct S2(Copyable, Movable, Stringable):
    var value1: String
    var value2: Int8

    # 文字列への変換の定義
    fn __str__(self) -> String:
        var str: String = "("+self.value1+", "+String(self.value2)+")"
        return str

10
-4.888
[9998, 7.255]
(Generics, -128)

Trait(特性)と制約

注目するのはこの箇所。Stringableという語が見えるが、字面の通り、「文字列に変換できる」ということを示す。

fn generic_print[DataTrait: Stringable](data: DataTrait):
    print( String(data) )

つまり、generic_print()の引数には「文字列に変換できるもののみ受け入れる」、「文字列に変換できないものは受け入れない」という制約を与えている。

Stringableは文字列に変換できるという特性を表し、このような何らかの特性は「Trait」と呼ばれている。

更に、[DataTrait: Stringable](data: DataTrait)は、引数dataが特性Stringableを持つことを要求する制約である。このように、何らかの特性を要求するような制約は、「Trait bound」(トレイト境界)と呼ばれている。

制約を課す分には簡単でよいが、制約を課される側の構造体は、特性Stringableを満たすための定義を持たなければならない。

2. データ型

チュートリアルの記事では整数や文字列、配列(リスト)、構造体が見られる。

Pythonの場合
整数 Int int
文字列 String str
配列(リスト) List list
構造体 struct -

Pythonとは型名が若干異なることに注意。

Pythonに構造体はないのか

Pythonにはstructというモジュールが存在する。しかしこれは画像のようなバイナリーデータの扱いに優れたものであり、Mojo🔥やRustCの構造体と並べると、やや使途が異なる。

Pythonに於いては、寧ろclassが極めて近い。実際のところ、Pythonに構造体らしいものは用意されておらず、クラスとdataclassを用いた真似事が行われている。

Pythonのクラス
class C1:
    value1: int
    value2: str

    def __init__(self, value1: int, value2: str):
        self.value1 = value1
        self.value2 = value2

Mojo🔥の構造体
struct S1:
    var value1: Int
    var value2: String

    fn __init__(out self, value1: Int, value2: String):
        self.value1 = value1
        self.value2 = value2

なお、プログラムを実行するに当たり、必ずしも型を明記する必要はない。但しPythonとは異なる点として、整数型の変数に小数を代入するような真似は出来ないようになっている。

  1> i = 10 
  2.  
(Int) i = 10
  2> i = 10.5 
  3.  
[User] error: Expression [1]:1:5: cannot implicitly convert 'FloatLiteral[10.5]' value to 'Int'
i = 10.5
    ^~~~

しかし敢えて型を明らかにしたい場合、VSCodeの拡張機能があれば、マウスカーソルを合わせることで知ることができる。

型情報の知り方
rowの型はIntであることが分かる

その他のデータ型に関する詳細な情報は、チュートリアルとは別に編輯されている。

https://docs.modular.com/mojo/manual/types

コラム:Pythonにはない型の区別

詳細に区別された型

Mojo🔥に限った話ではないが、整数や小数など、概念上同一の型が複数存在することがある。

符号付き整数(signed integer)と符号なし整数(unsigned integer)

コンピューターの設計上、あらゆるデータは二進数で表現されている。数学であれば、二進数に負の記号を付して-1011_{(2)}などと表現することができたが、コンピューターに於ける二進数は電気による物理的な現象であるため、そこに「負の記号などない」。

従って、負の数の表現を諦めた符号なし整数と、一桁を符号と見なして、その一桁だけ数値の表現できる範囲を犠牲にした符号付き整数との二つが使われている。難儀な話であることよ。

8桁(bit)を1byteという単位で扱うことから、次のように桁数によって型が分類される。

符号付き整数 符号なし整数
8 Int8 UInt8
16 Int16 UInt16
32 Int32 UInt32
64 Int64 UInt64
128 Int128 UInt128
256 Int256 UInt256

扱うデータの規模によって使い分けることで、無駄を省くことができる。

小数

小数も同様に桁数によって、Float16Float32Float64と分類がある。一般に桁が多い程、小数の表現精度が高まる。

SIMD(単一命令・複数データ)

並列処理のための設計と理解すればよい。Mojo🔥の数値型の根底にはSIMDがあり、上に示したようなデータ型は、それぞれSIMDによって定義されたものの別名である。例えば、Float16SIMD[DType.float16, 1]の別名に過ぎない。

SIMDによって次のような並列処理による計算ができる。

fn main():
    var vec1 = SIMD[DType.float64, 64](
        1.2,    2.3,    3.4,    4.5,    5.6,    6.7,    7.8,    8.9,
        91.2,   87.2,   83.4,   79.5,   75.6,   71.7,   67.8,   63.9,
        12.3,   22.4,   32.5,   42.6,   52.7,   62.8,   72.9,   82.0,
        92.1,   102.2,  112.3,  122.4,  132.5,  142.6,  152.7,  162.8,
        21.3,   31.4,   41.5,   51.6,   61.7,   71.8,   81.9,   91.0,
        101.1,  111.2,  121.3,  131.4,  141.5,  151.6,  161.7,  171.8,
        23.4,   33.5,   43.6,   53.7,   63.8,   73.9,   84.0,   94.1,
        104.2,  114.3,  124.4,  134.5,  144.6,  154.7,  164.8,  174.9,
    )

    var vec2 = SIMD[DType.float64, 64](
        7.5,    -44.5,  12.3,   -23.4,  56.7,   -67.8,  89.0,   -90.1,
        11.2,   -22.3,  33.4,   -44.0,  0.5,    -1.6,   2.7,    3.8,
        4.9,    -5.0,   6.1,    -7.2,   8.3,    -9.4,   10.5,   -11.6,
        12.7,   -13.8,  14.9,   -15.0,  16.1,   -17.2,  18.3,   -19.4,
        20.5,   -21.6,  22.7,   -23.8,  24.9,   -25.0,  26.1,   -27.2,
        28.3,   -29.4,  30.5,   -31.6,  32.7,   -33.8,  34.9,   -35.0,
        36.1,   -37.2,  38.3,   -39.4,  40.5,   -41.6,  42.7,   -43.8,
        44.9,   -45.0,  46.1,   -47.2,  48.3,   -49.4,  50.5,   -51.6,
    )
    var result = vec1 * vec2
    print("Result of SIMD multiplication:")
    print(result)

Result of SIMD multiplication:
[9.0, -102.35, 41.82, -105.3, 317.52, -454.26, 694.1999999999999, -801.89, 1021.4399999999999, -1944.5600000000002, 2785.56, -3498.0, 37.8, -114.72000000000001, 183.06, 242.82, 60.27000000000001, -112.0, 198.25, -306.72, 437.4100000000001, -590.32, 765.45, -951.1999999999999, 1169.6699999999998, -1410.3600000000001, 1673.27, -1836.0, 2133.25, -2452.72, 2794.41, -3158.32, 436.65000000000003, -678.24, 942.05, -1228.0800000000002, 1536.33, -1795.0, 2137.59, -2475.2, 2861.13, -3269.2799999999997, 3699.65, -4152.240000000001, 4627.05, -5124.079999999999, 5643.329999999999, -6013.0, 844.74, -1246.2, 1669.8799999999999, -2115.78, 2583.9, -3074.2400000000002, 3586.8, -4121.58, 4678.58, -5143.5, 5734.84, -6348.400000000001, 6984.179999999999, -7642.179999999999, 8322.400000000001, -9024.84]

Listprint()で表示できなかったのに対し、こちらは配列のようにして普通に表示されていることにも注目される。

3. 関数と処理

defまたはfnによって関数を定めることができる。Python同様、戻り値の型は->で記載する。

条件分岐や繰り返しもPythonと同様に書くことができる。

一定回数の繰り返し
  1> for i in range(10): 
  2.   print(i)
0
1
2
3
4
5
6
7
8
9

  6> for i in range(3):
  7.   for j in range(4):
  8.     print(i, j) 
0 0
0 1
0 2
0 3
1 0
1 1
1 2
1 3
2 0
2 1
2 2
2 3
その他の繰り返し
配列による繰り返し
  1> for i in [1, 3, 5]: 
  2.   print(i) 
  3.    
1
3
5

  4> for i in ["a", "b", "c"]:
  5.   print(i) 
a
b
c
条件による繰り返し
  1> var i = 0 
  2. while i <= 10: 
  3.   print(i) 
  4.   i += 1 
  5.    
0
1
2
3
4
5
6
7
8
9
10
(Int) i = 11

  6> var j = 0 
  7. while True: 
  8.   if j > 10: 
  9.     break
 10.   print(j) 
 11.   j += 1 
 12.    
0
1
2
3
4
5
6
7
8
9
10
(Int) j = 11
条件分岐
  1> var a = 10 
  2. if a > 0: 
  3.   print("正") 
  4. elif a == 0: 
  5.   print("0") 
  6. else: 
  7.   print("負")

(Int) a = 10
現状Pythonにしかない処理

Pythonにはmatchcaseがある。言語によって表記に揺れがあり、switchであったり、whenであったりするが、これも分岐の一種であろう。

matchの例
match data:
    case 0:
        ...
    case 1:
        ...
    case _:
        ...

時折使いたくなるものだが、現状、Mojo🔥にこのような処理はないと見られる。

ところで、チュートリアルでは行数と列数を変数にしていたが、元の配列から算出できるので、敢えて変数としての定義を削除した。

fn main() raises:

    # var rows: Int = 8 # 不要
    # var cols: Int = 8 # 不要
    var glider: List[List[Int]] = [
        [0, 1, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0],
        [1, 1, 1, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
    ]


fn grid_str(
    # rows: Int, # 不要
    # cols: Int, # 不要
    grid: List[List[Int]]
) -> String:
    var str: String = String()

    # 配列から取得する方式に変更
    var rows: Int = len(grid)
    var cols: Int = len(grid[0])

    for row in range(rows):
        for col in range(cols):
            if grid[row][col] == 1:
                str += "*"
            else:
                str += " "
        if row != rows-1:
            str += "\n"
    return str
コラム:defとfn

関数定義の方法

この違いは真っ先に目に入るものだが、その裏にある思惑には微々として程度の高い問題が関与している。少なくとも、チュートリアルでプログラミングを学ぼうとする者までもが、わざわざ配慮を求められるようなものではない。

Mojo🔥には(何故か)関数を定義する方法に二種類ある。一方はdefであり、Pythonと同じである。他方はfnであり、これはRustなどと同じである。

「失敗し得る」プログラムへの許容

驚くことに、defで定義するかfnで定義するかによって、許容される「記述の厳格さ」が変化する。

...

なんで?

具体的には、defで定めると、或る程度の粗雑な記述は許容される。そのためか、チュートリアルの記事では、関数の定義に全てdefを使用している。

反対にfnで定めた場合、defの場合には見過ごされていた箇所を追及されるようになる。そしてそのような箇所の例は既に現れている。

fnの場合にエラーとなる箇所
var name: String = input("Enter your name: ")
var greeting: String = String("Hello, {}!").format(name)

該当するのはinputformatである。これらは「失敗し得る箇所」であり、その安全性を必ずしも保証できない。

Pythonでは、このような「例外」への対処にtryexceptを使う。Mojo🔥でもこれを使うことはできるが、関数定義にraisesと付することで茶を濁すこともできる。

fn main() raises:

https://docs.modular.com/mojo/manual/errors/#declare-a-raising-function

エラーを起こすか起こさぬか

但しこれは「エラーへの未然対処を諦めた」ことを示しており、何の対策にもなっていないことを理解する必要がある。「fnで定義された関数はエラーを起こさない」ことが前提であり、その前提を覆す必要がある場合にraisesを用いるのが本来の使い方なのであろう。

正しい使い方
fn Func(a: Int) raises:
    if a < 0:
        raise Error("aは非負")
    ...

これを踏まえると、inputformat如きにraisesは使わず、「inputに失敗した場合はどうするか」や、「formatに失敗した場合はどうするか」を全て定めることが理想的である。とは言うものの、それはチュートリアルに学ぶような者が強いられることではない。従って本記事では、全ての例外処理をraisesで放棄している。

tryecxeptで例外処理を行う例

ここでは一例として、input()について危惧される例外「EOF」への律儀な対応を示す。

input.mojo
fn main():
    var text: String
    try:
        text = input("input your name: ")
    except e:
        # Ctrl + D で EOF Error が発生する
        print("\nAn error occurred: ", e)
        # invalid call to '__add__': failed to infer implicit parameter 'value' of argument 'rhs' type 'StringLiteral'
        # print("An error occurred: " + e)
    else:
        print("hello, " + text)
    finally:
        print("exit")

実行の様子
(mojo_env) $ mojo input.mojo 
input your name: eof
hello, eof
exit
(mojo_env) $ mojo input.mojo 
input your name: # ここで「Ctrl + D」を入力する
An error occurred:  EOF
exit

関数input()の実装はGitHubから見ることができる。

https://github.com/modular/modular/blob/main/mojo/stdlib/std/io/io.mojo#L463-L488

コメントを除外すると、実質これだけの行数となる。

https://github.com/modular/modular/blob/main/mojo/stdlib/std/io/io.mojo#L486-L488

これを見る限り、例外を起こす箇所raiseが見られない。そこで見慣れない記述_fdopenが使われていることに注目する。実際、この構造体_fdopenの定義中にraiseがある。

https://github.com/modular/modular/blob/main/mojo/stdlib/std/io/io.mojo#L71-L96

この定義により、「Ctrl + D(EOF)」を入力することで例外が起こることが分かる。

より厳格な言語の場合

Rustの場合、defのような逃げ道もなければraisesという逃げ道もなく、一つ一つ全ての失敗し得る箇所に適切な記述を施す必要がある。

コマンドライン引数から数字の入力を受ける例
let n: u64 =
    env::args() // コマンドライン引数を取得
    .nth(1) // index番号1のものを指定
    .unwrap_or("0".to_string()) // index番号1の取得に失敗した場合、代わりに"0"を代入する
    .parse::<u64>() // string型の文字列をu64型の数値に変換する
    .unwrap_or(0); // 文字列から数値への変換に失敗した場合、代わりに0を代入する

ここで使われているunwrap_or()は、まさしく「失敗した場合はどうする」という対策を定めるための関数である。しかし、これに似たunwrap()という関数もよく用いられる。

unwrap()は、失敗し得る箇所に取り合えず付して置かれる関数であり、「失敗した場合はパニックを起こす」。つまりMojo🔥のraisesのように、「エラーへの未然対処を諦めた」ことを示すものである。Rustでさえそのようなものが用意されているのだから、対策を放棄している罪悪感に掣肘されることはない。

定義した関数を使うにも特別なことは必要ない。

   var glider: List[List[Int]] = [
        [0, 1, 0, 0, 0, 0, 0, 0],
        [0, 0, 1, 0, 0, 0, 0, 0],
        [1, 1, 1, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
        [0, 0, 0, 0, 0, 0, 0, 0],
    ]
    var str1: String = grid_str(glider) # 定義した grid_str()
    print()
    print("row list version:")
    print(str1)

4. ファイル分けとオブジェクト指向

プログラムを書く上で、機能や意味合いによるファイル分けは避けられない。オブジェクト指向のように複雑なプログラムは別ファイルに隔離し、単純な記述だけが残るように工夫すると、所謂モジュール化できたというような状態となる。

チュートリアルではgridv1.mojoというファイルを作っている。本記事ではより短くgrid.mojoと言うファイル名である点に注意。

オブジェクト指向によって、新たにGridというデータ型が使えるようになる。これまではList[List[Int]]型という複雑なものであったから、見た目にも分かりやすく、記述量も減った。

grid.mojo: Grid型の定義
# @fieldwise_init
struct Grid(Copyable, Movable, StringableRaising):
    var rows: Int
    var cols: Int
    var data: List[List[Int]]

    fn __init__(
        out self,
        # rows: Int,
        # cols: Int,
        data: List[List[Int]]
    ):
        self.data = data
        self.rows = len(data)
        self.cols = len(data[0])

main.mojo: Grid型で変数を定義する
from grid import Grid # grid はファイル名 grid.mojo から



    var grid1: Grid = Grid(glider)
コラム:オブジェクト指向概説

オブジェクト指向に関わる記述

オブジェクト指向の基本的な記述について触れる。

クラスと構造体、関数の関係

  • 構造体は「変数(データ)」を集めたものである。
  • 関数は「処理」を集めたものである。
  1> struct User: 
  2.     var name: String 
  3.     var age: Int 
  4.      
  5> fn show(user: User): 
  6.   print("name: ", user.name, ", age: ", user.age) 
  7.
  8> var user: User
  9. user.name = "mj" 
 10. user.age = 45 
 11. show(user) 
name:  mj , age:  45
Cの例
#include <stdio.h>

typedef struct {
    char* name; // 名前を表す文字列
    int age; // 年齢を表す数値
} User; // User型を定義

// 名前を取得する
char* get_user_name(User user) {
    return user.name;
}

// 年齢を取得する
int get_user_age(User user) {
    return user.age;
}

int main() {
    User user = {"mj", 40}; // 構造体の初期化
    char* name = get_user_name(user); // 名前取得
    int age = get_user_age(user); // 年齢取得
    printf("name: %s, age: %d\n", name, age); // 表示
    return 0;
}
name: mj, age: 40

古くはこのようにして、「データと処理が分かれていた」。しかしこれには、人間がデータと処理の対応を把握する負担がある。

そこで「関わりのあるデータと処理を一括りにした」のが、Pythonにもある「クラス」という概念である。つまり、クラスは変数と関数を有する。

  1> struct User: 
  2.     var name: String 
  3.     var age: Int 
  4.  
  5.     fn __init__(out self, name: String, age: Int): # 初期化
  6.         self.name = name 
  7.         self.age = age 
  8.  
  9.     fn show(self): # 表示
 10.         print("name: ", self.name, ", age: ", self.age) 
 11.          
 11> user = User("mj", 67) 
 12. user.show() 
 13.  
name:  mj , age:  67

Mojo🔥にクラスはないが、Mojo🔥の構造体は実質クラスと言える。他にも、クラスを廃して構造体を使った言語は、RustJuliaを始め幾つかある。

コンストラクター定義の省略

user = User("mj", 67)のように変数を初期化しているが、初期化の処理は自分で定めなければならない。クラスから生成する変数をinstanceと言う。クラスからインスタンスを生成する際、必ず初期化を実行するが、この時の処理を定めた関数をconstructorという。

Python同様、Mojo🔥ではコンストラクターを__init__()として定める。

fn __init__(out self, name: String, age: Int):
    self.name = name 
    self.age = age 

単純なクラスの場合、コンストラクターの働きは代入のみである。その数が増えた時、全て網羅して記述することは大いに手を煩わすだろう。

Mojo🔥では、@fieldwise_initを付けることで、初期化のみのコンストラクターを定めたことにしてくれる。

コンストラクターを省略したクラス
@fieldwise_init
struct User:
    var name: String
    var age: Int

    fn show(self):
        print("name: ", self.name, ", age: ", self.age)

fn main():
    var user = User("mj", 30)
    user.show()

name:  mj , age:  30

オブジェクト指向には様々な役割や効果があるが、それらに共通するのは「人が見て分かりやすい」とか、「人が見て使いやすい」とかいうものである。本記事では、最終的に

var glider: List[List[Int]] = [
    [0, 1, 0, 0, 0, 0, 0, 0],
    [0, 0, 1, 0, 0, 0, 0, 0],
    [1, 1, 1, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
]

のような定義が

var grid: Grid = Grid.random(8, 8)

の一文に削減されることとなる。扱いやすいのは明らかに後者である。

コラム:クラスメンバー関数概説

各クラスメンバー関数について

クラスに属する変数と関数をそれぞれクラスメンバー変数、クラスメンバー関数ということがある。コンストラクター以外の各クラスメンバー関数について、簡単な補足を加えておく。

__str__()

構造体Gridが継承した特性の中に、「文字列への変換ができる」特性(StringableRaising)がある。その特性を満たすため、構造体は__str__()を定義しなければならない。

ところで元grid_str()の名前であったこの関数は、「配列からなるデータを文字列に変換する」仕組みを持っていた。このため、grid_str()をそのまま__str__()に改名している。

# fn grid_str(self) -> String:
fn __str__(self) raises -> String: # StringableRaisingの継承による
    var str: String = String()

    for row in range(self.rows):
        for col in range(self.cols):
            # if self.data[row][col] == 1:
            if self[row, col] == 1: # __getitem__による
                str += "*"
            else:
                str += " "
        if row != self.rows - 1:
            str += "\n"

    return str

なお似た特性にStringableがあり、双方には分かりやすい違いがある。

Stringable StringableRaising
エラー発生 しない する

チュートリアルではStringableRaisingを使っている。defの場合は不要であるも、fn__str__()を定義する際には、エラーが発生することを示すraisesが必要となる。

__getitem__()

クラスメンバー変数は、クラスの外からは隠蔽されることが望ましいとされる。つまり、直接grid.rowsなどとアクセスされることは望ましくないのである。

このためオブジェクト指向では、クラスメンバー変数を取得するためのgetterが定められることが多い。

fn __getitem__(self, row: Int, col: Int) -> Int:
    return self.data[row][col]

__setitem__()

同様に、直接クラスメンバー変数を設定されないよう、そのためのsetterが定められる。

fn __setitem__(mut self, row: Int, col: Int, value: Int) -> None:
    self.data[row][col] = value

random()

通常Gridは、二次元配列からなるデータを渡すことで初期化する。しかしそれでは使い勝手が悪い。

# 初期化用のデータ
var glider: List[List[Int]] = [
    [0, 1, 0, 0, 0, 0, 0, 0],
    [0, 0, 1, 0, 0, 0, 0, 0],
    [1, 1, 1, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0],
]
# 初期化
var str1: String = grid_str(glider)

そこでランダムなデータを自動で作り、初期化できるようにするのがこのrandom()である。

@staticmethod
fn random(rows: Int, cols: Int) -> Self:
    random.seed()
    var data: List[List[Int]] = [] # 空のデータ

    for _ in range(rows):
        var row_data: List[Int] = [] # 空の行
        for _ in range(cols):
            row_data.append(Int(random.random_si64(0, 1))) # 行にランダムなデータを与える
        data.append(row_data) # ランダムに作られた行をデータとして追加する

    return Self(
        # rows,
        # cols,
        data
    )
コラム:静的メンバー

静的メンバーとインスタンスの有無

@staticmethodと付いているものは、Grid.random()という使い方ができる。裏を返せば、他のものはGrid.evolve()などと使うことができない。grid = Grid()のようにインスタンスを作っていなければ(初期化していなければ)使えないのである。

Grid.random()は、インスタンス無しで使えるクラスメンバーである。このようなものを静的メンバーということがある。

Grid.random()がそうであるように、静的メンバーである関数は、しばしばインスタンス化のために使われることがある。

evolve()

ライフゲームのルールに基づき、次世代の生死を決定している。ライフゲームとしては肝となる箇所だが、文法上触れるべき重要な箇所は、演算子%である。

fn evolve(self) -> Self:
    var next_generation: List[List[Int]] = List[List[Int]]()
    var row_data: List[Int]
    var row_above: Int
    var row_below: Int
    var col_left: Int
    var col_right: Int
    var num_neighbors: Int
    var new_state: Int

    for row in range(self.rows):
        row_data = List[Int]()

        row_above = (row - 1) % self.rows
        row_below = (row + 1) % self.rows

        for col in range(self.cols):
            col_left = (col - 1) % self.cols
            col_right = (col + 1) % self.cols

            num_neighbors = (
                self[row_above, col_left]
                + self[row_above, col]
                + self[row_above, col_right]
                + self[row, col_left]
                + self[row, col_right]
                + self[row_below, col_left]
                + self[row_below, col]
                + self[row_below, col_right]
            )

            new_state = 0
            if self[row, col] == 1 and (num_neighbors == 2 or num_neighbors == 3):
                new_state = 1
            elif self[row, col] == 0 and num_neighbors == 3:
                new_state = 1
            row_data.append(new_state)

        next_generation.append(row_data)

    return Self(
        # self.rows,
        # self.cols,
        next_generation
    )

多くの言語と同様、%は割り算の余りを得る演算子である。

  1> print(10 % 5)
0
  2> print(10 % 4)
2
  3> print(10 % 3)
1

この他、和は+、差は-、積は*、商は/など、演算子は常識的なものが殆どである。

https://docs.modular.com/mojo/manual/operators

5. Pythonを使う

疾うに忘れているかもしれないが、PixiPythonPygameをインストールした。愈々、これらをMojo🔥から使役する。

# pygameのインポートはここではない
from python import Python, PythonObject



fn display(
    owned grid: Grid,
    pause: Float64, # オーバーロードのため
    window_height: Int = 600,
    window_width: Int = 600,
    background_color: String = "black",
    cell_color: String = "green",
    # pause: Float64 = 0.1, # オーバーロードのため
) raises -> None:
    # pygameをインポート
    var pygame: PythonObject = Python.import_module("pygame")
    pygame.init()

    var window: PythonObject = pygame.display.set_mode(
        # Pythonの関数にはPythonの連想配列を使う必要がある
        Python.tuple(window_height, window_width)
    )


コラム:オーバーロード

同名の関数を定義することを関数のoverloadと云う。但し、名前は同じでも引数は異なるものでなければならない。

  1> fn add(x: Int, y: Int): 
  2.   print(x+y)
  3> fn add(x: Float64, y: Float64): 
  4.   print(x+y)
  5> fn add(x: String, y: String): 
  6.   print(x+y)
  7> 
  8> add(10, 15)
25
  9> add(1.5, 20.5) 
 10.  
22.0
 10> add("over", "load")
overload

オーバーロードは抽象化の手法の一つである。極端な話、引数の型を変えたらよいため、手軽な方法として馴染み深い人もあるだろう。

しかしこの方法は手軽な反面、定義の複製に近いため、対応する型を増やすとコード量が急激に増えかねない。コード量の増加を防ぎつつ、対応する型を増やしたい場合には、『コラム:変数定義』に述べたように、ジェネリクスを使うこととなる。

コラム:所有権について

難:所有権

所有権とは、変数に対する権限である。最も分かりやすいのは、「所有権を持たないものは変数の値を変更できない」というものである。同時に、「所有権を持つ者が存在する限り、その変数は生き続ける」。

所有権に関してMojo🔥ではこのような三原則があるらしい。

  • 所有者は被所有者に対して唯一つ
  • 所有者の命が尽きた時、被所有者は破棄される
  • 被所有者への参照が存在する限り、所有者は延命する

しかし所有権は敢えて振りかざすものではなく、常に存在しているものである。特別な用が無ければ、所有権を意識したプログラミングをすることなどないはずである。

但し、チュートリアルの中にも所有権に関わる記述があるため、これのみ触れておく。

argument convention

直訳すると引数規則となるこれは、引数の所有権について表示するものである。

fn display(mut grid: Grid) raises -> None:
    while True:
        print(String(grid))
        print()
        if input("Enter 'q' to quit or press <Enter> to continue: ") == "q":
            break
        grid = grid.evolve()

これは一例だが、引数にmutとあるのが分かるだろう。

引数規則は、確認した限り6つ存在した。

https://docs.modular.com/mojo/manual/values/ownership

規則 概要
read 借用
不変参照となる
mut 可変
可変参照となる
owned 一意の所有権(?)
可変コピーとなる(?)
(var) エラーになる
error: Expression [7]:1:4: expected function name
ref 不定の参照
呼び出し元が可変ならば可変
不変ならば不変
(out) 初期化の保証
関数の開始時は未初期化だが、
関数の終了までには初期化される
主にコンストラクターに付される

これらを簡単に確認する。

fn main():
    var x = 10
    var y = 20

    readF(x, y)
    print("After readF, x: ", x, ", y: ", y)
    print()

    mutF(x, y)
    print("After mutF, x: ", x, ", y: ", y)
    print()

    ownedF(x, y)
    print("After ownedF, x: ", x, ", y: ", y)
    print()


# 不変参照
fn readF(read x: Int, read y: Int):
    print("In readF:")
    print("x: ", x, ", y: ", y)
    # 変更できない
    # x += 1
    # y += 1
    refF(x)
    refF(y)

# 可変参照
fn mutF(mut x: Int, mut y: Int):
    print("In mutF:")
    print("x: ", x, ", y: ", y)
    x += 1
    y += 1
    refF(x)
    refF(y)

# 所有権譲渡
fn ownedF(owned x: Int, owned y: Int):
    print("In ownedF:")
    print("x: ", x, ", y: ", y)
    x += 1
    y += 1
    print("x: ", x, ", y: ", y)
    refF(x)
    refF(y)

# 参照
fn refF[ is_mutable: Bool, //, origin: Origin[is_mutable] ](ref [origin] z: Int):
    print("In refF:")
    print("z: ", z)
    @parameter
    if is_mutable:
        print("Mutable")
    else:
        print("Immutable")
In readF:
x:  10 , y:  20
In refF:
z:  10
Immutable
In refF:
z:  20
Immutable
After readF, x:  10 , y:  20

In mutF:
x:  10 , y:  20
In refF:
z:  11
Mutable
In refF:
z:  21
Mutable
After mutF, x:  11 , y:  21

In ownedF:
x:  11 , y:  21
x:  12 , y:  22
In refF:
z:  12
Mutable
In refF:
z:  22
Mutable
After ownedF, x:  11 , y:  21

重要な点は、「可変か不変か」と「参照か複製か」である。

  • 値を変更する必要が無いならば、始めから不変を選ぶべきである。
  • 値を変更する必要があるならば、可変を選ばなければならない。
  • 複製には時間が掛かるため、複製する必要が無いならば、参照を選ぶべきである。
  • 呼び出し元に影響を及ぼすことを防ぐためには、複製しなければならない。

import

Mojo🔥からインポートするのはPythonであり、モジュールであるpygameをインポートすることはできない。

from python import Python, PythonObject

pygameのようなモジュールは全て、Python.import_module()により、変数のようにしてインポートすることになる。また詮ずるに、Pythonに関わる変数は全てPythonObject型である。

var pygame: PythonObject = Python.import_module("pygame")

Pythonの連想配列とMojo🔥の連想配列

pygameの記述中に、Python.tuple()を使った箇所が見られる。

var window: PythonObject = pygame.display.set_mode(
    Python.tuple(window_height, window_width)
)
pygame.draw.rect(
    window,
    cell_fill_color,
    Python.tuple(x, y, width, height),
)

Mojo🔥にタプルはないからかと思いきや、Mojo🔥にもタプルは存在する。やはりと言うべきか、print()での表示はできない。

  1> t1 = Tuple[Int, String, List[Int]](12, "tuple", [3, 3, 3]) 
  2.  
(Tuple[Int, String, List[Int]]) t1 = {
  (Int) [0] = 12
  (String) [1] = "tuple"
  (List[Int]) [2] = (size 3) {
    (Int) [0] = 3
    (Int) [1] = 3
    (Int) [2] = 3
  }
}

  2> t2 = (3.3, 4.0, 1, "value")
(Tuple[SIMD[float64, 1], SIMD[float64, 1], Int, String]) t2 = {
  (SIMD[float64, 1]) [0] = {
    (f64) [0] = 3.2999999999999998
  }
  (SIMD[float64, 1]) [1] = {
    (f64) [0] = 4
  }
  (Int) [2] = 1
  (String) [3] = "value"
}

同様にPythonにもMojo🔥にも、[]で表現される「リスト」が存在する。そしてこのような二者は、概念上は同一であるもMojo🔥では明確に区別されており、かつ使い分けることができる。PythonのリストはPython.list()関数で作成し、使用する。

Python.list
  1> from python import Python 
  2.  
  2> pl1 = Python.list(1, 2, 3)
(PythonObject) pl1 = {
  (PyObjectPtr) _obj_ptr = {
    (UnsafePointer[PyObject]) unsized_obj_ptr = {
      (pointer<_stdlib::_python::__cpython::_PyObject>) address = 0x00007b18ec950a80
    }
  }
}
  3> pl2 = Python.list( 
  4.     Python.list(1, 2),
  5.     Python.list(3, 4)
  6. ) 
(PythonObject) pl2 = {
  (PyObjectPtr) _obj_ptr = {
    (UnsafePointer[PyObject]) unsized_obj_ptr = {
      (pointer<_stdlib::_python::__cpython::_PyObject>) address = 0x00007b18ec998c80
    }
  }
}
  7> print(pl1)
[1, 2, 3]
  8> print(pl2)
[[1, 2], [3, 4]]

Mojo🔥のリストはprint()で表示できないのに対し、Pythonのリストは表示できることが分かる。この他、辞書(Dict)も同様である。

二者比較

Mojo🔥の連想配列

  1> mjList = [1, 2]
(List[Int]) mjList = (size 2) {
  (Int) [0] = 1
  (Int) [1] = 2
}
  2> mjTuple = (1, 2)
(Tuple[Int, Int]) mjTuple = {
  (Int) [0] = 1
  (Int) [1] = 2
}
  3> mjSet = {1, 2}
(Set[Int]) mjSet = {
  (Dict[Int, NoneType]) _data = {
    (Int) _len = 2
    (Int) _n_entries = 2
    (_DictIndex) _index = {
      (UnsafePointer[NoneType]) data = {
        (pointer<_stdlib::_builtin::_none::_NoneType>) address = 0x0000509ebfe02030
      }
    }
    (List[Optional[DictEntry[Int, NoneType]]]) _entries = (size 0) {}
  }
}
  4> mjDict: Dict[String, Int] = {"a": 1, "b": 2} 
(Dict[String, Int]) mjDict = {
  (Int) _len = 2
  (Int) _n_entries = 2
  (_DictIndex) _index = {
    (UnsafePointer[NoneType]) data = {
      (pointer<_stdlib::_builtin::_none::_NoneType>) address = 0x0000509ebfe02020
    }
  }
  (List[Optional[DictEntry[String, Int]]]) _entries = (size 0) {}
}

これらは全てprint()で表示できない。

  5> print(mjList)
[User] error: Expression [4]:1:6: invalid call to 'print': could not deduce parameter 'Ts' of callee 'print'
print(mjList)
~~~~~^~~~~~~~

[User] Expression [4]:1:7: failed to infer parameter 'Ts', argument type 'List[Int]' does not conform to trait 'Writable'
print(mjList)
      ^~~~~~

Expression [0] wrapper:17:5: function declared here
    mjList = [1, 2]
    ^


(null)
  5> print(mjTuple)
[User] error: Expression [5]:1:6: invalid call to 'print': could not deduce parameter 'Ts' of callee 'print'
print(mjTuple)
~~~~~^~~~~~~~~

[User] Expression [5]:1:7: failed to infer parameter 'Ts', argument type 'Tuple[Int, Int]' does not conform to trait 'Writable'
print(mjTuple)
      ^~~~~~~

Expression [0] wrapper:17:5: function declared here
    mjList = [1, 2]
    ^


(null)
  5> print(mjSet)
[User] error: Expression [6]:1:6: invalid call to 'print': could not deduce parameter 'Ts' of callee 'print'
print(mjSet)
~~~~~^~~~~~~

[User] Expression [6]:1:7: failed to infer parameter 'Ts', argument type 'Set[Int]' does not conform to trait 'Writable'
print(mjSet)
      ^~~~~

Expression [0] wrapper:17:5: function declared here
    mjList = [1, 2]
    ^


(null)
  5> print(mjDict)
[User] error: Expression [7]:1:6: invalid call to 'print': could not deduce parameter 'Ts' of callee 'print'
print(mjDict)
~~~~~^~~~~~~~

[User] Expression [7]:1:7: failed to infer parameter 'Ts', argument type 'Dict[String, Int]' does not conform to trait 'Writable'
print(mjDict)
      ^~~~~~

Expression [0] wrapper:17:5: function declared here
    mjList = [1, 2]
    ^


(null)

Pythonの連想配列

Pythonには集合(Set)があるが、Mojo🔥に於けるPython構造体には、リスト、タプル、辞書はあるものの、集合はない。

https://docs.modular.com/mojo/stdlib/python/python/Python

  1> from python import Python 
  2> pyList = Python.list(1, 2) 
(PythonObject) pyList = {
  (PyObjectPtr) py_object = {
    (UnsafePointer[PyObject]) unsized_obj_ptr = {
      (pointer<_stdlib::_python::__cpython::_PyObject>) address = 0x00007bb82a004e00
    }
  }
}
  3> pyTuple = Python.tuple(1, 2) 
(PythonObject) pyTuple = {
  (PyObjectPtr) py_object = {
    (UnsafePointer[PyObject]) unsized_obj_ptr = {
      (pointer<_stdlib::_python::__cpython::_PyObject>) address = 0x00007bb82a088800
    }
  }
}
  4> pyDict = Python.dict(a = 1, b = 2)
(PythonObject) pyDict = {
  (PyObjectPtr) py_object = {
    (UnsafePointer[PyObject]) unsized_obj_ptr = {
      (pointer<_stdlib::_python::__cpython::_PyObject>) address = 0x00007bb82a030680
    }
  }
}

これらはprint()で表示できる。

  5> print(pyList)
[1, 2]
  6> print(pyTuple)
(1, 2)
  7> print(pyDict)
{'a': 1, 'b': 2}
コラム:ポインターについて

難:ポインターとアドレス

狭義にポインターとは、「変数のアドレス」を保有する変数である。このアドレスによって、「間接的に」変数の値をも保有していると考えられる。

pointer
引用:https://docs.modular.com/mojo/manual/pointers/

次のプログラムでは、dataprint()で表示していない。しかしポインターによって、dataの値を間接的に表示している。

from memory import *

fn main():
    var data = "mojo"

    var p = Pointer(to=data)
    print("Pointer: ", p[])

    var op = OwnedPointer(data)
    print("OwnedPointer: ", op[])

    var ap = ArcPointer(data)
    print("ArcPointer: ", ap[])
Pointer:  mojo
OwnedPointer:  mojo
ArcPointer:  mojo

変数には、記憶領域上に配当されたアドレスがある。

Python 3.12.11 | packaged by conda-forge | (main, Jun  4 2025, 14:45:31) [GCC 13.3.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> data = "python"
>>> data
'python'
>>> id(data)
134060789298832
>>> another = "python"
>>> id(another)
134060789298832
>>> another = "python3"
>>> id(another)
134060787848544
>>> p = id(another)
>>> p
134060787848544
>>> type(p)
<class 'int'>

ポインターはこのアドレスを保有している。

危険性

ポインターには大きく、安全なものと危険なものとがある。

  • 上のように、変数に基づきそのアドレスを保有するポインターは安全とされる。
  • 対して下のように、変数に基づかずにアドレスを直接扱うポインターは危険とされる。
from memory import *

fn main():
    var up = UnsafePointer[Int].alloc(8)
    up.init_pointee_move(20250713)

    print("today: ", up[])
    up[] -= 1
    print("yesterday: ", up[])

    var up2 = UnsafePointer[String].alloc(32)
    up2.init_pointee_move("2025-07-13")
    print("today: ", up2[])
    up2[] = "2025-07-14"
    print("tomorrow: ", up2[])
today:  20250713
yesterday:  20250712
today:  2025-07-13
tomorrow:  2025-07-14

このプログラムには数値型の変数も文字列型の変数も現れておらず、記憶領域上に指定分の範囲を割り当て(allocate)、そこに直接データを与えるようなことをしている。

unsafe pointer
引用:https://docs.modular.com/mojo/manual/pointers/unsafe-pointers

更に、何もデータを与えずに表示することもできる。

from memory import *

fn main():
    var up3 = UnsafePointer[Int].alloc(1)
    print("up3: ", up3[])

    var up4 = UnsafePointer[Float32].alloc(1)
    print("up4: ", up4[])

    var up5 = UnsafePointer[String].alloc(4)
    print("up5: ", up5[])
up3:  140728114870904
up4:  0.0
up5:

表示の結果以前に、何のデータもないところを表示すること自体が間違っている。またup3の値は実行の都度変わる。このような結果が予期せぬところで現れ、不具合となる可能性があるため、「Unsafe」と呼ばれているのである。

ポインターの分類

広義にポインターとは、「記憶領域上のアドレス」を保有する変数である。

ポインターが、既存の変数に基づいてそのアドレスを保有するとき、これをスマートポインターと言うことがある。「参照」はこれに該当し、実体の複製を回避して変数を扱うことができるため、処理を高速化する手法としてよく用いられる。

一方、単にアドレスを保有するとき、これを生(row)ポインターと言うことがある。これは「物理的な事情」を考慮する際などに使用するものであり、通常のプログラムで使う機会は少ない。

生ポインターを使う場面

https://os.phil-opp.com/ja/minimal-rust-kernel/#hua-mian-nichu-li-suru

例えば、オペレーティングシステムが画面に文字を出力する仕組みの一つに、VGAテキストモードがある。

文字を表示するためには、「VGAテキストバッファーに文字を書きこむ」。ここで、VGAテキストバッファーのアドレスは0xB8000であると、あくまでも物理的に決まっている。これを指し示すために生ポインターを用いるのである。

Pythonからインポートした関数に、引数として連想配列を渡す時、Mojo🔥の連想配列を渡すとエラーになることがある。

  2> var np = Python.import_module("numpy")
(PythonObject) np = {
  (PyObjectPtr) py_object = {
    (UnsafePointer[PyObject]) unsized_obj_ptr = {
      (pointer<_stdlib::_python::__cpython::_PyObject>) address = 0x0000736a14142d90
    }
  }
}
  3> var a1 = np.array([1, 2]) 
(PythonObject) a1 = {
  (PyObjectPtr) py_object = {
    (UnsafePointer[PyObject]) unsized_obj_ptr = {
      (pointer<_stdlib::_python::__cpython::_PyObject>) address = 0x0000736a1412a1f0
    }
  }
}
  4> var a1 = np.array([ [1, 2] ])
[User] error: Expression [4]:1:18: invalid call to '__call__': argument #1 cannot be converted from list literal to 'PythonObject'
var a1 = np.array([ [1, 2] ])
         ~~~~~~~~^~~~~~~~~~~~

  5> var a1 = np.array(Python.list( Python.list(1, 2) ))
(PythonObject) a1 = {
  (PyObjectPtr) py_object = {
    (UnsafePointer[PyObject]) unsized_obj_ptr = {
      (pointer<_stdlib::_python::__cpython::_PyObject>) address = 0x0000736a0f1d0e10
    }
  }
}

このようにPythonMojo🔥は極力混同を避け、Pythonの関数にはPythonの連想配列を使う方が好ましいと思われる。

本記事では、チュートリアルを参考に基本的なMojo🔥の文法や仕様について紹介した。当然ながら、これらは相当省略した内容に過ぎない。全く触れていない概念もある。だがこれ以上は世の諸賢に任せ、一度筆を置くこととする。

Discussion