神話のプログラム言語 Odin エラー制御編
今回で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ファイルを作成します。
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ファイルを作成します。
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)を返すようにします。
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を使ってエラーを返すようにしています。
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ファイルを作成します。
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