「Understanding the Odin Programming Language」読書録

Hello, World
ビルド
Odin はパッケージ(ディレクトリ)単位でビルドする
ビルドして実行
# カレントディレクトリのパッケージをビルド&実行
odin run .
コンパイル
odin build .
ファイル指定ビルド
-file
オプション
odin build foo.odin -file
コンパイルオブション
オプション一覧
odin build -help
Odin推奨のコーディングスタイルを指定してビルド(違反したらエラー)
odin build . -vet -strict-style

変数と定数
基本
number: int // ゼロ値で初期化
number = 7
number := 42 // 型推論付き初期化
定数
コンパイル時定数のこと.
Odinでは変数と定数で型の扱いが異なる.
定数が格納可能な範囲であれば、どんな型の変数にも代入可能.
CONST_NUM :: 128 // `Untyped` Integer
CONST_FLT :: 27.32 // `Untyped` Float
number1 := CONST_NUM // int と推論されOK
float_num: f32 = CONST_NUM // f32 と推論されOK
number2: i8 = CONST_NUM // コンパイルエラー
number3: int = CONST_FLT // コンパイルエラー
4
, 3.14
のようなリテラルも Untyped
な定数.
デフォルトで int
, f64
に推論される.
型指定定数
Untyped
な挙動を抑制したいとき
DECIMAL_CONST: f32: 3.14
Untyped の動作が追加された理由
C言語では 0.5
は 64bit 浮動小数点であり、32bit として扱いたいときは 0.5f
と都度明示する必要があった.
Odin では必要になるまで型の決定を遅らせることで余計な手間を取らせないようにした.

基本文法(一部)
固定配列
// ゼロ初期化
ten_ints: [10]int
// 初期化つき定義
ten_ints := [10]int {
61, 81, 12, 41, 5, 10, 1234, 8, 4, 1,
}
無限ループ
for {
fmt.println("無限ループって怖くね")
}
While風ループ
Odin に While文はない
i := 0
for i < 10 {
fmt.println(i) // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
i += 1
}
範囲for
// 開区間 [0, 10)
for i in 0..<10 {
fmt.println(i) // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9
}
// 閉区間 [0, 10]
for i in 0..=10 {
fmt.println(i) // 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
}
固定配列の範囲for
for n in ten_ints {
fmt.println(n) // 61, 81, 12, 41, 5, 10, 1234, 8, 4, 1
}
// `#reverse` をつけると逆順になる
#reverse for n in ten_ints {
fmt.println(n) // 1, 4, 8, 1234, 10, 5, 41, 12, 81, 61
}
ラベル付き break
outer: for x in 0..<20 {
for y in 0..<20 {
if x == 5 && y == 5 {
// 外側のループを抜ける
break outer
}
}
}

構造体
// 定義
Point :: struct {
x: int,
y: int,
}
// 初期化
p1 := Point {
x = 10,
y = 20,
}
p2 := Point{30, 40}
構造体はメンバ関数を持てない
Go の構造体メソッド
func (f Foo) some_func() { ... }
のような機能は Odin にはない。
構造体のメンバに func: proc(i: int) -> int
などと書けば関数を格納できるが、有効な場面は少ないとのこと。
C言語のように構造体定義と、それを操作する関数をすべて別に書くのが Odin の基本的な作法。
構造体の埋め込み
Point :: struct {
x: int,
y: int,
}
Rectangle :: struct {
point: Point, // 埋め込み
width: int,
height: int,
}
// 初期化
r := Rect {
point = {
x = 10,
y = 10,
},
width = 100,
height = 200,
}
r.width = 200
r.point.x = 20
foo := r.point.y
usingキーワード
using
で埋め込まれた構造体名を省略可能
Person_Stats :: struct {
health: int,
age: int,
}
Person :: struct {
using stats: Person_Stats, // `stats` を省略してメンバにアクセス可能
name: string,
}
// アクセス
p := Person {
health = 20
}
p.age = 70
hs := p.health
using による型の抽象化
using
で埋め込まれた構造体型は、アップキャストしなくても関数に渡せる.
Foo :: struct {
x: int,
}
Bar :: struct {
using foo: Foo,
y: int,
}
bar := Bar{x = 10, y = 20}
print_foo :: proc(f: Foo) { fmt.println(f.x) } // Foo を受け取る関数
print_foo(bar) // OK!!

Enum (列挙型)
ほぼ Go の iota
と同じ
Computer_Type :: enum {
Laptop = 0, // value 0
Desktop, // value 1
Mainframe = 5, // value 5
Broken, // value 6
Gamer_Rig, // value 7
}
Switchで使う
Switch 文で Enum を使う場合すべての case を定義しないとコンパイルエラー.
#partial
を指定すると、網羅しなくてもコンパイルできる.
#partial switch ct {
case .Laptop: // 型推論が効くので `Computer_Type.` の記述が省略可能
fmt.println("It's a laptop")
case .Desktop:
fmt.println("It's a desktop")
}
内部の型
デフォルトで Enum には内部的に int
が使われる.
明示すれば変更可能. 他言語のライブラリのバインディングを作るときに使う.
// 内部的に u8 型をつかう
Animal_Type :: enum u8 {
Cat,
Rabbit,
}

Union 型
C言語の union
みたいなやつ.
現在保持している型と、その型の実際のデータの両方を格納する.
My_Union :: union {
f32,
int,
Person_Data,
}
Person_Data :: struct {
health: int,
age: int,
}
val: My_Union = int(12)
使用メモリ
Union内部の型は「バリアント」と呼ぶ.
Union型の使用メモリは「バリアント内の最大の型+メモリタグの8byte
」
メモリタグ: 現在どの型を保持しているかを追跡するための内部データ.
保持している型チェック
f32_val, f32_val_ok := val.(f32)
if f32_val_ok {
// f32_val is OK to use in here.
}
if 文での書き方
if f32_val, f32_val_ok := val.(f32); f32_val_ok {
// f32_val is OK to use in here.
}
値を変更したい場合
// `&` を付与すると f32_val がポインタ型となる
if f32_val, ok := &val.(f32); ok {
f32_val^ = 7 // 参照先を更新
}
Switch文で使う
Enumと若干記法が異なる.
// `in` が必要
// & を使うと case の処理内で値がミュータブルになる
switch &v in val {
case int:
v = 7
case f32:
v = 42
case Person_Data:
v.age = 7
}
Unionのゼロ値
デフォルトで nil
.
Union型の宣言時に #no_nil
を付与すると、一番最初のバリアントがゼロ値.
ゼロ値が使われるとき、内部の型の値も全てゼロ初期化される.
My_Union :: union #no_nil {
f32,
int,
}
val: My_Union // f32
C言語風の raw_union
C言語の union
にはメモリタグは存在しない.
これと同じ実装を行いたい場合の方法が #raw_union
// 構造体を使っていることに注意
My_Raw_Union :: struct #raw_union {
number: int,
struct_val: My_Struct,
}
a_raw_union: My_Raw_Union
#raw_union
は、構造体内のすべてのフィールドをメモリ上の同じ位置から開始させ、重なり合うようにすること.
ただしメモリタグ相当の処理を自分で実装する必要がある.
struct と union を組み合わせる
// 一般的なゲームエンティティ
Entity :: struct {
position: [2]f32,
texture: Texture,
variant: Entity_Variant,
}
// プレイヤー
Entity_Player :: struct {
can_jump: bool,
}
// ロケット
Entity_Rocket :: struct {
time_in_space: f32,
}
// Union定義
Entity_Variant :: union {
Entity_Player,
Entity_Rocket,
}
使用する例
// プレイヤーエンティティを定義
player_entity := Entity {
position = starting_position,
texture = player_graphics,
variant = Entity_Player {
can_jump = true,
}
}

Maybe 型
Odin 組み込みの値を持つか持たないかを表す型
内部的に1つのバリアントを持つUnion型を使って実装されている
Odinは多値を返せるので、あまり使われることはない機能。
Maybe :: union($T: typeid) {
T,
}
使い方
time: Maybe(int)
fmt.println(time) // nil
time = 5
fmt.println(time) // 5
アサーション
.?
は型推論によって型表記が省略された記法.
if time_val, time_val_ok := time.?; time_val_ok {
// Use time_val.
}
t := time.? // nil のとき実行時エラーとなる

ポインタ
C言語と異なり、 ^
を使う(Pascal由来)
宣言時 → 型名の先頭に ^
参照時 → 変数名の末尾に ^
ポインタの宣言
num: int = 42
num_ptr: ^int // ポインタ型のゼロ値は nil
num_ptr = &num // アドレスの格納
ポインタの操作
以下はC言語の *num += 1
と等価.
num_ptr^ += 1
構造体ポインタの参照時は ^
を省略してメンバの変更可能
Foo :: struct { int x }
inc_x :: proc(f: ^Foo) {
f.x += 1 // f^.x と書く必要はない
}
malloc 操作
new
関数を使う
num_p := new(32)
num_p^ = 42
free(num_p)
Foo :: struct {
x: int
}
foo := new(Foo)
foo^ = {
x = 42,
}
free(foo)

関数(手続き)
「関数」とは厳密には副作用のない処理を表すが、Odin に副作用がないことを保証する機能はないため「手続き」を表す proc
をキーワードにしている。
が、面倒なので引き続き"「関数」と呼ぶ。
基本
- Go と同じく多値を返せる
- Go と同じく named return 機能がある
- Python と同じデフォルト引数、名前付き引数の機能がある
- C++ の
[[nodiscard]]
と同じ機能がある
@require_results
divide_and_double :: proc(n: f32, d: f32) -> (f32, bool) { ... }
Odin はクロージャをサポートしない
そういう設計とのこと.
do_stuff :: proc() {
local_variable: int
print_message :: proc(msg: string) {
fmt.println(msg)
// fmt.println(local_variable) // コンパイルエラー: 外側スコープの変数をキャプチャしない
}
print_message("Hellope!")
}
引数は常に const
- 関数内で引数を更新しようとするとコンパイルエラー.
- Odin の関数引数は
16 Byte
を超えると C++ のconst &
のようなものが渡される内部動作.
- Odin の関数引数は
- ただしポインタを経由した操作はOK.
- ポインタそのもの(格納されたアドレス)は同様に
const
なので注意
- ポインタそのもの(格納されたアドレス)は同様に
divide_numbers :: proc(n, d: f32) -> f32 {
n = n / d // コンパイルエラー
return n
}
操作したければシャドーイングして関数内だけで有効な変数を定義する
divide_numbers :: proc(n, d: f32) -> f32 {
n := n // 関数内で有効な変数 `n` を定義
n = n / d
return n
}
関数のオーバーロード
proc { ... }
でまとめる.
まとめられた関数は引き続き呼び出し可能.
length :: proc {
length_float2,
length_float3,
}
length_float2 :: proc(v: [2]f32) -> f32 {
return math.sqrt(v.x*v.x + v.y*v.y)
}
length_float3 :: proc(v: [3]f32) -> f32 {
return math.sqrt(v.x*v.x + v.y*v.y + v.z*v.z)
}
init, fini 関数
Go 言語の init()
と同じ機能がある
main :: proc() {
fmt.println("Program running")
}
@init
startup :: proc() {
// 初期化処理をここに
}
@fini
shutdown :: proc() {
// 終了処理をここに
}
defer文
Go と同じ
read_file :: proc() {
f, err := os.open("my_file.txt")
if err != os.ERROR_NONE {
// handle error
}
defer os.close(f)
}

固定長配列
Swizzling糖衣構文
Swizzling と呼ばれるシェーダー言語由来の糖衣構文がある
配列の長さが4以下の場合、x
, y
, z
, w
で各要素にアクセスできる.
pos := [4]f32{1, 2, 3, 4}
fmt.println(pos.x, pos.y, pos.z, pos.w) // 1 2 3 4
// 各要素から別の配列を生成する
fmt.println(pos.xxyy, pos.xz, pos.zw) // [1, 1, 2, 2] [1, 3] [3, 4]
配列演算
APL、J言語っぽい機能がある
// 型エイリアス
Vec3 :: [3]f32
pos1 := Vec3{0, 1, 10}
pos2 := Vec3{1 ,-2, 3}
fmt.println(pos1 + pos2) // [1, -1, 13]
fmt.println(pos1 - pos2) // [-1, 3, 7]
fmt.println(pos1 * pos2) // [0, -2, 30]
fmt.println(pos1 / pos2) // [0, -0.5, 3.3333333]
fmt.println(pos1 * 10) // [0, 10, 100]
列挙型配列
列挙型の各要素に合わせた配列が作成可能
Nice_People :: enum {
Bob,
Klucke,
Tim,
}
nice_rating := [Nice_People]int {
.Bob = 5,
.Klucke = 7,
.Tim = 3,
}
fmt.println(nice_rating) // [.Bob = 5, .Klucke = 7, .Tim = 3]
bobs_niceness := nice_rating[.Bob] // 5
// 部分的な初期化
nice_rating := #partial [Nice_People]int {
.Klucke = 10,
}

動的配列
- メモリ管理が必要
- 「アロケータ」によって動的にメモリが確保される
- Odin のデフォルトは
context.allocator
- Odin のデフォルトは
基本
dyn_arr: [dynamic]int // 動的配列の定義
append(&dyn_arr, 5) // 要素を動的に追加する
cap
関数でアロケータによって確保されている要素数を確認できる
この要素数を超えて append
すると追加でメモリが確保される
メモリ解放
free
もあるが、配列には delete
しか使わない.
// 変数名をそのまま渡していることに注意
delete(dyn_arr)
delete 後も dyn_arr を再利用したい場合はゼロリセット(dyn_arr = {}
)する方が良い
配列のリセット
clear(&dyn_arr) // 確保されたメモリ容量は変えず長さだけ 0 になる
shrink(&dyn_arr)
で容量を縮小できるが、まず使う場面がないだろう.
要素の削除
順不同削除
早いが削除後の配列の要素の順序が変更されるので注意.
unordered_remove(&dyn_arr, index)
順序保持削除
要素削除後も全体の並び順が保持される.
ordered_remove(&dyn_arr, index)

メモリアロケーター
TODO...

スライス
- Python のスライス記法とほぼ同じ.
- 型は
[]Foo
のように記述. - スライスは元となった配列とメモリを共有している
- RDBMSのViewのような感じ.
- 作成元の配列から特定の範囲のメモリ参照を切り出したものがスライス
- メモリアドレスを通じてスライスの要素を変更可能
- スライスの作成元の配列も変更される
- RDBMSのViewのような感じ.
配列を扱う関数は引数の型を slice にすると再利用性が増す
固定長配列も、動的配列もスライスを作れるから.
package slice_example
import "core:fmt"
import "core:math/rand"
Foo :: struct {
x: int,
}
append_foo :: proc(foos: ^[dynamic]Foo) {
random_age := rand.int_max(12) + 2
append(foos, Foo{x = random_age})
}
print_foos :: proc(foos: []Foo) {
for foo, i in foos {
fmt.printfln("Foo[%d].x = %d", i, foo.x)
}
}
mutate_foos :: proc(foos: []Foo) {
// `&` 付与でスライスの要素を変更可能
for &foo in foos {
foo.x = rand.int_max(99)
}
}
main :: proc() {
foos: [dynamic]Foo
append_foo(&foos)
append_foo(&foos)
print_foos(foos[:])
mutate_foos(foos[:])
print_foos(foos[:])
}
スライスをDeepCopyしたいとき
import "core:slice"
numbers: [128]int
first_20 := numbers[:20]
first_20_clone := slice.clone(first_20) // clone する
スライスの動的確保
// delete しないとメモリリーク
int_slice := make([]int, 128)
スライスのリテラル
numbers := []int{1, 2, 3}
// これは以下の操作と等価
_nums := [3]int{1, 2, 3}
numbers := _nums[;]
スライスのリテラルの落とし穴
numbers: []int // グローバル変数
set_numbers :: proc() {
// tmp := [3]int{7, 42, 13}; numbers = tmp[:] と同じ
numbers = {7, 42, 13} // この {..} のリテラルは proc 内だけで有効なことに注意!
fmt.println(numbers) // [7, 42, 13]
}
main :: proc() {
set_numbers()
fmt.println(numbers) // [4296937425, 1, 6170161600] 解放されたメモリの内容を参照しているので不定な値が出力される
}

Map(連想配列)
Mapは delete
しないとメモリリークするので注意
m: map[string]int // 宣言
m["foo"] = 1
m["bar"] = 2
m["baz"] = 3
// 要素の削除
delete_key(&m, "foo")
// 要素の取得
if v, ok := m["bar"]; ok {
fmt.println(v)
}
// 実行時エラーにはならない
does_not_exist := m["foo"]
fmt.println(does_not_exist) // 0
// in, not_in
fmt.println("bar" in m) // true
fmt.println("foo" not_in m) // true
// for
for k, v in m { ... }
for k, &v in m { ... } // v を変更したいとき
// delete しないとメモリリーク
delete(m)
作成時のメモリ指定
アロケータの指定
m := make(map[string]int, context.foo_allocator)
// 初期容量の指定
m := make(map[string]int, 64, context.temp_allocator)
Mapを使って集合をつくる
Odinは集合(Set)をビルトインで持たないので、Mapを使って表現する.
set: map[string]struct{} // struct{} は匿名型を表す(無名の型)
set["foo"] = {} // 値の追加
if "foo" in set { ... }
カスタムイテレータ
package iter_example
import "core:fmt"
Foo :: struct {
x: int,
used: bool,
}
// イテレーションの状態を保存する構造体
Foo_Iterator :: struct {
index: int,
data: []Foo,
}
// イテレータの作成
make_foo_iterator :: proc(data: []Foo) -> Foo_Iterator {
return {data = data}
}
// イテレーションの実行
foo_iterator :: proc(it: ^Foo_Iterator) -> (val: Foo, idx: int, cond: bool) {
cond = it.index < len(it.data)
for ; cond; cond = it.index < len(it.data) {
// used が false の場合はスキップしループに現れない
if !it.data[it.index].used {
it.index += 1
continue
}
val = it.data[it.index]
idx = it.index
it.index += 1
break
}
return
}
main :: proc() {
// 128 個の Foo を用意する
foos := make([]Foo, 128)
foos[10] = {
x = 7,
used = true,
}
// used == true の場合のみ現れるイテレータ
it := make_foo_iterator(foos[:])
for val in foo_iterator(&it) {
fmt.println(val) // foos[10] のみ出力される
}
}

文字列
コード中に動的に構築される文字列はアロケータによってメモリが動的にメモリが割り当てられるためメモリ管理が必要.
コード中にべた書きされた文字列リテラルは data 領域に置かれる. これを delete しようとするとクラッシュするので注意.
allocated_string := strings.clone("Hellope!", context.allocator)
// 解放する必要がある
delete(allocated_string)
文字列のイテレーション
for r in "Hello" {
fmt.println(r) // r は rune 型
}
rune型
UTF-8のコードポイントを表す型.
コードポイントは 多くの場合 一文字を表す.
コードポイントは1~4バイトの範囲で、文字によって可変.
文字列のスライス
スライスはバイト単位で行うため、文字列に対して行うと文字化けすることが多い.
str := "小猫咪"
sub_str := str[1:]
fmt.println(sub_str) // ��猫咪
文字化けさせずに部分文字列を得たい場合は strings.substring_from
を使う.
str := "小猫咪"
sub_str, _ := strings.substring_from(str, 1)
fmt.println(sub_str) // 猫咪
イテレーションforでルーンのバイト数を得る方法
str := "小猫咪"
// `i` にルーンのバイト数が入る
for r, i in str {
fmt.println(i, r)
}
出力は以下
0 小
3 猫
6 咪
バイト数が分かれば、スライス操作でも部分文字列を得ることができる.
str := "小猫咪"
for r, i in str {
from_start := str[:i + utf8.rune_size(r)]
fmt.println(from_start)
}
出力は以下
小
小猫
小猫咪
書記素クラスタ
複数のコードポイントで一文字を表す文字
str := "g̈"
for r in str {
fmt.println(r)
}
g
̈
書記素クラスタを扱いたい場合は以下をつかうこと. ただしやや重い関数とのこと.
フォーマット文字列
動的に文字列を組み立てるのでメモリ確保処理が必要.
tprintf
や aprintf
, bprintf
を使う.
t
→ temp_allocator
a
→ アロケータを指定できる
b
→ スタック上のバッファを利用する
name := "taro"
age := 7
// temp_allocator
fmt.tprintf("%v is %v", name, age) // taro is 7
// アロケータ指定
fmt.aprintf("%v is %v", name, age, allocator = context.temp_allocator)
// スタック領域上のバッファを利用する
buf: [128]byte
fmt.bprintf(buf[:], "%v is %v", name, age)
ストリングビルダーによる文字列の構築
でかい文字列を扱う場合の筆者おすすめの方法とのこと.
...省略...
Windows の文字列
Windows API の文字列は UTF-16 かつヌル終端文字が必要.
Windows はこれをワイド文字列と呼び wstring
などと表記される.
Odin は UTF-8 なので UTF-8 ↔ wstring
の変換が必要になる.
import "core:sys/windows"
// UTF-8 を wstring へ
windows.utf8_to_wstring("foo bar baz")
// wstring を UTF-8 へ
title_wstr: [128]windows.WCHAR
title_len := windows.GetWindowTextW(hwnd, raw_data(&title_wstr), len(title_wstr)) // ウィンドウのタイトルを取得
title_str, err := windows.wstring_to_utf8(raw_data(&title_wstr), int(title_len))
if err == nil {
fmt.println(title_str)
}

暗黙の Context 型変数
Odin では関数呼び出し時、context
構造体が暗黙的に渡される.
base/runtime/core.odin
に以下の定義.
関数内で context
に加えた変更は、関数内でのみ有効で呼び出し元には伝播しない.
Context :: struct {
allocator: Allocator, // デフォルトのメモリアロケータ
temp_allocator: Allocator, // デフォルトのテンポラリメモリアロケータ
assertion_failure_proc: Assertion_Failure_Proc, // `assert(cond, "Error message")` が失敗したときに呼ばれる関数
logger: Logger, // デフォルトロガー "core:log"
random_generator: Random_Generator, // 乱数生成器の定義
// ユーザーが自由に利用できる拡張データへのポインタとindex
user_ptr: rawptr,
user_index: int,
// Internal use only
_internal: rawptr,
}
context.allocator
context.allocator
を変更すると、それ以降関数で使用されるデフォルトのメモリアロケータを上書きできる.
以下の場合におすすめ
- 他のアロケータを使いたいが、関数引数にアロケータを指定できるシグネチャがない
- スコープ内のすべてのコードで指定したアロケータを使いたいとき.
- 例えば
main()
の先頭で上書きするとプログラム全体で上書きできる.
- 例えば
「1」のパターンを避けるため、動的にメモリを割り当てる関数は、アロケータを受け渡せるようにするのが良い
some_func :: proc(allocator := context.allocator) -> []int {
ints := make([]int, 4096, allocator)
...
return ints
}
context.temp_allocator
についてはデフォルトで用意されているもので問題ない. ここを変更したいケースはほぼないはず.
context.assertion_failure_proc
assert失敗時に実行される関数の設定.
NOTE: 戻り値の型 -> !
はプログラムがクラッシュし継続しないことを表す.
assert_fail :: proc(prefix, msg: string, loc := #caller_location) -> ! {
fmt.printfln("assert failed. detail: %s at line: %v", msg, loc)
runtime.trap() // OSに障害発生を伝える関数
}
main :: proc() {
context.assertion_failure_proc = assert_fail
assert(false, "FAILED!!!")
}
context.logger
import "core:log"
import "core:os"
main :: proc() {
mode: int = 0
when ODIN_OS == .Linux || ODIN_OS == .Darwin {
mode = os.S_IRUSR | os.S_IWUSR | os.S_IRGRP | os.S_IROTH
}
logh, err := os.open("log.txt", (os.O_CREATE | os.O_TRUNC | os.O_RDWR), mode)
if err == os.ERROR_NONE {
os.stdout = logh // 標準出力の接続先を `log.txt` に上書きする(fmt.print 等もファイルに書き出される)
os.stderr = logh // 同様
}
// 三項演算子
logger := err == os.ERROR_NONE ? log.create_file_logger(logh) : log.create_console_logger()
// logger を上書きしたので、これ以降のログ出力が変更される
context.logger = logger
if err == os.ERROR_NONE {
log.destroy_file_logger(logger)
} else {
log.destroy_console_logger(logger)
}
}
context.{user_ptr,user_index}
foo := Foo { ... }
context.user_ptr = &foo
// 関数の呼び出し先で
context_foo := (^Foo)(context.user_ptr)
// Foo を使った処理などする...