Mojo 入門

第5章

List・Dict・Set・Optional

Python の list・dict・set にあたる List・Dict・Set と、「値がないかもしれない」を型で表す Optional の使い方を学びます。範囲外のアクセスや存在しないキーなど、うまくいかないときの動きも実際に確かめます。

この章の内容
  1. 5.1List を作る
  2. 5.2要素の型はそろっている
  3. 5.3範囲の外を読むと
  4. 5.4くり返し・並べ替え・切り出し
  5. 5.5メモリの中ではどう並ぶか
  6. 5.6Dict:キーと値の対応表
  7. 5.7Set:重複のない集まり
  8. 5.8Optional:値がないかもしれない
  9. 5.9Python との対応表
  10. 5.10まとめ

Python では、[1, 2, 3] や {"a": 1} と書くだけでリストや辞書が手に入り、何でも入れられました。Mojo にも同じ役割の型があります。ただし、Mojo の型は 要素の型がそろっている ことが前提で、「値がないかもしれない」場合の扱いも、Python とは違います。

この章では、Python プログラマーが毎日使うコレクションを Mojo ではどう書くのかを、動かしながら確かめていきます。うまくいかないとき(範囲の外を読んだとき、存在しないキーを引いたとき)に何が起きるかも、ごまかさずに見ます。

この章を終えると、次のことができるようになります。

型の仕組みそのもの(ジェネリクス)は第6章と第15章、値の渡し方と所有権は第11章で学びます。この章では、必要になった場面で最小限だけ触れます。

5.1List を作る

Mojo の List は、Python の list にいちばん近い型です。要素の型を角かっこで指定して List[Int] のように書きます。まず作って、増やして、読んでみましょう。

list_basic.mojo
def main():
    var nums: List[Int] = [10, 20, 30]
    nums.append(40)
    print(len(nums))
    print(nums[0], nums[3])
    print(nums)
Ubuntu
~$ mojo run list_basic.mojo
4
10 40
[10, 20, 30, 40]

1行ずつ見ていきます。

型の注釈を省くと別の型になる

Python の感覚で、型の注釈を付けずに書いたらどうなるでしょうか。

array_trap.mojo
def main():
    var nums = [10, 20, 30]
    nums.append(40)
    print(nums)
Ubuntu
~$ mojo run array_trap.mojo
/home/user/array_trap.mojo:3:9: error: 'Array[Int, Int(3)]' value has no attribute 'append'
    nums.append(40)
    ~~~~^
mojo: error: failed to parse the provided Mojo source module

append が見つからないというエラーになりました。メッセージの Array[Int, Int(3)] に注目してください。注釈のない [10, 20, 30] は List ではなく、長さが 3 に固定された Array になります。固定長なので、要素を増やす append はありません。

固定長の Array でも、添字での読み書きや len() は使えます。長さを変えない小さな表なら、Array のままで十分です。この章では、増やせる List を中心に説明します。

空のリストと、他の作り方

空のリストを作って、あとから append で埋めるのは定番の書き方です。決まった長さを同じ値で埋めたり、range から作ったりすることもできます。

list_create.mojo
def main():
    var squares = List[Int]()
    for i in range(5):
        squares.append(i * i)
    print(squares)

    var zeros = List[Int](length=3, fill=0)
    print(zeros)

    var counting = List(range(5))
    print(counting)
Ubuntu
~$ mojo run list_create.mojo
[0, 1, 4, 9, 16]
[0, 0, 0]
[0, 1, 2, 3, 4]

5.2要素の型はそろっている

Python の list には、整数も文字列も小数も一緒に入れられました。Mojo でやってみましょう。

mixed.mojo
def main():
    var mixed = [1, "two"]
    print(len(mixed))
Ubuntu
~$ mojo run mixed.mojo
/home/user/mixed.mojo:2:18: error: value of type 'IntLiteral[1]' is not compatible with value of type 'StringLiteral["two"]'
    var mixed = [1, "two"]
                 ^  ~~~~~
/home/user/mixed.mojo:3:15: error: use of unknown declaration 'mixed'
    print(len(mixed))
              ^~~~~
mojo: error: failed to parse the provided Mojo source module

1つめのエラーは「IntLiteral[1](整数)と StringLiteral["two"](文字列)は互換性がない」と言っています。mixed が作れなかったので、2つめのエラーで mixed が宣言されていないと言われています。これは1つの間違いから複数のエラーが出る例です(第1章で見たとおり、一番上のエラーから直します)。

List[Int] のリストに、あとから文字列を追加しようとした場合も同じです。

wrong_append.mojo
def main():
    var nums: List[Int] = [1, 2]
    nums.append("three")
    print(nums)
Ubuntu
~$ mojo run wrong_append.mojo
/home/user/wrong_append.mojo:3:17: error: invalid call to 'append': value passed to 'value' cannot be converted from 'StringLiteral["three"]' to 'Int'
    nums.append("three")
    ~~~~~~~~~~~ ^~~~~~~
Imported from /home/user/wrong_append.mojo:1:
Mojo/stdlib/std/collections/list.mojo:882:1: note: function declared here
def append(mut self, var value: T, /) where conforms_to(T, Movable)    # note - synthetic signature
^
mojo: error: failed to parse the provided Mojo source module

「append に渡した値を StringLiteral["three"] から Int に変換できない」というエラーです。エラーは実行の前、コンパイルの段階で見つかります。Python のように、動かしてみて初めて型の食い違いに気づく、ということはありません。

どうしても色々な型を混ぜたいときのための道具もありますが、本書では扱いません。まずは「1つのリストには1種類の型」と考えてください。

5.3範囲の外を読むと

存在しない添字を読んだら、何が起きるでしょうか。Python なら IndexError が出て、try で受け止められました。Mojo の動きを見てみましょう。

oob.mojo
def main():
    var nums: List[Int] = [10, 20, 30]
    var i = 7
    print("before")
    print(nums[i])
    print("after")
Ubuntu
~$ mojo run oob.mojo 2>&1 | grep -v '^#'; echo "終了コード: ${PIPESTATUS[0]}"
before
At: /home/user/oob.mojo:5:15: Assert Error: index 7 is out of bounds, valid range is 0 to 2
mojo: error: execution crashed
To get a symbolicated stack trace, compile your program using `mojo build` with debug info enabled (e.g., `-debug-level=line-tables`) and execute it separately.
終了コード: 1

2>&1 | grep -v '^#' は、長いスタックトレース(#0 0x... で始まる行)を隠すための細工です。${PIPESTATUS[0]} で mojo run 自身の終了コードを見ています。

出力を読みます。before は表示されましたが、after は表示されていません。Assert Error: index 7 is out of bounds, valid range is 0 to 2 は「添字 7 は範囲外です。有効なのは 0 から 2 です」という意味で、続けて mojo: error: execution crashed(実行がクラッシュした)と出て、プログラムが止まりました。

負の添字は使えない

Python では nums[-1] で末尾の要素を取れました。Mojo ではどうなるでしょうか。

negative.mojo
def main():
    var nums: List[Int] = [10, 20, 30]
    print(nums[-1])
Ubuntu
~$ mojo run negative.mojo
/home/user/negative.mojo:1:5: error: function instantiation failed
def main():
    ^
/home/user/negative.mojo:3:15: note: call expansion failed with parameter value(s): (..., ...)
    print(nums[-1])
              ^
Mojo/stdlib/std/collections/list.mojo:1467:9: note: function instantiation failed
Mojo/stdlib/std/collections/list.mojo:1478:9: note: constraint failed: negative indexing is not supported, use e.g. `x[len(x) - 1]`
mojo: error: failed to run the pass manager

コンパイルの段階で、「負の添字はサポートされていない。x[len(x) - 1] のように書く」と教えてくれます。末尾の要素は、メッセージのとおりに書きます。

では、添字が変数に入っているときはどうでしょう。コンパイルの時点では値が分からないので、実行してみないと分かりません。

negative_var.mojo
def main():
    var nums: List[Int] = [10, 20, 30]
    print(nums[len(nums) - 1])
    var i = -1
    print(nums[i])
Ubuntu
~$ mojo run negative_var.mojo 2>&1 | grep -v '^#'; echo "終了コード: ${PIPESTATUS[0]}"
30
At: /home/user/negative_var.mojo:5:15: Assert Error: index -1 is out of bounds, valid range is 0 to 2
mojo: error: execution crashed
To get a symbolicated stack trace, compile your program using `mojo build` with debug info enabled (e.g., `-debug-level=line-tables`) and execute it separately.
終了コード: 1

len(nums) - 1 で末尾の 30 が取れました。一方、変数 i = -1 で読むと、「添字 -1 は範囲外」というエラーでクラッシュします。Python のように「後ろから数える」意味にはなりません。

5.4くり返し・並べ替え・切り出し

for でくり返す

list_iter.mojo
def main():
    var nums: List[Int] = [1, 2, 3, 4]
    var total = 0
    for n in nums:
        total += n
    print("total:", total)

    for i in range(len(nums)):
        nums[i] *= 10
    print(nums)
Ubuntu
~$ mojo run list_iter.mojo
total: 10
[10, 20, 30, 40]

for n in nums: は Python と同じ形で、要素を順に取り出します。要素を 書き換えたい ときは、range(len(nums)) で添字を作り、nums[i] *= 10 のように添字を通して書き換えます。

ここで、for n in nums: の中で n を書き換えようとするとどうなるでしょうか。

iter_mutate.mojo
def main():
    var nums: List[Int] = [1, 2, 3]
    for n in nums:
        n += 1
    print(nums)
Ubuntu
~$ mojo run iter_mutate.mojo
/home/user/iter_mutate.mojo:4:9: error: expression must be mutable for in-place operator destination
        n += 1
        ^
mojo: error: failed to parse the provided Mojo source module

「書き換えられない式に += は使えない」というエラーになりました。ループ変数 n は、リストの中身を 読むだけ の参照です。Python のように「値を受け取って好きに書き換える」ものではありません。この「参照」の考え方は、第11章で詳しく学びます。ここでは「書き換えるなら添字を使う」と覚えておいてください。

追加・取り出し・並べ替え

list_ops.mojo
def main():
    var nums: List[Int] = [3, 1, 2]
    nums.append(5)
    nums.insert(1, 9)
    print(nums)

    var last = nums.pop()
    print(last, nums)

    sort(nums)
    print(nums)

    nums.reverse()
    print(nums)

    print(2 in nums, 7 in nums)
Ubuntu
~$ mojo run list_ops.mojo
[3, 9, 1, 2, 5]
5 [3, 9, 1, 2]
[1, 2, 3, 9]
[9, 3, 2, 1]
True False

sort は Int だけでなく、String や Float64 のリストにも使えます(比較できる型であれば並べ替えられます)。

スライスで一部を切り出す

list_slice.mojo
def main():
    var nums: List[Int] = [1, 2, 3, 4, 5]
    print(nums[:2])
    print(nums[2:])
    print(nums[::2])

    var part = List(nums[1:4])
    part.append(100)
    print(part)
    print(nums)
Ubuntu
~$ mojo run list_slice.mojo
[1, 2]
[3, 4, 5]
[1, 3, 5]
[2, 3, 4, 100]
[1, 2, 3, 4, 5]

nums[:2] や nums[::2](1つおき)のようなスライスは、Python と同じ書き方で使えます。ただし、スライスの結果は新しいリストではなく、元のリストの一部を指す Span という型です。独立したリストが欲しいときは、List(nums[1:4]) のように List(...) で包みます。part に 100 を足しても、nums が変わっていないことが出力から分かります。

リストのコピー

Python では b = a とすると、a と b は同じリストを指しました。Mojo ではどうでしょうか。

list_copy_trap.mojo
def main():
    var a: List[Int] = [1, 2]
    var b = a
    b.append(3)
    print(a, b)
Ubuntu
~$ mojo run list_copy_trap.mojo
/home/user/list_copy_trap.mojo:3:13: error: value of type 'List[Int]' cannot be implicitly copied, it does not conform to 'ImplicitlyCopyable'
    var b = a
            ^
/home/user/list_copy_trap.mojo:3:13: note: consider transferring the value with '^'
    var b = a
            ^
             ^
/home/user/list_copy_trap.mojo:3:13: note: you can copy it explicitly with '.copy()'
    var b = a
            ^
             .copy()
mojo: error: failed to parse the provided Mojo source module

List[Int] は暗黙にコピーできない(ImplicitlyCopyable ではない)というエラーです。メッセージの提案どおり、コピーしたいときは .copy() と 明示 します。

list_copy.mojo
def main():
    var a: List[Int] = [1, 2]
    var b = a.copy()
    b.append(3)
    print(a, b)
Ubuntu
~$ mojo run list_copy.mojo
[1, 2] [1, 2, 3]

b に追加しても a は変わりません。大きなリストを、うっかりコピーして遅くなる、という事故を防ぐための仕様です。「コピーか、移動か、参照か」の詳しい話は第11章で学びます。

5.5メモリの中ではどう並ぶか

Mojo が要素の型をそろえたがる理由を、図で確かめましょう。

![Python の list と Mojo の List[Int] のメモリ上の並び方](figures/ch05/list-layout.svg)

Python の list の各スロットは、別の場所にあるオブジェクトへのポインタ です。そのため、整数も文字列も小数も入れられますが、値を読むたびにポインタをたどる必要があります。

一方、Mojo の List[Int] 本体が持っているのは、(1) 要素の並びの先頭の位置、(2) 長さ、(3) 容量の3つだけです。要素の Int 自体は、1つの連続した領域に 値そのまま で並びます。実際に大きさを調べてみましょう。

sizes.mojo
from std.sys import size_of

def main():
    print(size_of[Int]())
    print(size_of[List[Int]]())
    print(size_of[Optional[Int]]())
Ubuntu
~$ mojo run sizes.mojo
8
24
16

Int は 8 バイトです。List[Int] の本体は、要素がいくつあっても 24 バイト(3つの項目が8バイトずつ)で、要素は別の連続した領域に置かれます。3つ目の Optional[Int] は、あとで出てきます。

連続して並んでいると、CPU が隣の要素を先読みしやすくなり、第20章で学ぶ SIMD(1命令で複数の値を処理する仕組み)も使いやすくなります。「型がそろっている」ことは、不便な制限であると同時に、速さの源でもあります。

5.6Dict:キーと値の対応表

Dict は Python の dict にあたります。キーと値の型を Dict[String, Int] のように2つ指定します。

dict_basic.mojo
def main() raises:
    var ages: Dict[String, Int] = {"alice": 30, "bob": 25}
    ages["carol"] = 41
    ages["bob"] = 26
    print(len(ages))
    print(ages["alice"])
    print("bob" in ages, "dave" in ages)
    print(ages)
Ubuntu
~$ mojo run dict_basic.mojo
3
30
True False
{alice: 30, bob: 26, carol: 41}

def main() raises: の raises は「この関数の中でエラーが起こるかもしれない」という印です。ages["alice"] のようなキーでの検索は、キーがなければエラーを起こす可能性があるので、これを使う関数には raises が必要になります。エラー処理の詳しい話は第9章で学びます。

存在しないキーを引くと

Python では KeyError でした。Mojo ではどうなるでしょうか。

dict_missing.mojo
def main() raises:
    var ages: Dict[String, Int] = {"alice": 30}
    print("start")
    print(ages["dave"])
    print("end")
Ubuntu
~$ mojo run dict_missing.mojo
start
stack trace was not collected. Enable stack trace collection with environment variable `MODULAR_DEBUG=stack-trace-on-error`
Unhandled exception caught during execution: DictKeyError[String]()
mojo: error: execution exited with a non-zero result: 1

今回は、範囲外アクセスと違い、クラッシュではなく エラー(例外) です。Unhandled exception caught during execution: DictKeyError[String]() が「受け止められなかった例外」を表します。start は表示され、end は表示されません。例外なので、try / except で受け止めることもできます。

dict_try.mojo
def main():
    var ages: Dict[String, Int] = {"alice": 30}
    try:
        print(ages["dave"])
    except e:
        print("error:", e)
    print("続けて実行できます")
Ubuntu
~$ mojo run dict_try.mojo
error: DictKeyError[String]()
続けて実行できます

今度は main に raises が要りません。try で受け止めているので、エラーは関数の外に出ないからです。except e: の e はエラーの値で、print でメッセージを表示できます。メッセージはキーそのものを含まず、DictKeyError[String]() という簡素なものです。

get で安全に引く

キーがないかもしれないときは、get が便利です。

dict_get.mojo
def main():
    var ages: Dict[String, Int] = {"alice": 30}
    print(ages.get("alice"))
    print(ages.get("dave"))
    print(ages.get("dave", -1))
Ubuntu
~$ mojo run dict_get.mojo
30
None
-1

get("dave") は、例外を起こさずに None を返しました。get("dave", -1) のように2つめの引数を渡すと、キーがないときにその値(既定値)を返します。get が返すものの正体は、この章の最後の Optional の節で説明します。

キー・値・組をくり返す

dict_iter.mojo
def main() raises:
    var ages: Dict[String, Int] = {"alice": 30, "bob": 25, "carol": 41}

    for key in ages:
        print("key:", key, "->", ages[key])

    for v in ages.values():
        print("value:", v)

    for item in ages.items():
        print(item.key, item.value)

    print(ages.pop("alice"))
    print(len(ages))
Ubuntu
~$ mojo run dict_iter.mojo
key: alice -> 30
key: bob -> 25
key: carol -> 41
value: 30
value: 25
value: 41
alice 30
bob 25
carol 41
30
2

5.7Set:重複のない集まり

Set は、同じ値を2度は持たない集まりです。使う前に import が必要 です。

set_basic.mojo
from std.collections import Set

def main() raises:
    var a: Set[Int] = {1, 2, 3}
    a.add(3)
    a.add(4)
    print(len(a))
    print(2 in a, 9 in a)
    a.remove(1)
    print(a)

    var b: Set[Int] = {3, 4, 5}
    print(a | b)
    print(a & b)
    print(a - b)
Ubuntu
~$ mojo run set_basic.mojo
4
True False
{2, 3, 4}
{2, 3, 4, 5}
{3, 4}
{2}

5.8Optional:値がないかもしれない

Python では、「値がない」を None で表しました。どんな変数にも None を入れられるので、関数が int か None を返す、ということが日常的に起きます。

Mojo の Int には None を入れられません。試してみましょう。

none_int.mojo
def main():
    var x: Int = None
    print(x)
Ubuntu
~$ mojo run none_int.mojo
/home/user/none_int.mojo:2:18: error: cannot implicitly convert 'None' value to 'Int'
    var x: Int = None
                 ^~~~
mojo: error: failed to parse the provided Mojo source module

「None を Int に変換できない」というエラーです。Int は必ず整数が入っている型です。それでは、「見つからなかった」を返したいときはどうするのでしょうか。「あるかもしれないし、ないかもしれない」を表す型が Optional です。Optional[Int] は、Int か None のどちらかです。

optional_basic.mojo
def find(xs: List[Int], target: Int) -> Optional[Int]:
    for i in range(len(xs)):
        if xs[i] == target:
            return i
    return None

def main():
    var xs: List[Int] = [5, 6, 7]

    var hit = find(xs, 6)
    if hit:
        print("found at", hit.value())
    else:
        print("not found")

    var miss = find(xs, 99)
    if miss:
        print("found at", miss.value())
    else:
        print("not found")

    print(hit)
    print(miss)
    print(hit.or_else(-1), miss.or_else(-1))
Ubuntu
~$ mojo run optional_basic.mojo
found at 1
not found
1
None
1 -1

先ほどの Dict.get に話を戻します。ages.get("dave") が返していたのは、実は Optional[Int] です。None が表示されたのはそのためで、if で確かめて .value() で取り出す方法がそのまま使えます。

dict_get_optional.mojo
def main():
    var ages: Dict[String, Int] = {"alice": 30}
    var found = ages.get("alice")
    if found:
        print("alice is", found.value() + 1, "next year")
    var missing = ages.get("dave")
    if not missing:
        print("dave is unknown")
Ubuntu
~$ mojo run dict_get_optional.mojo
alice is 31 next year
dave is unknown

5.9Python との対応表

やりたいことPythonMojo
リストを作るnums = [1, 2, 3]var nums: List[Int] = [1, 2, 3]
末尾に追加nums.append(4)nums.append(4)
末尾の要素nums[-1]nums[len(nums) - 1]
範囲外の添字IndexError(try で受け止められる)クラッシュ
並べ替えnums.sort()sort(nums)
コピーb = a.copy()b = a.copy()(暗黙のコピーは不可)
異なる型を混ぜるできるできない
辞書を作るd = {"a": 1}var d: Dict[String, Int] = {"a": 1}
存在しないキーKeyErrorraises を要求。実行時は DictKeyError
キーがあるか"a" in d"a" in d
安全に引くd.get("a")d.get("a")(Optional を返す)
組を取り出すfor k, v in d.items()for item in d.items()、item.key、item.value
集合{1, 2, 3}from std.collections import Set のあと var s: Set[Int] = {1, 2, 3}
値がないNone(どんな型にも)Optional[T](Optional の型だけ)

5.10まとめ

確認クイズ

  1. var nums = [10, 20, 30] と書いたあとに nums.append(40) を呼ぶと、なぜエラーになりますか?

    解答を見る

    型を注釈しないリテラルは List ではなく、長さが固定された Array になるためです。append を使いたいときは var nums: List[Int] = [10, 20, 30] と型を書きます。

  2. List[Int] の添字に -1 を使うと、末尾の要素を取れますか?

    解答を見る

    取れません。nums[-1] と直接書くとコンパイルエラーに、変数に入っている -1 だと実行時のクラッシュになります。末尾は nums[len(nums) - 1] で読みます。

  3. 辞書から存在しないキーを d["x"] で引くと、何が起きますか? 安全に引くにはどうしますか?

    解答を見る

    DictKeyError という例外になり、try / except で受け止めなければプログラムは止まります。また、この呼び出しを含む関数には raises が必要です。安全に引くなら d.get("x") を使うと、キーがないとき None が返ります。

  4. Set を使うために必要なことは何ですか?

    解答を見る

    from std.collections import Set の import です。List や Dict と違い、import しないと「Set が宣言されていない」というエラーになります。

  5. Mojo に Optional があるのはなぜですか?

    解答を見る

    Int のような普通の型には None を入れられないためです。「値がないかもしれない」ことを Optional[Int] という型で明示することで、Optional でない値は確認なしで安全に使えます。

練習問題 重複を取り除く

List[Int] の [3, 1, 3, 2, 1] から、重複を取り除いて昇順に並べた List[Int] を作り、表示してください。ヒント:いったん Set に入れてから、List に戻します。

解答例を見る

Set に add で入れ、取り出した要素を新しい List に append して、sort で並べ替えます。

mojo
from std.collections import Set

def main():
    var data: List[Int] = [3, 1, 3, 2, 1]
    var seen = Set[Int]()
    for x in data:
        seen.add(x)
    var result = List[Int]()
    for x in seen:
        result.append(x)
    sort(result)
    print(result)

実行すると [1, 2, 3] と表示されます。

練習問題 単語を数える

List[String] の ["apple", "fig", "apple", "pear", "fig", "apple"] について、各単語が何回出てくるかを Dict[String, Int] に数えて表示してください。

解答例を見る

get で現在の回数(なければ 0)を調べて 1 を足します。

mojo
def main() raises:
    var words: List[String] = ["apple", "fig", "apple", "pear", "fig", "apple"]
    var counts = Dict[String, Int]()
    for w in words:
        counts[w] = counts.get(w).or_else(0) + 1
    for item in counts.items():
        print(item.key, item.value)

実行すると、apple 3、fig 2、pear 1 の3行が表示されます。

練習問題 最初の偶数を探す

List[Int] を受け取り、最初の偶数を Optional[Int] で返す関数 first_even を書いてください。偶数がなければ None を返し、呼び出し側で [1, 3, 4, 6] と [1, 3] の両方を試してください。

解答例を見る

x % 2 == 0 で偶数かどうかを調べ、見つかったらすぐ return します。

mojo
def first_even(xs: List[Int]) -> Optional[Int]:
    for x in xs:
        if x % 2 == 0:
            return x
    return None

def main():
    var a: List[Int] = [1, 3, 4, 6]
    var b: List[Int] = [1, 3]
    print(first_even(a).or_else(-1))
    print(first_even(b).or_else(-1))

実行すると、4 と -1 が表示されます。