🐥

神話のプログラム言語 Odin エラー制御編

2024/10/31に公開

今回で4回目のOdin言語の記事になります。
流石に基礎的な文法を4回も書いてれば、飽きてきました。(笑)

でも、Odin言語に関する日本語の記事や、書籍がないので、日本で認知されるように頑張って記載していきます。ちなみに、海外でもOdinに関する書籍はまだありません。今年の11月頃にWeb上で出版されるらしいです。

【関数の戻り値について、神からのお告げがありました】

神はおっしゃいました!!、本当の意味で、複数の戻り値を返せる言語は、GoとOdinだけじゃないかな~、他の言語は、タプルでエミュレートしてるのばっかりじゃん。
gingerBill氏の複数の戻り値の調査

a, b, c, d := 2.67, foo()   // Ans. a=2.67, b=1, c="test", d=true

foo :: proc() -> (int, string, bool) {
  return 1, "test", true
}

でも、Go言語に、こんな事、出来た?出来ないよね!、Odinでは出来るんだよ。と神はGoユーザを煽っております。

私は、Go言語は知りませんが、今のGo言語ユーザには、この記事を見て貰いたい。
Golang開発者はOdinを試すべきだ

Odinでは、複数の戻り値を利用して、戻り値の最後にエラー情報を付ける事も出来ます。
今回は、そう言ったエラー制御について、記事を書いて行こうと思っています。

エラー制御

1.複数の戻り値を返す

プロジェクト直下に「err01」ディレクトリを作成し、以下のようにmain.odinファイルを作成します。

err01/main.odin
package main

import "core:fmt"

main :: proc() {
  // 複数の戻り値を返すテスト
  a, b, c := test_rtn(1)
  fmt.println(a, b, c)

  // 複数の戻り値を返せると言う事は、複数の引数を持つ関数に直接渡せると言う事
  test_get( test_rtn(1) )
}

// --- 複数の戻り値を返す関数
test_rtn :: proc(id: int) -> (int, string, bool) {
  return id, "test", true
}

// --- 複数の引数を貰う関数
test_get :: proc(a: int, b: string, c: bool) {
  fmt.printf("test_rtn関数からの引数 int=%v string=%v bool=%v\n", a, b, c)
  return
}
$ odin run .\err01\
1 test true   <= 複数の戻り値を返す関数を単独で実行した結果
test_rtn関数からの引数 int=1 string=test bool=true  <= 連結結果

2.booleanでエラーを返す

プロジェクト直下に「err02」ディレクトリを作成し、main.odinファイルを作成します。

err02/main.odin
package main

import "core:fmt"

main :: proc() {
  a, b := 5, 0  // a=5 b=0を設定
  c := a / b    // 値0で割り算を行っているため、これ以降の処理を行わず終了
  fmt.printf("割り算: %v / %v = %v\n", a, b, c)
}

上記処理では、変数bが0であるため、0割りエラーが発生し、プログラムが中断されます。
なので、次のように0割り関数を作成し、分母が0であれば、エラー(false)を返すようにします。

err02/main.odinを修正
package main

import "core:fmt"
import "core:os"

main :: proc() {
  { // パターン1:or_elseでエラーが発生した場合にpanicにさせて終了
    a, b := 5, 0
    c := test_divied(a, b) or_else fmt.panicf("ゼロ割りエラー: %v / %v", a, b)
    fmt.printf("割り算: %v / %v = %v\n", a, b, c)
  }
  { // パターン2:戻り引数を確認し、エラー処理を行う
    a, b: f64 = 5.2, 0
    c, ok := test_divied(a, b)
    if !ok {  // ok変数がfalseの場合
      fmt.printf("ゼロ割りエラー value=%v err=%v\n", c, ok)
      os.exit(1)
    }
    fmt.printf("割り算: %v / %v = %v\n", a, b, c)
  }
}

// --- ゼロ割り関数
// Odinではエラーを返す場合、複数の戻り値の最後にエラー情報を設定する
test_divied :: proc(a, b: $T) -> (T, bool) {
  if b == 0 {   // 分母が0の場合は、falseを返す
    return 0, false
  }
  return a / b, true
}

main関数内で{}ブロック分けし、1つ目のパターンがor_elseでエラーだった場合、右項の処理を行うようにしました。2つ目のパターンは、値とエラー値の両方を受け取り、エラー処理を行います。

3.enumでエラーを返す

プロジェクト直下に「err03」ディレクトリを作成し、main.odinファイルを作成します。
下記の処理は、bool値でエラーを返すのではなく、enumを使ってエラーを返すようにしています。

err03/main.odin
package main

import "core:fmt"
import "core:os"

NumError :: enum {  // enumの1行目はnilまたは0
  None, 
  Zero_Divide,
}

main :: proc() {
  { // パターン1:or_elseでエラーが発生した場合にpanicにさせて終了
    a, b: f64 = 5.0, 0
    // 引数が2個以上あれば or_else はnilまたはboolなら問題ない
    num := test_divied2(a, b) or_else -99.9  // エラーの場合、num変数に-99.9を設定
    fmt.printf("ok2 num=%v\n", num)
  }
  { // パターン2:戻り引数を確認し、エラー処理を行う
    a, b: f64 = 5.0, 0
    num, err := test_divied2(a, b)  // boolじゃないからor_elseのifは使えない
    if err != .None { // enum値は「.値」と言うように表す
      fmt.println("2 error")
      os.exit(1)
    }
    fmt.printf("ok1 num=%v\n", num)
  }
  { // パターン3:パターン2の簡略版
    a, b: f64 = 5.0, 0
    if num, err := test_divied2(a, b); err != .None {  // boolなら!errが使える
      fmt.printf("ok3 num=%v\n", num)
    }
  }
}

// --- ゼロ割りテスト
// unionでエラーを返す
test_divied2 :: proc(a, b: $T) -> (T, NumError) {
  if b == 0 {
    fmt.println("b = 0")
    return 0, .Zero_Divide  // enum値は.Zero_Divideで表す
  }
  return a / b, .None       // enum値は.Noneで表す
}

4.unionでエラーを返す

プロジェクト直下に「err04」ディレクトリを作成し、main.odinファイルを作成します。

err04/main.odin
package main

import "core:fmt"
import "core:os"
import "core:io"
import "core:strings"
import "core:mem"

MySystemError :: union #shared_nil {  // このプログラム全体のエラー情報
  ReadError,              // ファイル読み込み時のエラー情報
  CopyError,              // ファイルコピー時のエラー情報
  mem.Allocator_Error,    // アロケート時のエラー情報(未使用)
  io.Error,               // ファイル制御時のエラー情報(未使用)
}

ReadError :: struct {     // ファイル読み込み時のエラーを格納する構造体
  filename: string,
  msg: string,
  error: os.Errno,
}

CopyError :: struct {     // ファイルコピー時のエラーを格納する構造体
  path: string,
  filename: string,
  error: os.Errno,
}

main :: proc() {
  data, ok := read_file("test.txt")      // ファイル読み込み関数をデータを取り出す
  if ok != nil {
    fmt.printf("error all [%v]\n", ok)   // 一括で出力する場合と個々に出す場合で分けて出力
    fmt.printf("error [%v] [%v] [%v]\n", ok.(ReadError).filename, ok.(ReadError).msg, ok.(ReadError).error)
    os.exit(1)
  }

  ok = copy_file("my_copy", "my.txt", data)  // to: my_copy/my.txtファイルにデータをコピー
  if ok != nil {
    fmt.printf("error [%v]\n", ok)
    os.exit(1)
  }
  return
}

// --- ファイル読み込み処理
read_file :: proc(filename: string) -> (lines: []byte, error: MySystemError) {
  data, ok := os.read_entire_file(filename) // 小さいファイルなら、これで一括抽出
  if !ok {
    errno := os.Errno(os.get_last_error())  // 最後に処理したエラーを得る
    return nil, ReadError{ filename, "ファイルがありません!", errno }
  }
  defer delete(data)

  return data, nil
}

// --- コピー先に書き込み処理
copy_file :: proc(to_path, to_filename: string, data: []byte) -> (error: MySystemError) {

  // ディレクトリ作成
  if ok := os.exists(to_path); !ok {          // 存在チェックをして、無ければディレクトリ作成
    if ok := os.make_directory(to_path); ok != nil {
      errno := os.Errno(os.get_last_error())  // 最後に処理したエラーを得る
      return CopyError{ to_path, to_filename, errno }
    }
  }

  // ファイル作成
  filename := strings.concatenate({to_path, "/", to_filename})
  defer delete(filename)
  if ok := os.exists(filename); !ok {         // 存在チェックをして、無ければファイルに書き込み
    if ok := os.write_entire_file(filename, data); !ok {
      errno := os.Errno(os.get_last_error())  // 最後に処理したエラーを得る
      return CopyError{ to_path, to_filename, errno }
    }
  }

  return nil
}

通常、Odin言語では、Unionでエラー情報をまとめ、その中にStructやEnumによる、個々のモジュール毎のエラー情報(構造体)を格納する形になっています。上記プログラムではMySystemError共用体で、エラー情報をまとめて管理しています。(本来はファイルの読み書きは1つでも良いとは思いますが、サンプル用として分けただけです。)
※ファイルの読み書きにはread_entire_fileとwrite_entire_file関数を使っていますが、本来、大きいファイルを扱う場合は、open、バッファストリーム、read/writeで読み書きすべきだと思います。

おわりに

Odinにはエラー型がありません。Enum、Union、Structによってエラー制御を行うため、他の言語に較べてはるかにわかりやすいと思います。次回は、Odinで外部ライブラリRaylibの記事を書いていきます。本来は、Raylibの記事が書きたくて、Odinの基礎文法を書いてたのですが、やっと本来の目的が書けそうです。
※まだ、Threadやlog、SOAの説明もしないといけないですが、おいおい書いて行きます。

Discussion