The V Programming Language の 公式ドキュメント の和訳だよ
情報はすべて↑から
コンパイルが速いこととC/C++との相互変換がウリの模様 (C/C++ 勢なので注目せざるを得ない
2019 年 4 月にアーリーアクセス、同年 6 月 22 日にオープンソースで公開済み
すでに V 自身で V コンパイラは作成されている
もともと GUI アプリ用につくられたので
クロスプラットフォームな GUI ライブラリとかもあったりする
私も和訳で閲覧数を稼ぐコスいことしてみる
ただドキュメントが完全ではないのでまだ把握しきれてない
ちょくちょく更新してるのでストックしとくといいかも
以下から訳文
V ドキュメント
導入
V は保守しやすいソフトウェアを構築するために設計された静的型付け言語です。
Go にとてもよく似通っており、Oberon、Rust、Swift、Kotlin、Python にも影響されています。
V は非常にシンプルな言語です。1 時間ほどかけてこのドキュメントを読めば、言語の全貌がほどよくつかめるでしょう。
この言語は最小限の抽象化でシンプルかつ明瞭なコード記述を促します。
シンプルですが、開発者にとって大きな力になります。他の言語でできることなら、V もできますよ。
ソースからのインストール
最新で最高な V を入手する主な方法は、ソースからのインストールです。これは簡単で、通常はたった数秒で完了します。
git clone https://github.com/vlang/v
cd v
make
# ヒント: Windows をお使いですか?: cmd.exe シェルで make.bat を実行してみましょう
詳細は README の V のインストール の節をご覧ください。
V を最新バージョンへアップグレードする
V が既にマシン上にインストールされているのなら、V の組み込み自己アップデーターを用いて最新バージョンへアップグレードできます。アップグレードするには v up
コマンドを実行してください。
頒布用に V をパッケージ化する
V のパッケージの準備手順についてのノート をご覧ください。
はじめに
ターミナルで以下のコマンドを使用することで、V にプロジェクトの最小構成を自動セットアップさせることができます。
-
v init
→ 現在のフォルダにファイルを追加して V プロジェクトを作成します -
v new abc
→ 新規フォルダabc
内に新規プロジェクトを作成します。これはデフォルトで「hello world」のプロジェクトです。 -
v new abcd web
→ 新規フォルダabc
内に新規プロジェクトを作成します。このとき vweb テンプレートを使用します。
Hello World
fn main() {
println('hello world')
}
このスニペットを hello.v
ファイルに保存して、v run hello.v
を実行してみましょう。
こちら で説明している通り、ここでは V を
v symlink
でシンボリックリンクしていることを前提にしています。
まだ設定していない場合、V へのパスは手動でタイプする必要があります。
おめでとうございます——初めての V プログラムを書いて、実行しました!
v hello.v
で実行せずにプログラムをコンパイルできます。サポートしている全コマンドは v help
でご覧ください。
上記の例から、関数は fn
キーワードで宣言されることが分かるでしょう。戻り値型は関数名の後ろで指定されます。今回の main
は何も返さないので、戻り値型はありません。
他の言語 (C、Go、Rust など) と同じで、main
はエントリーポイントです。
println
はいくつかある 組み込み関数 の一つです。これは標準出力へ値を出力します。
fn main()
の宣言は単一ファイルプログラム内では省略できます。これは小さいプログラムである「スクリプト」を書くときや、言語について学ぶだけのときに役立ちます。簡潔にするため、このチュートリアルでは fn main()
は省略されます。
すなわち "hello world" プログラムは V だとここまでシンプルにできます。
println('hello world')
fn main() {}
を明示的に使用しない場合、すべての宣言はあらゆる変数の代入または最上位の関数呼び出しより前に登場する必要があります。なぜなら V は、最初の代入関数呼び出し以降を暗黙のメイン関数の一部として考慮するからです。
複数ファイルのプロジェクトフォルダを実行
あるフォルダにいくつかの .v ファイルがあり、そのうちの 1 つには main()
関数が、他のファイルにはヘルパー関数があるとします。それらは課題別に整理されているかもしれませんが、まだ 再利用可能な個別のモジュールになるほどの構造ではなく、それらすべてを 1 つのプログラムにコンパイルしたいとします。
他の言語では、インクルードやビルドシステムを使ってすべてのファイルを列挙し、それらを別々にオブジェクトファイルにコンパイルしてから、単一実行ファイルにリンクする必要があります。
しかし V では、.v
ファイルのフォルダ全体をまとめてコンパイルし実行でき、v run .
だけで済みます。パラメータを渡すこともできるので、v run . --yourparam some_other_stuff
のようにすることができます。
上記は、まずファイルを単一プログラム (フォルダ/プロジェクトにちなんだ名前) にコンパイルし、CLI 引数として --yourparam some_other_stuff
を渡してプログラムを実行します。
プログラムからは、以下のように CLI 引数を利用できます。
import os
println(os.args)
実行後、V は生成した実行ファイルを削除します。実行ファイルを残しておきたい場合は、代わりに v -keepc run .
を使うか、v .
で手動コンパイルしてください。
V コンパイラのフラグはすべて run
コマンドより前で渡してください。ソースファイル/フォルダより後にあるものはそのままプログラムに渡され、V は処理しません。
コメント
// これは単行コメント。
/*
これは複行コメント。
/* ネストできます。 */
*/
関数
fn main() {
println(add(77, 33))
println(sub(100, 50))
}
fn add(x int, y int) int {
return x + y
}
fn sub(x, y int) int {
return x - y
}
更に、型は引数名の後に来ます。
Go や C のように、関数はオーバーロードできません。これはコードが単純になり保守性と可読性が向上します。
巻き上げ
関数はその宣言より前で使用できます。add
と sub
を main
の後に宣言しても、main
から呼び出すことができます。これは V のすべての宣言に当てはまり、ヘッダーファイルの必要性やファイルの順序と宣言について考慮する必要がなくなります。
複数の値を返す
fn foo() (int, int) {
return 2, 3
}
a, b := foo()
println(a) // 2
println(b) // 3
c, _ := foo() // `_` で値を無視します
シンボルの可視性
pub fn public_function() {
}
fn private_function() {
}
関数は、デフォルトで非公開 (エクスポートしないよう) になっています。他のモジュールから使用できるようにするには、前に pub
を付けます。これは定数や型定義でも同じです。
pub
は名前付きモジュールでのみ使用できます。詳しくは モジュール をご参照ください。
変数
name := 'Bob'
age := 20
large_number := i64(9999999999)
println(name)
println(age)
println(large_number)
変数は :=
で宣言と初期化が行われます。これは V で変数を宣言する唯一の手段です。つまり、変数は必ず初期値があるということです。
変数の型は右側の値から推論されます。他の型に強制するには、型変換を使います。T(v)
と書くと値 v
を型 T
に変換します。
他の多くの言語とは異なり、V は関数内でしか変数の定義を許可しません。グローバル (モジュールレベルな) 変数は使用できません。V に大域な状態はないのです (詳細は 既定で純粋関数 を参照)。
他のコードベースとの一貫性のため、すべての変数と関数の名前は snake_case
スタイルを使ってください。型名には PascalCase
を使ってください。
可変の変数
mut age := 20
println(age)
age = 21
println(age)
変数の値を変更するには =
を使用します。V での変数はデフォルトでイミュータブルです。変数の値を変えられるようにするには、mut
をつけて宣言する必要があります。
最初の行の mut
を消してコンパイルしてみると分かるでしょう。
初期化と代入の違い
:=
と =
の (大事な) 違いに注意してください。 :=
は宣言と初期化に使いますが、=
は代入に使います。
fn main() {
age = 21
}
上のコードはコンパイルできません。変数 age
は宣言されていないからです。V ではすべての変数は宣言されている必要があります。
fn main() {
age := 21
}
複数の変数の値を一行で変更できます。以下のように、一時変数を介さずに値を入れ替えできます。
mut a := 0
mut b := 1
println('${a}, ${b}') // 0, 1
a, b = b, a
println('${a}, ${b}') // 1, 0
宣言時のエラー
開発モードでのこのコードは "未使用の変数" の警告を出します。実動モード (v -prod foo.v
のように v に -prod
を渡すと有効化) では、Go と同じくコンパイルできません。
fn main() {
a := 10
if true {
a := 20 // エラー: 変数の再定義
}
// 警告: 未使用の変数 `a`
}
多くの言語と異なり、変数のシャドーイングは許可されていません。親スコープですでに使用されている名前の変数を宣言するとコンパイルエラーになります。
V の型
プリミティブ型
bool
string
i8 i16 int i64 i128 (そのうち)
u8 u16 u32 u64 u128 (そのうち)
rune // Unicode コードポイントに相当
f32 f64
isize, usize // プラットフォーム依存の、メモリ上の位置を表すのに必要なサイズ
voidptr // C との相互運用に用いられる
any // C の void* や Go の interface{} のようなもの
C や Go と異なり、int
は常に 32 ビット整数になります。
V では全演算子の両側が同じ型の値でなければならないという規則がありますが、これには例外があります。
一方の小さいプリミティブ型が他方の型のデータ範囲に完全に収まる場合、自動的に拡大変換できます。
以下にこの変換が可能な経路を示します。
i8 → i16 → int → i64
↘ ↘
f32 → f64
↗ ↗
u8 → u16 → u32 → u64 ⬎
↘ ↘ ↘ ptr
i8 → i16 → int → i64 ⬏
例えば、int
の値は f64
や i64
へ自動的に拡大変換できますが、u32
にはできません (u32
では負値の符号が失われてしまいます)。しかし、int
から f32
への拡大変換は現在自動的に行われます (しかし、大きな値では精度が低下する可能性があります)。
123
や 4.56
のようなリテラルは特別な方法で扱われます。これらは型の拡大変換にはつながりませんが、型の決定が必要な場合はデフォルトでそれぞれ int
と f64
に設定されます。
u := u16(12)
v := 13 + u // v の型は `u16` - 拡大変換なし
x := f32(45.6)
y := x + 3.14 // y の型は `f32` - 拡大変換なし
a := 75 // a の型は `int` - int リテラルなのでデフォルト通り
b := 14.7 // b の型は `f64` - float リテラルなのでデフォルト通り
c := u + a // c の型は `int` - `u` の値を自動的に拡大変換
d := b + x // d の型は `f64` - `x` の値を自動的に拡大変換
文字列
name := 'Bob'
assert name.len == 3 // 3 を返します
assert name[0] == u8(66) // 添字アクセスはバイトとして、u8(66) == `B` を与えます
assert name[1..3] == 'ob' // スライスは文字列 'ob' を与えます
// エスケープコード
windows_newline := '\r\n' // エスケープ特殊文字は C のようにします
assert windows_newline.len == 2
// 任意バイトは `\x##` 記法で直接指定でき、`#` は
// 16 進数の一桁です。
aardvark_str := '\x61ardvark'
assert aardvark_str == 'aardvark'
assert '\xc0'[0] == u8(0xc0)
// また `\###` 記法では、`#` を 8 進数の一桁とした 8 進エスケープができます。
aardvark_str2 := '\141ardvark'
assert aardvark_str2 == 'aardvark'
// Unicode は `\u####` で直接指定でき、この `#` 16 進数の一桁です。
// そしてこれは内部で UTF-8 表現へと変換されます。
star_str := '\u2605' // ★
assert star_str == '★'
assert star_str == '\xe2\x98\x85' // UTF-8 はこのように指定することもできます。
V では、文字列は読み取り専用のバイト配列です。文字列データは UTF-8 エンコードされます。
s := 'hello 🌎' // 絵文字が 4 バイト使う
assert s.len == 10
arr := s.bytes() // `string` を `[]u8` へ変換
assert arr.len == 10
s2 := arr.bytestr() // `[]u8` を `string` へ変換
assert s2 == s
文字列の値はイミュータブルであり, 以下のように要素を変更できません。
mut s := 'hello 🌎'
s[0] = `H` // 不可能
error: cannot assign to
s[i]
since V strings are immutable
注意として, 文字列にインデックスでアクセスすると, rune
や string
ではなく u8
(バイト) が得られます。インデックスは Unicode コードポイントではなく文字列内の バイト に対応します。u8
を string
に変換したい場合は, byte
の ascii_str()
メソッドを使用して以下のようにしてください。
country := 'Netherlands'
println(country[0]) // 出力: 78
println(country[0].ascii_str()) // 出力: N
文字列を表すのに, シングルクォートとダブルクォートの両方が使用できます。一貫性のため, vfmt
は文字列内にシングルクォートが含まれているかに関わらず, ダブルクォートをシングルクォートへ変換します。
生文字列は、r
を前に付けます。生文字列はエスケープされません。
s := r'hello\nworld'
println(s) // "hello\nworld"
文字列は以下のように簡単に整数へ変換できます。
s := '42'
n := s.int() // 42
// すべての整数リテラルをサポートしています
assert '0xc3'.int() == 195
assert '0o10'.int() == 8
assert '0b1111_0000_1010'.int() == 3850
assert '-0b1111_0000_1010'.int() == -3850
より発展的な文字列の処理と変換については、vlib/strconv モジュールをご参照ください。
文字列内挿
基本的な文字列内挿は, 以下のように変数名の前に ${
を付け, 後ろに }
を付けるだけの非常にシンプルな構文です。変数は文字列へ変換され, リテラルへ埋め込まれます。
name := 'Bob'
println('Hello, ${name}!') // Hello, Bob!
これは 'age = ${user.age}' のようなフィールドでも動作します。必要であれば, 'can register = ${user.age > 13}'
のように複雑な式を用いることもできます。
C の printf()
のようなフォーマット指定子もサポートしています。f
、g
、x
、o
、b
などが出力フォーマットで指定できます。コンパイラがストレージのサイズを考慮するため、hd
や llu
はありません。
フォーマット指定子を使う場合は、以下のパターンに従ってください。
${変数名:[フラグ][幅][.精度][型]}
- フラグ: 次のものを 0 個以上指定します。
-
はフィールドの出力の左寄せ、0
はデフォルトの空白
文字の代わりに0
を使用します。
:::note
V はフォーマットのフラグに'
や#
をサポートしていません。また、右寄せとして+
をサポートしていますが、デフォルトで右寄せなので不要です。
::: - 幅: 出力するフィールドの最小幅を表す整数値です。
- 精度:
.
の後ろに整数値を続けると、浮動小数点数を出力するときの小数点以下の有効桁数になります。変数が整数の場合は無視されます。 - 型:
f
とF
は入力の浮動小数点数をそのまま小数として描画させたいとき、e
とE
は入力の浮動小数点数を指数表記で描画させたいとき (部分的に動作しません)、g
とG
は入力の浮動小数点数を小さい値では浮動小数点数表記にして大きい値では指数表記に描画させたいとき、d
は入力の整数を 10 進の桁で描画させたいとき、x
とX
は入力の整数を 16 進の桁で描画させたいとき、0
は入力の整数を 8 進の桁で描画させたいとき、b
は入力の整数を 2 進の桁で描画させたいとき、s
は文字列を要求します (大抵は不要)。
数値型が描画される時、16 進数の文字列や infinity
のような特殊な値によってアルファベットが出力されることもあります。この場合でも、小文字の型では小文字になり大文字の型では大文字になります。
大抵の場合では、フォーマットの型を空のままにしておくのが最善です。浮動小数点数はデフォルトで g
として描画され、整数はデフォルトで d
として描画され、s
は常に冗長となります。型指定が推奨されるのは以下の 3 つの状況だけです。
- フォーマット文字列がコンパイル時にパースされるので、指定した型によってエラー検出を助ける場合
- フォーマット文字列は 16 進数や
e
の指数表記ではデフォルトで小文字表記なので、大文字の 16 進数や大文字のE
の指数表記を使用するように強制するとき - フォーマット文字列で簡便に整数から 16, 2, 8 進文字列を得るとき
さらなる情報は フォーマット指定子の仕様 をご覧ください。
x := 123.4567
println('[${x:.2}]') // 小数点以下第二位で丸める => [123.46]
println('[${x:10}]') // 左に空白の右寄せ => [ 123.457]
println('[${int(x):-10}]') // 右に空白の左寄せ => [123 ]
println('[${int(x):010}]') // 左を 0 埋め => [0000000123]
println('[${int(x):b}]') // 2 進数として出力 => [1111011]
println('[${int(x):o}]') // 8 進数として出力 => [173]
println('[${int(x):X}]') // 大文字の 16 進数として出力 => [7B]
println('[${10.0000:.2}]') // 末尾の無効な 0 を取り除く => [10]
println('[${10.0000:.2f}]') // 数に影響しない末尾の 0 でも表示する => [10.00]
文字列の演算子
name := 'Bob'
bobby := name + 'by' // 文字列結合には + を使います
println(bobby) // "Bobby"
mut s := 'hello '
s += 'world' // 文字列の追加は `+=` です
println(s) // "hello world"
V のすべての演算子は、両辺が同じ型の値でなければなりません。以下のように、整数を文字列に連結することはできません。
age := 10
println('age = ' + age) // 不可能
error: infix expr: cannot use
int
(right expression) asstring
以下のように age
を文字列に変換する必要があります。
age := 11
println('age = ' + age.str())
以下のような文字列内挿でもできます (こちらが好ましい)。
age := 12
println('age = ${age}')
string のすべてのメソッドや関連するモジュールの strings, strconv もご参照ください。
Rune
rune
は Unicode 文字を表し、u32
のエイリアスです。Rune はこのように作成できます。
rocket := `🚀`
rune
は .str()
メソッドを使用して UTF-8 文字列へ変換できます。
rocket := `🚀`
assert rocket.str() == '🚀'
rune
は .bytes()
メソッドを使用して UTF-8 バイト列へ変換できます。
rocket := `🚀`
assert rocket.bytes() == [u8(0xf0), 0x9f, 0x9a, 0x80]
16 進、Unicode、8 進エスケープシーケンスは以下のように rune
リテラルでも動作します。
assert `\x61` == `a`
assert `\141` == `a`
assert `\u0061` == `a`
// マルチバイトリテラルも同じく動作します
assert `\u2605` == `★`
assert `\u2605`.bytes() == [u8(0xe2), 0x98, 0x85]
assert `\xe2\x98\x85`.bytes() == [u8(0xe2), 0x98, 0x85]
assert `\342\230\205`.bytes() == [u8(0xe2), 0x98, 0x85]
注意として rune
リテラルは文字列と同じエスケープ構文を使用しますが、こちらは Unicode 文字 1 つのみしか保持できません。そのため、コード内で Unicode 文字 1 つを指定しなかった場合はコンパイル時にエラーとなります。
また、文字列は以下のようにインデックスでアクセスできますが、rune ではできません。
rocket_string := '🚀'
assert rocket_string[0] != `🚀`
assert 'aloha!'[0] == `a`
文字列は .runes()
メソッドで rune の列へ変換できます。
hello := 'Hello World 👋'
hello_runes := hello.runes() // [`H`, `e`, `l`, `l`, `o`, ` `, `W`, `o`, `r`, `l`, `d`, ` `, `👋`]
数値
a := 123
これは a
に 123 の値を代入しています。既定で a
は int
になります。
16 進数、2 進数, 8 進数の整数リテラルも使用できます。
a := 0x7B
b := 0b01111011
c := 0o173
これらはすべて同じ 123 の値を代入しています。型を指定していないため、すべて int
型になります。
V では _
を桁区切りに数を書くこともできます。
num := 1_000_000 // 1000000 と同じ
three := 0b0_11 // 0b11 と同じ
float_num := 3_122.55 // 3122.55 と同じ
hexa := 0xF_F // 255 と同じ
oct := 0o17_3 // 0o173 と同じ
異なる型の整数にしたい場合は、キャストを使うことでできます。
a := i64(123)
b := u8(42)
c := i16(12345)
浮動小数点数の代入も同様に動作します。
f := 1.0
f1 := f64(3.14)
f2 := f32(3.14)
この型を明示しない場合、浮動小数点数リテラルは既定で f64
型になります。
浮動小数点数リテラルは以下のように 10 のべき乗で宣言することもできます。
f0 := 42e1 // 420
f1 := 123e-2 // 1.23
f2 := 456e+2 // 45600
配列
配列は、同じ型のデータ要素の集まりです。角括弧で囲まれた要素のリストで表されます。要素にアクセスするには、配列変数に角括弧で囲んだ添字(0 始まり)を付ける必要があります。
mut nums := [1, 2, 3]
println(nums) // `[1, 2, 3]`
println(nums[0]) // `1`
println(nums[1]) // `2`
nums[1] = 5
println(nums) // `[1, 5, 3]`
プッシュ演算子 <<
を用いて、要素を配列の末尾に追加できます。これは配列全体を追加することもできます。
mut nums := [1, 2, 3]
nums << 4
println(nums) // "[1, 2, 3, 4]"
// 配列を追加する
nums << [5, 6, 7]
println(nums) // "[1, 2, 3, 4, 5, 6, 7]"
mut names := ['John']
names << 'Peter'
names << 'Sam'
// names << 10 <-- これはコンパイルされません。`names` は文字列の配列です。
val in array
は配列に val
が含まれていると true を返します。[in
演算子](#in 演算子) をご覧ください。
names := ['John', 'Peter', 'Sam']
println('Alex' in names) // "false"
配列のフィールド
配列にはその「サイズ」を制御する以下 2 つのプロパティがあります。
-
len
: length - 配列内で事前に確保して初期化された要素の数 -
cap
: capacity - 要素のために確保しているが、未初期化で要素としてカウントされていないメモリ空間の量。配列は、再確保せずにこのサイズまで伸長できます。通常、V はこのプロパティを自動処理しますが、ユーザによる手動の最適化 (以下 を参照) をする場合もあります 。
mut nums := [1, 2, 3]
println(nums.len) // "3"
println(nums.cap) // "3" 以上
nums = [] // 配列が空になります
println(nums.len) // "0"
data
は voidptr
型のフィールドで、最初の要素のアドレスです。これはローレベルの unsafe
コード向けとなります。
これらのプロパティは読み取り専用のフィールドでありユーザは変更できません。
配列の初期化
基本的な初期化の構文は上記の通りです。配列の型は、その最初の要素で決まります。
-
[1, 2, 3]
は int の配列 ([]int
)。 -
['a', 'b']
は string の配列 ([]string
)。
ユーザは、[u8(16), 32, 64, 128]
のように最初の要素の型を明示的に指定できます。V の配列はすべての要素が同じ型でなければなりません。すなわち、[1, 'a']
のようなコードはコンパイルできません。
上記の構文はよくある要素数が少ない配列では問題ありませんが、非常に大きな配列や空の配列のために 2 つ目の初期化構文があります。
mut a := []int{len: 10000, cap: 30000, init: 3}
これは 10000 個の int
要素の配列が作成され、すべてが 3
で初期化されます。メモリスペースは 30000 要素ぶん確保されます。パラメータの len
、cap
、init
は任意です。len
のデフォルトは 0
で、init
は要素型のデフォルトの初期値になります (数値型は 0
、string
は ''
、など)。ランタイムシステムは、容量が len
より小さくならないようにします (より小さい値が明示的に指定されていても)。
arr := []int{len: 5, init: -1}
// `arr == [-1, -1, -1, -1, -1]`, arr.cap == 5
// 空の配列の宣言:
users := []int{}
容量を指定することで再確保の回数が減り、挿入時のパフォーマンスが向上します。
mut numbers := []int{ cap: 1000 }
println(numbers.len) // 0
// この要素の追加では再確保しない
for i in 0 .. 1000 {
numbers << i
}
上記のコードは 範囲 for
文を使用しています。
配列の初期化は、その要素のインデックスを与える index
変数へアクセスすることで以下に示すように初期化できます。
count := []int{len: 4, init: index}
assert count == [0, 1, 2, 3]
mut square := []int{len: 6, init: index * index}
// square == [0, 1, 4, 9, 16, 25]
配列の型
配列は以下の型にできます。
型 | 定義例 |
---|---|
数値 |
[]int []i64
|
文字列 | []string |
Rune | []rune |
真偽値 | []bool |
配列 | [][]int |
構造体 | []MyStructName |
チャンネル | []chan f64 |
関数 |
[]MyFunctionType []fn (int) bool
|
Interface | []MyInterfaceName |
直和型 | []MySumTypeName |
ジェネリック型 | []T |
辞書配列 | []map[string]f64 |
列挙体 | []MyEnumType |
エイリアス | []MyAliasTypeName |
スレッド | []thread int |
参照 | []&f64 |
共有 | []shared MyStructType |
コード例
この例では、構造体 と 直和型 を使用して、さまざまな種類のデータ要素 (点、線など)を処理できる配列を作成しています。
struct Point {
x int
y int
}
struct Line {
p1 Point
p2 Point
}
type ObjectSumType = Line | Point
mut object_list := []ObjectSumType{}
object_list << Point{1, 1}
object_list << Line{
p1: Point{3, 3}
p2: Point{4, 4}
}
dump(object_list)
/*
object_list: [ObjectSumType(Point{
x: 1
y: 1
}), ObjectSumType(Line{
p1: Point{
x: 3
y: 3
}
p2: Point{
x: 4
y: 4
}
})]
*/
多次元配列
配列は 1 次元よりも多くできます。
二次元配列の例:
mut a := [][]int{len:2, init: []int{len:3}}
a[0][1] = 2
println(a) // [[0, 2, 0], [0, 0, 0]]
三次元配列の例:
mut a := [][][]int{len:2, init: [][]int{len:3, init: []int{len:2}}}
a[0][1][1] = 2
println(a) // [[[0, 0], [0, 2], [0, 0]], [[0, 0], [0, 0], [0, 0]]]
配列のメソッド
すべての配列は println(arr)
で簡単に出力でき、s := arr.str()
で文字列に変換できます。
配列からのデータのコピーには .clone()
を使います。
nums := [1, 2, 3]
nums_copy := nums.clone()
配列は以下のように .filter()
と .map()
メソッドで効果的にフィルター、マップできます。
nums := [1, 2, 3, 4, 5, 6]
even := nums.filter(it % 2 == 0)
println(even) // [2, 4, 6]
// filter には無名関数が使える
even_fn := nums.filter(fn (x int) bool {
return x % 2 == 0
})
println(even_fn)
words := ['hello', 'world']
upper := words.map(it.to_upper())
println(upper) // ['HELLO', 'WORLD']
// map にも無名関数が使える
upper_fn := words.map(fn (w string) string {
return w.to_upper()
})
println(upper_fn) // ['HELLO', 'WORLD']
it
は filter
/ map
メソッド内で現在処理している要素を表す組み込み変数です。
さらに .any()
や .all()
を使えば、要素が条件を満たすかどうか簡単にテストできます。
nums := [1, 2, 3]
println(nums.any(it == 2)) // true
println(nums.all(it >= 2)) // false
配列には他にも以下のような組み込みメソッドがあります。
-
b := a.repeat(n)
n
個ぶんのa
の要素を結合する -
a.insert(i, val)
新しい要素val
をインデックスi
に挿入して以降のすべての要素を後ろに動かす -
a.insert(i, [3, 4, 5])
複数の要素を挿入する -
a.prepend(val)
値を最初に挿入する、a.insert(0, val)
と等価 -
a.prepend(arr)
配列arr
の要素を最初に挿入する -
a.trim(new_len)
長さを切り詰める (new_length < a.len
である場合のみ、さもなくば何もしない) -
a.clear()
配列を空にする (cap
は変わらない、a.trim(0)
と等価) -
a.delete_many(start, size)
インデックスstart
から始まる連続したsize
個の要素を削除する - 再確保を起こす -
a.delete(index)
a.delete_many(index, 1)
と等価 -
v := a.first()
v := a[0]
と等価 -
v := a.last()
v := a[a.len - 1]
と等価 -
v := a.pop()
最後の要素を取得して配列から取り除く -
a.delete_last()
配列から最後の要素を取り除く -
b := a.reverse()
b
がa
の要素の順番を逆にしたものを格納する -
a.reverse_in_place()
a
の要素の順番を逆にする -
a.join(joiner)
joiner
文字列を区切りにして、文字列の配列を文字列へと連結する。
vlib/arrays もご覧ください。
配列のソート
大抵の配列のソートは非常にシンプルで直感的です。特殊変数 a
と b
でソートの条件をカスタマイズできます。
mut numbers := [1, 3, 2]
numbers.sort() // 1, 2, 3
numbers.sort(a > b) // 3, 2, 1
struct User {
age int
name string
}
mut users := [User{21, 'Bob'}, User{20, 'Zarkon'}, User{25, 'Alice'}]
users.sort(a.age < b.age) // User.age int フィールドでソート
users.sort(a.name > b.name) // User.name string フィールドで逆順にソート
V は、配列メソッド sort_with_compare
によるカスタムソートもサポートしています。これにはソート順を定義する比較関数が必要です。カスタムのソートルールで複数のフィールドを同時にソートするのに便利です。以下のコードでは、配列を名前の昇順、年齢の降順でソートしています。
struct User {
age int
name string
}
mut users := [User{21, 'Bob'}, User{65, 'Bob'}, User{25, 'Alice'}]
custom_sort_fn := fn (a &User, b &User) int {
// a が b より前なら -1 を返す
// 療法が同じ順番なら 0 を返す
// b が a より前なら 1 を返す
if a.name == b.name {
if a.age < b.age {
return 1
}
if a.age > b.age {
return -1
}
return 0
}
if a.name < b.name {
return -1
} else if a.name > b.name {
return 1
}
return 0
}
users.sort_with_compare(custom_sort_fn)
配列のスライス
スライスは部分的な配列です。スライスは ..
演算子で区切られた2つのインデックスの間にあるすべての要素を表します。右辺のインデックスは左辺のインデックス以上でなければなりません。
右辺のインデックスがない場合は配列の長さになります。左辺のインデックスが存在しない場合は 0 になります。
nums := [0, 10, 20, 30, 40]
println(nums[1..4]) // [10, 20, 30]
println(nums[..4]) // [0, 10, 20, 30]
println(nums[1..]) // [10, 20, 30, 40]
V のスライスはそれ自体が配列でもあります (違う型にはなりません)。すべての配列操作はスライスに対しても実行できます。すなわち、スライスは同じ型の配列に追加できます。
array_1 := [3, 5, 4, 7, 6]
mut array_2 := [0, 1]
array_2 << array_1[..3]
println(array_2) // [0, 1, 3, 5, 4]
スライスは、元の配列の容量や長さに関わらず常に最小の容量 cap == len
(上記の cap
を参照)で作成されます。その結果、サイズが大きくなったときはすぐに再確保され、別のメモリ位置にコピーされるので、親配列から独立したものになります(増加時にコピー)。特に、スライスに要素をプッシュしても親配列は変更されません。
mut a := [0, 1, 2, 3, 4, 5]
mut b := a[2..4]
b[0] = 7 // `b[0]` は `a[2]` を参照しています
println(a) // `[0, 1, 7, 3, 4, 5]`
b << 9
// `b` は再確保され `a` から独立するようになります
println(a) // `[0, 1, 7, 3, 4, 5]` - 変更なし
println(b) // `[7, 3, 9]`
親の配列へ追加することで子のスライスから独立することもあれば、しないこともあります。この挙動は親の容量に依存しており、予測可能です。
mut a := []int{len: 5, cap: 6, init: 2}
mut b := unsafe { a[1..4] }
a << 3
// 再確保なし - `cap` に収まります
b[2] = 13 // `a[3]` が変更されます
a << 4
// `a` は再確保され `b` から独立するようになります (`cap` が増加します)
b[1] = 3 // `a` は変化しません
println(a) // `[2, 2, 2, 13, 2, 3, 4]`
println(b) // `[2, 3, 13]`
独立したコピーが必要であれば、スライスで .clone()
を呼び出すとよいでしょう。
mut a := [0, 1, 2, 3, 4, 5]
mut b := a[2..4].clone()
b[0] = 7 // 注意: `b[0]` は `a[2]` を参照していません。.clone() しなければ参照するようになります
println(a) // [0, 1, 2, 3, 4, 5]
println(b) // [7, 3]
負のインデックスによるスライス
V は負のインデックスによる配列と文字列のスライスをサポートしています。負のインデックスでは配列の末尾から始まり、始端に向かって進みます。例えば -3
は array.len - 3
に等しいです。負のスライスは通常のスライスと異なる構文になっており、これは配列名と角括弧の間に gate
を a#[..-3]
のように追加する必要があります。この gate
はスライスの別種であることを示し、配列内でそのスライス結果を「ロック」するように記憶します。返されたスライスは常に有効な配列であり、空になることもあります。
a := [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
println(a#[-3..]) // [7, 8, 9]
println(a#[-20..]) // [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
println(a#[-20..-8]) // [0, 1]
println(a#[..-3]) // [0, 1, 2, 3, 4, 5, 6]
// 空の配列
println(a#[-20..-10]) // []
println(a#[20..10]) // []
println(a#[20..30]) // []
配列のメソッドチェーン
.filter()
や .map()
のような配列のメソッド呼び出しは、チェーンでき、it
組み込み変数を用いて典型的な map/filter
関数型パラダイムを得られます。
// filter、map、負の配列スライスの利用
files := ['pippo.jpg', '01.bmp', '_v.txt', 'img_02.jpg', 'img_01.JPG']
filtered := files.filter(it#[-4..].to_lower() == '.jpg').map(it.to_upper())
// ['PIPPO.JPG', 'IMG_02.JPG', 'IMG_01.JPG']
固定長配列
V は固定長配列もサポートしています。通常の配列とは異なり、その長さは一定です。配列に要素を追加したり縮小したりできず、その要素を変更することしかできません。
しかし、固定長配列の要素へのアクセスは効率的で、通常の配列よりも少ないメモリしか必要としません。
ほとんどのメソッドは通常の配列で動作するように定義されており、固定長配列では動作しません。固定長配列をスライスすることで通常の配列に変換できます。
mut fnums := [3]int{} // fnums は 3 要素の固定長配列です。
fnums[0] = 1
fnums[1] = 10
fnums[2] = 100
println(fnums) // => [1, 10, 100]
println(typeof(fnums).name) // => [3]int
fnums2 := [1, 10, 100]! // 同じことをする短縮初期化構文 (構文は変わる可能性あり)
anums := fnums[0..fnums.len]
println(anums) // => [1, 10, 100]
println(typeof(anums).name) // => []int
注意として、スライスする際に固定長配列のデータが新たに作成した通常の配列へコピーされます。
辞書配列
mut m := map[string]int // `string` をキーに `int` を格納する辞書配列
m['one'] = 1
m['two'] = 2
println(m['one']) // "1"
println(m['bad_key']) // "0"
println('bad_key' in m) // `in` を使ってそのようなキーが存在するかどうか検出する
m.delete('two')
辞書配列のキーは任意の型を保持でき、以下の場合は int
にしています。
以下の短縮構文で辞書配列全体を初期化することもできます。
numbers := {
1: 'one'
2: 'two'
}
println(numbers)
指定のキーが見つからなかった場合、デフォルトでゼロ値が返されます。
sm := {
'abc': 'xyz'
}
val := sm['bad_key']
println(val) // ''
intm := {
1: 1234
2: 5678
}
s := intm[3]
println(s) // 0
また、or {}
ブロックを使用することで存在しなかったキーのハンドリングも可能です。
mm := map[string]int{}
val := mm['bad_key'] or { panic('key not found') }
さらに、キーが存在するかどうか確認し、有る場合はその値を取得し、無ければそのまま進むこともこのようにできます。
m := {
'abc': 'def'
}
if v := m['abc'] {
println('the map value for that key is: ${v}')
}
この任意の確認は、配列でも同様に適用されます。
arr := [1, 2, 3]
large_index := 999
val := arr[large_index] or { panic('out of bounds') }
println(val)
// エラーを伝搬したい場合、以下のようにすることもできます。
val2 := arr[333]!
println(val2)
V はネストされた辞書配列もサポートしています。
mut m := map[string]map[string]int{}
m['greet'] = {
'Hello': 1
}
m['place'] = {
'world': 2
}
m['code']['orange'] = 123
print(m)
辞書配列は、Python の dictionary のように挿入順に順序付けされます。この順序は言語によって保証されています。なお、将来的に変更される可能性があります。
モジュールのインポート
モジュールの作り方は モジュール を参照してください。
モジュールはキーワード import
を使用してインポートすることができます。
import os
fn main() {
// stdin からテキストを読み取る
name := os.input('Enter your name: ')
println('Hello, ${name}!')
}
このプログラムでは、input
関数のような os
モジュールの公開された定義を使用することができます。一般的なモジュールとその公開されたシンボルのリストについては、標準ライブラリ のドキュメントを参照してください。
デフォルトでは、外部関数を呼び出すたびにモジュールを前に指定しなければなりません。最初は冗長に見えるかもしれませんが、コードがずっと読みやすくなり、どのモジュールのどの関数が呼び出されているのか理解しやすくなります。これは大規模なコードベースでは特に有用です。
Go のように、モジュールの循環インポートはできません。
選択インポート
関数や型は他のモジュールから直接インポートできます。
import os { input }
fn main() {
// 標準入力からテキストを読む
name := input('Enter your name: ')
println('Hello, $name!')
}
これにより、モジュールも同様にインポートされます。また、定数でこれはできません。定数は常にプレフィックスを付けなければなりません。
複数の特定のシンボルを一度にインポートすることもできます。
import os { input, user_os }
name := input('Enter your name: ')
println('Name: ${name}')
current_os := user_os()
println('Your OS is ${current_os}.')
モジュールインポートの別名
as
キーワードを用いてインポートしたモジュール名に別名をつけることができます。
この例は mymod/sha256/なにかの名前.v
を作成していない限りコンパイルできません (サブモジュール名は .v
ファイルの名前ではなくそのパスによって決定します)。
import crypto.sha256
import mymod.sha256 as mysha256
fn main() {
v_hash := sha256.sum('hi'.bytes()).hex()
my_hash := mysha256.sum('hi'.bytes()).hex()
assert my_hash == v_hash
}
関数や型のインポートに別名を付けることはできませんが、型の再宣言は できます。
import time
type MyTime = time.Time
fn (mut t MyTime) century() int {
return 1 + t.year % 100
}
fn main() {
my_time := MyTime{
year: 2020,
month: 12,
day: 25
}
println(time.new_time(my_time).utc_string())
println('Century: ${my_time.century()}')
}
文と式
if
a := 10
b := 20
if a < b {
println('${a} < ${b}')
} else if a > b {
println('${a} > ${b}')
} else {
println('${a} == ${b}')
}
if
文は素直で単純かつ他のほとんどの言語と同じです。
他の C 系の言語とは異なり、条件式は括弧で囲まず、波括弧は常に必要です。
if
は以下のように式としても使用できます。
num := 777
s := if num % 2 == 0 { 'even' } else { 'odd' }
println(s)
// "odd"
or {}
を用いることができる全ての場合について, "if unwrapping" をすることができます。式が none
やエラーではなかった場合には、中身の値を変数に代入することができます。
m := {
'foo': 'bar'
}
// handle missing keys
if v := m['foo'] {
println(v) // bar
} else {
println('not found')
}
fn res() !int {
return 42
}
// functions that return a result type
if v := res() {
println(v)
}
struct User {
name string
}
arr := [User{'John'}]
// if unwrapping with assignment of a variable
u_name := if v := arr[0] {
v.name
} else {
'Unnamed'
}
println(u_name) // John
型チェックとキャスト
直和型の現在の型がどうなっているかは is
で、否定は !is
で確認できます。
これは、以下のように if
の中でも可能です。
struct Abc {
val string
}
struct Xyz {
foo string
}
type Alphabet = Abc | Xyz
x := Alphabet(Abc{'test'}) // 直和型
if x is Abc {
// x は自動で Abc にキャストをされここで使える
println(x)
}
if x !is Abc {
println('Not Abc')
}
もしくは、以下のように match
を使います。
match x {
Abc {
// x は自動で Abc へキャストされてここで使える
println(x)
}
Xyz {
// x は自動で Xyz へキャストされてここで使える
println(x)
}
}
これは構造体のフィールドでも動作します。
struct MyStruct {
x int
}
struct MyStruct2 {
y string
}
type MySumType = MyStruct | MyStruct2
struct Abc {
bar MySumType
}
x := Abc{
bar: MyStruct{123} // MyStruct は自動的に MySumType 型へ変換される
}
if x.bar is MyStruct {
// x.bar は自動でキャストされる
println(x.bar)
} else if x.bar is MyStruct2 {
new_var := x.bar as MyStruct2
// もしくは `as` を用いて手動でキャストすることもできます。
println(new_var)
}
match x.bar {
MyStruct {
// x.bar は自動でキャストされる
println(x.bar)
}
else {}
}
可変な変数は変化することがあり、安全にキャストできません。しかし、可変であっても型キャストが必要な場合もあります。この場合、開発者は式に mut
キーワードを付けて、コンパイラに自分がしていることを分かっていると伝える必要があります。
これは以下のように動作します。
mut x := MySumType(MyStruct{123})
if mut x is MyStruct {
// x が可変であっても MyStruct へキャストされる
// mut キーワードがなければ動作しない
println(x)
}
// match も同様
match mut x {
MyStruct {
// x が可変であっても MyStruct へキャストされる
// mut キーワードがなければ動作しない
println(x)
}
}
in 演算子
in
はある配列にある要素が含まれるかどうかを確かめることができます。反対のことは !in
でできます。
nums := [1, 2, 3]
println(1 in nums) // true
println(4 !in nums) // true
m := {
'one': 1
'two': 2
}
println('one' in m) // true
println('three' !in m) // true
in
は辞書配列にキーが含まれるかを確認しますが、値は確認しません。
m := {
'one': 1
'two': 2
}
println('one' in m) // true
println('three' !in m) // true
さらに、以下のように真偽値の式をより明瞭かつコンパクトに書くのに役立ちます。
enum Token {
plus
minus
div
mult
}
struct Parser {
token Token
}
parser := Parser{}
if parser.token == .plus || parser.token == .minus || parser.token == .div || parser.token == .mult {
...
}
if parser.token in [.plus, .minus, .div, .mult] {
...
}
V はこういった式を最適化します。上に挙げた両方の if
文は同じ機械語を生成し、配列は作成されません。
Match
os := 'windows'
print('V is running on ')
match os {
'darwin' { println('macOS.') }
'linux' { println('Linux.') }
else { println(os) }
}
match
文は if - else
文の羅列を短く書く手段です。条件式の値と等しい最初の節が見つかると、続く式が評価されます。else
節は他に一致する節がない場合に評価されます。
number := 2
s := match number {
1 { 'one' }
2 { 'two' }
else { 'many' }
}
match
式は各節の最後の式を返します。
enum Color {
red
blue
green
}
fn is_red_or_blue(c Color) bool {
return match c {
.red, .blue { true } // カンマで分けて複数の値を検知できる
.green { false }
}
}
match
文は .種類
という省略構文で enum
の種類を分岐することにも使用できます。
c := `v`
typ := match c {
`0`...`9` { 'digit' }
`A`...`Z` { 'uppercase' }
`a`...`z` { 'lowercase' }
else { 'other' }
}
println(typ)
// 'lowercase'
match
のパターンに範囲を使用することもできます。値が節のその範囲に収まれば、その節を実行します。
注意として、範囲には ..
(点 2 つ) ではなく ...
(点 3 つ) を使用してください。これは含まない (..
の範囲) のと違い、範囲が最後の要素を 含む ようになります。..
を match
の節に使用するとエラーが発生します。
match
式は for
ループや if
文の条件式の中では使用できません。
for ループ
V でのループ構造は for
のみで、いくつかの形式があります。
for
/in
これは最も一般的な形式です。配列、辞書配列、数値の範囲で使用できます。
配列 for
numbers := [1, 2, 3, 4, 5]
for num in numbers {
println(num)
}
names := ['Sam', 'Peter']
for i, name in names {
println('${i}) ${name}')
// Output: 0) Sam
// 1) Peter
}
for value in arr
ループは配列の要素を取っていくのに用いられます。インデックスが必要な場合、もう一つの書き方として for index, value in
が使用できます。
注意として、value
は読み取り専用です。ループで配列を変更する必要がある場合、インデックスを用います。
mut numbers := [0, 1, 2]
for mut num in numbers {
num++
}
println(numbers) // [1, 2, 3]
識別子がアンダースコア一つのときは、無視されます。
カスタムイテレータ
Option
を返す next
メソッドを実装した型は、for
ループで繰り返せます。
struct SquareIterator {
arr []int
mut:
idx int
}
fn (mut iter SquareIterator) next() ?int {
if iter.idx >= iter.arr.len {
return none
}
defer {
iter.idx++
}
return iter.arr[iter.idx] * iter.arr[iter.idx]
}
nums := [1, 2, 3, 4, 5]
iter := SquareIterator{
arr: nums
}
for squared in iter {
println(squared)
}
上記のコードは以下を出力します。
1
4
9
16
25
辞書配列 for
m := {'one':1, 'two':2}
for key, value in m {
println("${key} -> ${value}") // 出力: one -> 1
} // two -> 2
キーや値を無視するにはアンダースコアを識別子に用いることでできます。
m := {
'one': 1
'two': 2
}
// キーで繰り返し
for key, _ in m {
println(key)
// Output: one
// two
}
// 値で繰り返し
for _, value in m {
println(value)
// Output: 1
// 2
}
範囲 for
// '01234' と出力する
for i in 0..5 {
print(i)
}
low..high
で 末尾を含まない 範囲を意味します。つまり low
以上 high
未満で high
を含まない すべての整数を表します。
条件 for
mut sum := 0
mut i := 0
for i <= 100 {
sum += i
i++
}
println(sum) // "5050"
このループの書き方は他の言語での while
ループと同様です。このループは条件式が false
に評価されると繰り返しを止めます。同じく、条件式は括弧で囲まず、波括弧は常に必要です。
危険な for
mut sum := 0
for {
num++
if num >= 10 {
break
}
}
println(num) // "10"
条件式は省略でき、これは無限ループになります。
C for
for i := 0; i < 10; i++ {
// 6 は出力しない
if i == 6 {
continue
}
println(i)
}
最後に、C スタイルの for
ループもあります。これは while
より安全な書き方です。なぜなら、後者の場合はカウンターの更新を忘れて無限ループに陥りやすいからです。
この i
は mut
をつけて宣言する必要はありません。定義したとき常にミュータブルになります。
ラベル付き break
と continue
break
と continue
はデフォルトで直近の for
ループを制御します。外側の for
ループを示すには, そのラベル名を break
や continue
に続けて書きます。
outer: for i := 4; true; i++ {
println(i)
for {
if i < 7 {
continue outer
} else {
break outer
}
}
}
ラベルは外側のループの前に付ける必要があります。上記コードの出力は以下のとおりです。
4
5
6
7
Defer
defer
文は、これが書かれているの関数が戻るまでその文にあたるブロックの実行を引き伸ばします。
fn read_log() {
mut ok := false
mut f := os.open('log.txt') or { panic(err) }
defer { f.close() }
// ...
if !ok {
// defer 文がここで呼び出され、ファイルが閉じられる
return
}
// ...
// defer 文がここで呼び出され、ファイルが閉じられる
}
関数が値を返す場合は、return式が評価された後にdeferブロックが実行されます。
import os
enum State {
normal
write_log
return_error
}
// ログファイルを書き込んで、書き込んだバイト数を返す
fn write_log(s State) !int {
mut f := os.create('log.txt')!
defer {
f.close()
}
if s == .write_log {
// `f.close()` は、`f.write()` が実行された後、
// `write_log()` が最終的に書き込まれたバイト数を
// `main()` へ返す前に呼び出されます
return f.writeln('This is a log file')
} else if s == .return_error {
// ファイルは `error()` 関数が返された後に
// クローズされるので、エラーメッセージにはオープン
// と表示されます
return error('nothing written; file open: ${f.is_opened}')
}
// ここでもファイルは閉じられます
return 0
}
fn main() {
n := write_log(.return_error) or {
println('Error: ${err}')
0
}
println('${n} bytes written')
}
defer
ブロックの中からその関数の実行結果にアクセスするには、$res()
式を用います。$res()
は 1 つの値を返すときにだけ利用でき、複数の値があるときは $res(idx)
で引数を渡すようになります。
fn (mut app App) auth_middleware() bool {
defer {
if !$res() {
app.response.status_code = 401
app.response.body = 'Unauthorized'
}
}
header := app.get_header('Authorization')
if header == '' {
return false
}
return true
}
fn (mut app App) auth_with_user_middleware() (bool, string) {
defer {
if !$res(0) {
app.response.status_code = 401
app.response.body = 'Unauthorized'
} else {
app.user = $res(1)
}
}
header := app.get_header('Authorization')
if header == '' {
return false, ''
}
return true, 'TestUser'
}
Goto
V では、goto
でラベルへ無条件ジャンプできます。ラベル名は goto
文と同じ関数内に含まれている必要があります。プログラム中で現在のスコープの外側またはそれよりも深い場所にあるラベルへ goto
できますが、変数の初期化を飛ばしたりメモリの解放を複数回行ってしまう可能性があるため、 unsafe
を付ける必要があります。
if x {
// ...
if y {
unsafe {
goto my_label
}
}
// ...
}
my_label:
goto
は避けて、できる限り for
を使用するべきです。特に、ラベル付き break はネストしたループからの脱出に利用できます。
構造体
struct Point {
x int
y int
}
p := Point{
x: 10
y: 20
}
println(p.x) // 構造体のフィールドはドットでアクセスできます
// もう一つのリテラル構文
p = Point{10, 20}
assert p.x == 10
ヒープの構造体
構造体はスタック上に確保されます。ヒープ上に確保する場合は、以下のように &
接頭子を使用してその参照を取得します。
struct Point {
x int
y int
}
p := &Point{10, 10}
// 参照にもフィールドにアクセスする構文がある
println(p.x)
p
の型は &Point
です。これは Point
への 参照 です。参照は Go のポインタや C++ の参照と似ています。
struct Foo {
mut:
x int
}
fa := Foo{1}
mut a := fa
a.x = 2
assert fa.x == 1
assert a.x == 2
// fb := Foo{ 1 }
// mut b := &fb // error: `fb` is immutable, cannot have a mutable reference to it
// b.x = 2
mut fc := Foo{1}
mut c := &fc
c.x = 2
assert fc.x == 2
assert c.x == 2
println(fc) // Foo{ x: 2 }
println(c) // &Foo{ x: 2 } // `&` の接頭辞に注意。
スタックとヒープ もご覧ください。
フィールドのデフォルト値
struct Foo {
n int // n は 0 がデフォルト値
s string // s は '' がデフォルト値
a []int // a は `[]int{}` がデフォルト値
pos int = -1 // カスタムデフォルト値
}
構造体の全フィールドは、構造体の作成時にデフォルトでゼロになります。配列と辞書配列のフィールドはメモリが確保されます。
カスタムのデフォルト値を定義することも可能です。
必須フィールド
struct Foo {
n int [required]
}
構造体のフィールドに [required]
属性を付けることで、その構造体のインスタンスを初期化する時にそのフィールドが必要であると V に伝えることが出来ます。
以下の例は、フィールド n
が明示的に初期化されていないのでコンパイルできません。
_ = Foo{}
短縮構造体リテラル構文
struct Point{
x int
y int
}
mut p := Point{
x: 10
y: 20
}
// 構造体名が分かっているので省略できます
p = Point{
x: 30
y: 4
}
assert p.y == 4
//
// 配列: 最初の要素が配列の型を決定する
points := [Point{10, 20}, Point{20, 30}, Point{40, 50}]
println(points) // [Point{x: 10, y: 20}, Point{x: 20, y: 30}, Point{x: 40,y: 50}]
構造体名の省略は、構造体リテラルを返す場合や、関数の引数として構造体を渡す場合に機能します。
構造体の更新構文
V はオブジェクトの変更版を簡単に返せます。
struct User {
name string
age int
is_registered bool
}
fn register(u User) User {
return User{
...u
is_registered: true
}
}
mut user := User{
name: 'abc'
age: 23
}
user = register(user)
println(user)
末尾構造体リテラル引数
[params]
struct ButtonConfig {
text string
is_disabled bool
width int = 70
height int = 20
}
fn new_button(c ButtonConfig) &Button {
return &Button{
width: c.width
height: c.height
text: c.text
}
}
button := new_button(text:'Click me', width:100)
// height を指定していないので、デフォルト値になります
assert button.height == 20
ご覧の通り、構造体名や波括弧は省略できます。以下のようにする必要はありません。
new_button(ButtonConfig{text:'Click me', width:100})
これは、構造体を最後の引数に取る関数でのみ動作します。
[params]
タグは、末尾構造体引数を完全に省略できることを V に対して伝えるものです。これにより、button := new_button()
のように書くことができます。これが無ければ、デフォルト値がある場合でもフィールドの名前を 少なくとも 1 つは指定しないといけません。指定しないと、引数なしでその関数を呼び出す場合にコンパイラがエラーを発生させます。error: expected 1 arguments, but got 0
アクセス指定子
構造体のフィールドはデフォルトで非公開かつイミュータブルです (構造体もイミュータブルです)。これらのアクセス指定は pub
と mut
で変更できます。以下のようにあわせて 5 つの選択肢があります。
struct Foo {
a int // 非公開 イミュータブル (デフォルト)
mut:
b int // 非公開 ミュータブル
c int // (同じアクセス指定で複数のフィールドを並べられる)
pub:
d int // 公開 イミュータブル (読み取り専用)
pub mut:
e int // 公開 親モジュールでのみミュータブル
__global:
f int // 公開かつ親モジュールの内側でも外側でもミュータブル
} // (利用を推奨していないので、'global' キーワードは __ で始まります)
非公開フィールドは同じ モジュール 内でのみ利用でき、他モジュールからの直接のアクセスはコンパイル時にエラーを生じます。公開のイミュータブルなフィールドはどの場所からでも読み取り専用です。
匿名構造体
V は匿名構造体をサポートしています。これは別々に宣言された構造体の名前を持たない構造体です。
struct Book {
author struct {
name string
age int
}
title string
}
book := Book{
author: struct {
name: 'Samantha Black'
age: 24
}
}
assert book.author.name == 'Samantha Black'
assert book.author.age == 24
静的型メソッド
V は User.new()
のような静的型メソッドをサポートするようになりました。これらは fn [型名].[関数名]
のようにして構造体について定義でき、その構造体に関する関数をすべて組織化できます。
struct User {}
fn User.new() User {
return User{}
}
user := User.new()
これは fn new_user() User {}
のようなファクトリ関数の代替手段であり、代わりにこちらを用いるべきです。
注意として、これらはコンストラクタではなく単なる関数です。V にはコンストラクタやクラスはありません。
[noinit]
構造体
V は [noinit]
構造体をサポートしています。これはそれが定義されたモジュールの外からは初期化できない構造体です。内部的に使用するか ファクトリ 関数を介して外部的に使用するものです。
例として、sample
ディレクトリにある以下のソースを考えてみましょう。
module sample
[noinit]
pub struct Information {
pub:
data string
}
pub fn new_information(data string) !Information {
if data.len == 0 || data.len > 100 {
return error('data must be between 1 and 100 characters')
}
return Information{
data: data
}
}
ここで new_information
は ファクトリ 関数です。これでこの構造体はモジュール外から利用したときは以下のようになります。
import sample
fn main() {
// こちらは [noinit] 属性があるため使えません。
// info := sample.Information{
// data: 'Sample information.'
// }
// こちらを使用します。
info := sample.new_information('Sample information.')!
println(info)
}
メソッド
struct User {
age int
}
fn (u User) can_register() bool {
return u.age > 16
}
user := User{
age: 10
}
println(user.can_register()) // "false"
user2 := User{
age: 20
}
println(user2.can_register()) // "true"
V にはクラスがありません。しかし、型にメソッドを定義できます。メソッドは特殊なレシーバー引数のある関数です。レシーバーの引数リストは fn
キーワードとメソッド名の間に出てきます。
この例では、can_register
メソッドには User
型の u
と命名されたレシーバーがあります。慣習として self
や this
のようなレシーバー名は使わずに、短めで、一文字長の名前が好まれます。
埋め込み構造体
V は埋め込み構造体をサポートしています。
struct Size {
mut:
width int
height int
}
fn (s &Size) area() int {
return s.width * s.height
}
struct Button {
Size
title string
}
埋め込むことで、構造体 Button
は自動的に構造体 Size
から全てのフィールドとメソッドを得ます。よって以下のようなことができます。
mut button := Button{
title: 'Click me'
height: 2
}
button.width = 3
assert button.area() == 6
assert button.Size.area() == 6
print(button)
出力:
Button{
Size: Size{
width: 3
height: 2
}
title: 'Click me'
}
継承とは異なり、構造体と埋め込まれた構造体の間で型変換はできません (埋め込んでいる構造体にはそれ独自のフィールドも持つことができ、複数の構造体を埋め込むこともできます)。
埋め込んだ構造体へ直接アクセスする必要があれば、button.Size
のように明示的な参照を使用してください。
概念的に、埋め込み構造体は OOP における Mixin と似ており、基底クラスとは 異なります。
埋め込み構造体も以下のように初期化できます。
mut button := Button{
Size: Size{
width: 3
height: 2
}
}
値の代入は以下のとおりです。
button.Size = Size{
width: 4
height: 5
}
複数の埋め込んだ構造体に同じ名前のメソッドやフィールドがある場合や、同じ名前のメソッドやフィールドの構造体内で定義されている場合、メソッドの呼び出しや変数への代入は button.Size.area()
のように行います。埋め込んだ構造体の名前を指定しなければ、最も外側の構造体が選ばれます。
共用体
構造体のように、共用体も埋め込みをサポートしています。
struct Rgba32_Component {
r u8
g u8
b u8
a u8
}
union Rgba32 {
Rgba32_Component
value u32
}
clr1 := Rgba32{
value: 0x008811FF
}
clr2 := Rgba32{
Rgba32_Component: Rgba32_Component{
a: 128
}
}
sz := sizeof(Rgba32)
unsafe {
println('Size: ${sz}B,clr1.b: ${clr1.b},clr2.b: ${clr2.b}')
}
出力: Size: 4B, clr1.b: 136, clr2.b: 0
共用体のメンバーへのアクセスは unsafe
ブロック内で行なければなりません。
埋め込み構造体の引数は必ずしも列挙した順番通りに格納されるわけではありません。
関数 2
既定で純粋関数
V の関数はデフォルトで純粋です。これは戻り値は引数によってのみ決定されるという意味で、その評価に副作用はありません。
これはグローバル変数がないことと、関数の引数は 参照 を渡してもデフォルトでイミュータブルであることによって実現しています。
しかし、V は純粋関数型言語ではありません。
グローバル変数を有効にするコンパイラフラグ (--enable-globals
) がありますが、これはカーネルやドライバといった低水準アプリケーションを想定しています。
可変引数
以下のように mut
キーワードを使うことで、関数の引数を変更できます。
struct User {
name string
mut:
is_registered bool
}
fn (mut u User) register() {
u.is_registered = true
}
mut user := User{}
println(user.is_registered) // "false"
user.register()
println(user.is_registered) // "true"
この例では、レシーバー (単なる第一引数) がミュータブルにされているので、register()
は user
オブジェクトを変更できます。以下のレシーバーの無い関数でも同様に動作します。
fn multiply_by_2(mut arr []int) {
for i in 0..arr.len {
arr[i] *= 2
}
}
mut nums := [1, 2, 3]
multiply_by_2(mut nums)
println(nums) // "[2, 4, 6]"
注意として、mut
を nums
の前に追加してこの関数を呼び出す必要があります。これは呼び出す関数が値を変更することを明確にします。
引数を変更するよりは戻り値を返すほうが望ましいです。引数の変更は、アプリケーションのパーフォーマンス的に重要な箇所でメモリ確保やコピーを削減するためにのみされるべきです。
この理由から、V ではプリミティブ型 (整数など) の引数を変更できません。配列や辞書配列のような、より複雑な型のみが変更できます。
register(mut user)
よりは user.register()
や user = register(user)
を使用してください。
可変長引数
V は任意個の、可変な個数の引数を受け取る関数をサポートしています。これは ...
接頭辞で表します。以下の、a ...int
は任意個の引数が a
という名前の配列として集められることを表しています。
fn sum(a ...int) int {
mut total := 0
for x in a {
total += x
}
return total
}
println(sum()) // 出力: 0
println(sum(1)) // 1
println(sum(2, 3)) // 5
// 配列展開を使用
a := [2, 3, 4]
println(sum(...a)) // <-- ... 接頭子をここで使います。出力: 9
b := [5, 6, 7]
println(sum(...b)) // 出力: 18
無名関数と高階関数
fn sqr(n int) int {
return n * n
}
fn cube(n int) int {
return n * n * n
}
fn run(value int, op fn (int) int) int {
return op(value)
}
fn main() {
// 関数は他の関数に渡せる
println(run(5, sqr)) // "25"
// 関数の中で無名の関数を宣言できる
double_fn := fn (n int) int {
return n + n
}
println(run(5, double_fn)) // "10"
// 変数に代入しなくても、関数を渡したりできる
res := run(5, fn (n int) int {
return n + n
})
println(res) // "10"
// 関数の配列/辞書配列もできる
fns := [sqr, cube]
println(fns[0](10)) // "100"
fns_map := {
'sqr': sqr
'cube': cube
}
println(fns_map['cube'](2)) // "8"
}
クロージャ
V はクロージャもサポートしています。これは匿名関数が作られたそのスコープの変数を継承できるということです。継承される全ての変数は明示的に列挙しなければなりません。
my_int := 1
my_closure := fn [my_int] () {
println(my_int)
}
my_closure() // 1 を出力する
継承される変数は匿名関数が作成されたときにコピーされます。すなわち、関数の作成後に元の変数を変更しても、それはその関数には反映されません。
mut i := 1
func := fn [i] () int {
return i
}
println(func() == 1) // true
i = 123
println(func() == 1) // true のまま
しかし、変数は匿名関数の中から変更できます。その変更は外側には反映されませんが、以後のその関数の呼び出しには影響します。
fn new_counter() fn () int {
mut i := 0
return fn [mut i] () int {
i++
return i
}
}
c := new_counter()
println(c()) // 1
println(c()) // 2
println(c()) // 3
関数の外側の値を変更する必要がある場合は、参照を使用してください。警告: 参照は常に有効であるようにしてください。さもなくば未定義動作を引き起こします。
mut i := 0
mut ref := &i
print_counter := fn [ref] () {
println(*ref)
}
print_counter() // 0
i = 10
print_counter() // 10
引数の評価順序
関数呼び出しの引数の評価順序は保証 されていません。以下のプログラムを例に挙げます。
fn f(a1 int, a2 int, a3 int) {
dump(a1 + a2 + a3)
}
fn main() {
f(dump(100), dump(200), dump(300))
}
V は現在 100, 200, 300 の順で出力することを保証しません。唯一の保証はその後に 600 が (f
の本体処理によって) 出力されることだけです。
これは V 1.0 で変更される かもしれません。
参照
struct Foo{}
fn (foo Foo) bar_method() {
// ...
}
fn bar_function(foo Foo) {
// ...
}
上の例での foo
のように引数がイミュータブルな場合、V は値か参照で値渡しできます。この決定はコンパイラによって行われ、開発者がそのことを考慮する必要はありません。
構造体を値で渡すべきか参照で渡すべきか覚えておく必要はもうないのです。
常に参照渡しすることを確実にするには、以下のように &
を付けます。
struct Foo {
abc int
}
fn (foo &Foo) bar() {
println(foo.abc)
}
これでも foo
はイミュータブルで変更されません。そのためには、(mut foo Foo)
とする必要があります。
一般的に、V の参照は Go のポインタや C++ の参照と似ています。例えば、木構造の定義はこのようになります。
struct Node[T] {
val T
left &Node[T]
right &Node[T]
}
参照を参照外しするには、C のように *
演算子を使用します。
定数
const (
pi = 3.14
world = '世界'
)
println(pi)
println(world)
定数は const
キーワードで宣言されます。これらはモジュールレベル (関数の外側) で定義できます。定数は変更できません。
const e = 2.71828
V の定数はほとんどの言語よりも柔軟です。以下の複雑な値も代入できます。
struct Color {
r int
g int
b int
}
fn rgb(r int, g int, b int) Color {
return Color{
r: r
g: g
b: b
}
}
const (
numbers = [1, 2, 3]
red = Color{
r: 255
g: 0
b: 0
}
// 関数呼び出しはコンパイル時に評価される
blue = rgb(0, 0, 255)
)
println(numbers)
println(red)
println(blue)
* WIP - 現在、関数呼び出しをしている場合はプログラム起動時に評価されます
グローバル変数が許可されていないので、これは非常に役立ちます。
モジュール
定数は以下のように pub const
で公開できます。
module mymodule
pub const golden_ratio = 1.61803
fn calc() {
println(mymodule.golden_ratio)
}
pub
キーワードは const
キーワードの前にのみ付加でき、const ( )
ブロック内では使用できません。
main モジュールの外にあるすべての定数はモジュール名をその前に付ける必要があります。
モジュール名を前に付けることが必須
定数の名前をつけるときは、snake_case
を使わなければなりません。定数はローカル変数と区別するため、定数のフルパスを指定する必要があります。たとえば、PI 定数にアクセスするには、完全な math.pi
という名前を math
モジュールの外と中の両方で使用する必要があります。この制限は main
モジュール (fn main()
を含むモジュール) においてのみ緩和され、そこでは main.numbers
ではなく、numbers
のように定義された定数の非限定的な名前を使用できます。
vfmt はこのルールを管理しており、math
モジュール内で println(pi)
と入力すると、vfmt は自動的に println(math.pi)
に更新します。
組み込み関数
println
のような関数は組み込みです。以下は組み込み関数の完全なリストです。
fn print(s string) // 標準出力に出力します
fn println(s string) // 標準出力に出力して改行します
fn eprint(s string) // print() と同じですが、標準エラー出力を使います
fn eprintln(s string) // println() と同じですが、標準エラー出力を使います
fn exit(code int) // 指定のエラーコードでプログラムを終了します
fn panic(s string) // メッセージとバックトレースを標準エラー出力に出力して、エラーコード 1 でプログラムを終了します
fn print_backtrace() // バックトレースを標準エラー出力に出力します
print
関数は文字列を受け取ることになっていますが、V は他の出力可能な型も受け付けます。詳細は以下をご覧ください。
dump
という特殊な組み込み関数もあります。
println
println
はシンプルかつ強力な組み込み関数です。これは、文字列、数値、配列、辞書配列、構造体など、なんでも出力できます。
struct User {
name string
age int
}
println(1) // "1"
println('hi') // "hi"
println([1, 2, 3]) // "[1, 2, 3]"
println(User{ name: 'Bob', age: 20 }) // "User{name:'Bob', age:20}"
配列のメソッド もご覧ください。
型のカスタム出力
自分の型に対してカスタムの出力値を定義したい場合は、str() string
メソッドを定義するだけです。
struct Color {
r int
g int
b int
}
pub fn (c Color) str() string {
return '{${c.r}, ${c.g}, ${c.b}}'
}
red := Color{
r: 255
g: 0
b: 0
}
println(red)
実行時に式を出力する
あらゆる V の式は、dump(expr)
を用いてその値を出力/トレースできます。例えば、以下のコードサンプルを factorial.v
として保存し、v run factorial.v
で実行してみましょう。
fn factorial(n u32) u32 {
if dump(n <= 1) {
return dump(1)
}
return dump(n * factorial(n - 1))
}
fn main() {
println(factorial(5))
}
このように出力されます。
[factorial.v:2] n <= 1: false
[factorial.v:2] n <= 1: false
[factorial.v:2] n <= 1: false
[factorial.v:2] n <= 1: false
[factorial.v:2] n <= 1: true
[factorial.v:3] 1: 1
[factorial.v:5] n * factorial(n - 1): 2
[factorial.v:5] n * factorial(n - 1): 6
[factorial.v:5] n * factorial(n - 1): 24
[factorial.v:5] n * factorial(n - 1): 120
120
注意として dump(expr)
はソース上の位置、式自体と式の値のすべてをトレースします。
モジュール
フォルダのルートにあるすべてのファイルは、同じモジュールの一部です。単純なプログラムではモジュール名を指定する必要はありませんが、その場合はデフォルトで「main」となります。
モジュールの作成
V はとてもモジュール方式な言語です。再利用可能なモジュールを作成することは、推奨されており、非常に簡単です。新しいモジュールを作成するには、モジュールの名前のディレクトリを作成してコードを書いた .v
ファイルを入れます。
cd ~/code/modules
mkdir mymodule
vim mymodule/myfile.v
module mymodule
// 関数をエクスポートするには `pub` を使用します
pub fn say_hi() {
println('hello from mymodule!')
}
モジュール内のすべてのアイテムは、pub
キーワードが付けられているか否かにかかわらずそのモジュールのファイルの間では利用できます。
// myfile2.v
module mymodule
pub fn say_hi_and_bye() {
say_hi() // myfile.v から
println('goodbye from mymodule')
}
これで mymodule
をコード内で使えるようになりました。
import mymodule
fn main() {
mymodule.say_hi()
mymodule.say_hi_and_bye()
}
- モジュールの名前は 10 文字以下くらいに短くするべきです。
- モジュール名は
snake_case
を使用しなければなりません。 - 循環インポートはできません。
- モジュール内に
.v
ファイルをいくらでも作成できます。 - 今現在はモジュールをどこにでも作成できます。
- すべてのモジュールは静的に単一の実行形式にコンパイルされます。
init
関数
インポートされた場合に何らかのセットアップ/初期化コードを自動的に呼び出してほしい場合、以下のような init
関数を利用できます。
fn init() {
// ここにセットアップコード ...
}
init
関数は公開できません。自動的に呼び出されます。これは C ライブラリの初期化に役立ちます。
型宣言
型エイリアス
新しい型 NewType
を ExistingType
の別名として定義するには、type NewType = ExistingType
とします。これは 直和型 の宣言の特別な場合です。
列挙体
enum Color as u8 {
red green blue
}
mut color := Color.red
// V は `color` が `Color` 型だとわかっています。ここで `color = Color.green` とする必要はありません。
color = .green
println(color) // "green"
match color {
.red { println('the color was red') }
.green { println('the color was green') }
.blue { println('the color was blue') }
}
列挙型のマッチは、条件を全て網羅しているか、else
ブランチを持たなければなりません。これにより、新しい列挙フィールドが追加された場合でも、コード内の全箇所で正しい処理が保証されます。
列挙体のフィールドでは、予約済みのキーワードを再利用できません。しかし、予約済みのキーワードは @ でエスケープできます。
enum Color {
@none
red
green
blue
}
color := Color.@none
println(color)
整数を列挙体のフィールドに代入することもできます。
enum Grocery {
apple
orange = 5
pear
}
g1 := int(Grocery.apple)
g2 := int(Grocery.orange)
g3 := int(Grocery.pear)
println('Grocery IDs: ${g1}, ${g2}, ${g3}')
出力: Grocery IDs: 0, 5, 6
.
列挙体の変数では演算できず、int
に明示的にキャストしなければなりません。
列挙体も、構造体のようにメソッドを持てます。
enum Cycle {
one
two
three
}
fn (c Cycle) next() Cycle {
match c {
.one {
return .two
}
.two {
return .three
}
.three {
return .one
}
}
}
mut c := Cycle.one
for _ in 0 .. 10 {
println(c)
c = c.next()
}
出力:
one
two
three
one
two
three
one
two
three
one
関数型
型のエイリアスは、特定の関数シグネチャの名前に使用できます。以下はその例です。
type Filter = fn (string) string
これは、他の型と同様に機能します。例えば、関数が関数型の引数を取ることができます。
type Filter = fn (string) string
fn filter(s string, f Filter) string {
return f(s)
}
V はダックタイピングを採用しているので、関数は関数型との互換性を宣言する必要はありません。
fn uppercase(s string) string {
return s.to_upper()
}
// これで `uppercase` は Filter を必要とするあらゆる場所で使用できる
互換な関数は以下のように関数型へ明示的にキャストすることもできます。
my_filter := Filter(uppercase)
ここでのキャストは純粋に情報的なものです。繰り返しになりますが、ダックタイピングとは明示的にキャストしなくても結果的に型が同じになることを意味します。
my_filter := uppercase
代入した関数を引数として渡すこともできます。
println(filter('Hello world', my_filter)) // `HELLO WORLD` と出力する
もちろん、ローカル変数を使わずに直接渡すこともできます。
println(filter('Hello world', uppercase))
そして匿名関数でも同様に動作します。
println(filter('Hello world', fn (s string) string {
return s.to_upper()
}))
完全な コード例はこちら でご覧ください。
インターフェイス
struct Dog {
breed string
}
struct Cat {
breed string
}
fn (d Dog) speak() string {
return 'woof'
}
fn (c Cat) speak() string {
return 'meow'
}
// Go とは異なり、TypeScript と同様に V のインターフェイスはメソッドだけでなくフィールドも定義できます。
interface Speaker {
breed string
speak() string
}
dog := Dog{'Leonberger'}
cat := Cat{'Siamese'}
mut arr := []Speaker{}
arr << dog
arr << cat
for item in arr {
println('a $item.breed says: $item.speak()')
}
インターフェイスの実装
型がインターフェイスを実装するには、そのメソッドを実装することでできます。意図を明示的に宣言することはできず、"implements" キーワードもありません。
インターフェイスは mut:
の区間を持つことができます。型の実装においてはこの mut
レシーバーを、インターフェイスの mut:
区間に宣言されているメソッドごとに持っている必要があります。
module main
interface Foo {
write(string) string
}
// => この型のメソッドシグネチャでは、Foo を実装するときはこのようになります。
// `fn (s Type) write(a string) string`
interface Bar {
mut:
write(string) string
}
// => この型のメソッドシグネチャでは、Bar を実装するときはこのようになります。
// `fn (mut s Type) write(a string) string`
struct MyStruct {}
// MyStruct はインターフェイス Foo を実装していますが、インターフェイス Bar は実装 *していません*
fn (s MyStruct) write(a string) string {
return a
}
fn main() {
s1 := MyStruct{}
fn1(s1)
// fn2(s1) -> コンパイルエラー、MyStruct は Bar を実装していません
}
fn fn1(s Foo) {
println(s.write('Foo'))
}
// fn fn2(s Bar) { // 適合しない
// println(s.write('Foo'))
// }
インターフェイスのキャスト
動的キャスト演算子を使用して、インターフェイスの基底型を判別できます。
この例のように、動的キャストは変数 s
をその if
文の中でのみポインタに変換します。
interface Something {}
fn announce(s Something) {
if s is Dog {
println('a ${s.breed} dog') // `s` は自動的に `Dog` へとキャストされます (スマートキャスト」
} else if s is Cat {
println('a cat speaks ${s.speak()}')
} else {
println('something else')
}
}
fn main() {
dog := Dog{'Leonberger'}
cat := Cat{'Siamese'}
announce(dog)
announce(cat)
}
interface IFoo {
foo()
}
interface IBar {
bar()
}
// implements only IFoo
struct SFoo {}
fn (sf SFoo) foo() {}
// implements both IFoo and IBar
struct SFooBar {}
fn (sfb SFooBar) foo() {}
fn (sfb SFooBar) bar() {
dump('This implements IBar')
}
fn main() {
mut arr := []IFoo{}
arr << SFoo{}
arr << SFooBar{}
for a in arr {
dump(a)
// In order to execute instances that implements IBar.
if a is IBar {
// a.bar() // Error.
b := a as IBar
dump(b)
a.bar()
}
}
}
詳細は、動的キャスト を参照してください。
インターフェイスメソッドの定義
Go とは異なり、構造体がメソッドを持つのと同じように、インターフェイスはそれ自身がメソッドを持てます。これらの「インターフェイスメソッド」は実装されている必要はなく、そのインターフェイスを実装する構造体それぞれによって実装されます。これは some_function(i)
の代わりに i.some_function()
と書く便利な手段であり、構造体において xyz(s)
の代わりに s.xyz()
と書くとような利便性と似たようなものです。
この機能は C# などでの「デフォルト実装」ではありません。
例えば、構造体 cat
がインターフェイス a
によってラップされていて、同じ名前 speak
のメソッドを実装しているとします。このとき構造体によってそのメソッドが実装されていますが、a.speak()
としたときはインターフェイスメソッドのみが呼び出されます。
interface Adoptable {}
fn (a Adoptable) speak() string {
return 'adopt me!'
}
struct Cat {}
fn (c Cat) speak() string {
return 'meow!'
}
struct Dog {}
fn main() {
cat := Cat{}
assert dump(cat.speak()) == 'meow!'
//
a := Adoptable(cat)
assert dump(a.speak()) == 'adopt me!' // Adoptable の `speak` を呼び出します
if a is Cat {
// この `if` の中では、V にとって `a` がただの Adoptable の一種ではなく
// 実際には Cat であることが分かるので、Adoptable の `speak`
// ではなく Cat の `speak` が使用されます。
dump(a.speak()) // meow!
}
//
b := Adoptable(Dog{})
assert dump(b.speak()) == 'adopt me!' // Adoptable の `speak` を呼び出します。
// if b is Dog {
// dump(b.speak()) // error: unknown method or field: Dog.speak
// }
}
埋め込みインターフェイス
インターフェイスは、構造体のような埋め込みをサポートしています。
pub interface Reader {
mut:
read(mut buf []u8) ?int
}
pub interface Writer {
mut:
write(buf []u8) ?int
}
// ReaderWriter は Reader と Writer の両方を埋め込んでいます。
// これは Reader と Writer のメソッド/フィールドをすべて ReaderWriter
// へコピー/ペーストするのと同じ効果です。
pub interface ReaderWriter {
Reader
Writer
}
直和型
直和型のインスタンスはいくつか異なる種類の値を保持できます。以下のように type
キーワードで直和型を宣言できます。
struct Moon {}
struct Mars {}
struct Venus {}
type World = Moon | Mars | Venus
sum := World(Moon{})
assert sum.type_name() == 'Moon'
println(sum)
組み込みメソッド type_name
は現在保持している型の名前を返します。
直和型を使用すると、再帰的な構造体を構築でき、その上で簡潔かつ強力なコードを書けます。
// V の二分木
struct Empty {}
struct Node {
value f64
left Tree
right Tree
}
type Tree = Empty | Node
// 全ノードの値を合計
fn sum(tree Tree) f64 {
return match tree {
Empty { f64(0) } // TODO: match を賢くして f64() を取り除く
Node { tree.value + sum(tree.left) + sum(tree.right) }
}
}
fn main() {
left := Node{0.2, Empty{}, Empty{}}
right := Node{0.3, Empty{}, Node{0.4, Empty{}, Empty{}}}
tree := Node{0.5, left, right}
println(sum(tree)) // 0.2 + 0.3 + 0.4 + 0.5 = 1.4
}
動的キャスト
直和型の実際の型を確かめるには、以下のように sum is Type
を使用します。特定の種類にキャストするには sum as Type
とします。
struct Moon {}
struct Mars {}
struct Venus {}
type World = Mars | Moon | Venus
fn (m Mars) dust_storm() bool {
return true
}
fn main() {
mut w := World(Moon{})
assert w is Moon
w = Mars{}
// `as` を使用して Mars インスタンスにアクセスします
mars := w as Mars
if mars.dust_storm() {
println('bad weather!')
}
}
as
では w
が Mars
インスタンスを保持していないとパニックします。スマートキャストのほうが安全な方法です。
スマートキャスト
if w is Mars {
assert typeof(w).name == 'Mars'
if w.dust_storm() {
println('bad weather!')
}
}
wは
if文の本文の中に
Mars という型を持っています。これは *フロー感知型付け* として知られています。
wが可変な識別子の場合、コンパイラがスマートキャストするために警告なしで危険になることがあります。このために
is式の前で
mut` を宣言する必要があります。
if w is Mars as mars {
assert typeof(w).name == 'Mars'
if mars.dust_storm() {
println('bad weather!')
}
}
この場合の w
は元の型のままです。
この形は、単純な変数名でも
user.name
のような複雑な式の場合でも動作します。
直和型のマッチング
match
を使用して種類を特定することもできます。
struct Moon {}
struct Mars {}
struct Venus {}
type World = Mars | Moon | Venus
fn open_parachutes(n int) {
println(n)
}
fn land(w World) {
match w {
Moon {} // 大気なし
Mars {
// 薄い大気
open_parachutes(3)
}
Venus {
// 濃い大気
open_parachutes(1)
}
}
}
match
は各列挙子に対してのパターンを用意するか、else
節が必要です。
struct Moon {}
struct Mars {}
struct Venus {}
type World = Moon | Mars | Venus
fn (m Moon) moon_walk() {}
fn (m Mars) shiver() {}
fn (v Venus) sweat() {}
fn pass_time(w World) {
match w {
// シャドーイングされたマッチ変数の使用例。この場合は `w` (スマートキャスト)
Moon { w.moon_walk() }
Mars { w.shiver() }
else {}
}
}
Option/Result 型とエラーハンドリング
Option 型は、none
になる可能性があることを表現する型です。Result 型は、関数からの戻り値がエラーになるかもしれないことを表現する型です。
以下のようにOption 型は ?Type
で宣言でき、Result 型は !Type
で宣言できます。
struct User {
id int
name string
}
struct Repo {
users []User
}
fn (r Repo) find_user_by_id(id int) !User {
for user in r.users {
if user.id == id {
// V は自動的にこれを Result 型にラップする
return user
}
}
return error('User ${id} not found')
}
// Option 型を用いたバージョン
fn (r Repo) find_user_by_id(id int) ?User {
for user in r.users {
if user.id == id {
return user
}
}
return none
}
fn main() {
repo := Repo{
users: [User{1, 'Andrew'}, User{2, 'Bob'},
User{10, 'Charles'},
]
}
user := repo.find_user_by_id(10) or { // Option/Result 型は `or` ブロックでハンドリングする必要がある
return
}
println(user.id) // "10"
println(user.name) // "Charles"
user2 := repo.find_user_by_id2(10) or { return }
}
V はかつて Option
と Result
を一つの型に組み合わせていましたが、今は別の型になっています。
関数を Option/Result な関数へ「アップグレード」する必要のある作業が山のようにあっても、その変更は小さくなります。戻り値の型に ?
または !
を加えて、何かがおかしいときに none
またはエラーを返すだけです。
これは V でエラーを制御する主要な手段です。これらは値でもあり、Go のようですが、エラーを無視できないという利点があります。且つ、これらを制御することはそこまで煩わしくありません。
他の言語とは異なり、V は throw/try/catch
で例外を処理するようなことはしません。
err
は or
ブロックの中で定義され、error()
関数で渡された文字列のメッセージがセットされています。
user := repo.find_user_by_id(7) or {
println(err) // "User 7 not found"
return
}
Option/Result のハンドリング
Option のハンドリングには 4 つの方法があります。1 つ目はエラーを伝播させます。
import net.http
fn f(url string) !string {
resp := http.get(url)!
return resp.body
}
http.get
は !http.Response
を返します。呼び出し時に !
を付けているため、このエラーは f
の呼び出し元へと伝播されます。Option
型を返す関数呼び出しの後に ?
を使用する場合、それを囲っている関数も同様にオプションを返さなければなりません。関数 main()
でこのエラーの伝播を使用した場合、エラーはそれ以上伝播されずに、代わりに panic
をします。
f
の本文は、基本的には以下を短縮したものです。
resp := http.get(url) or { return err }
return resp.body
2 つ目の手法は、実行を早めに切り上げることです。
user := repo.find_user_by_id(7) or { return }
ここでは、panic()
や exit()
を呼び出してプログラム全体の実行を停止するか、制御フロー文 (return
、break
、continue
など) を使って現在のブロックから抜け出すことができます。
break
と continue
は for
ループ内でのみ使用できます。
V には強制的に Option
を "unwrap" する方法 (他の言語では Rust の unwrap()
や Swift の !
など) がありません。これをするには、代わりに or { panic(err) }
とします。
3 つ目の手法は or
ブロックの最後でデフォルト値を提供することです。この場合のエラー処理部は、その値が代わりに代入されます。そのため、Option
の中身の型と同じ型を返す必要があります。
fn do_something(s string) !string {
if s == 'foo' {
return 'foo'
}
return error('invalid string') // `return none` のようにもできる
}
a := do_something('foo') or { 'default' } // a will be 'foo'
b := do_something('bar') or { 'default' } // b will be 'default'
println(a)
println(b)
4 つ目の手法は if
で取り出すものです。
if resp := http.get('https://google.com') {
println(resp.body) // resp は http.Response であり、Option ではありません
} else {
println(err)
}
上記では、http.get
は !http.Response
を返します。resp
のスコープは最初の if
節の中だけです。err
のスコープは else
節の中だけです。
カスタムエラー型
V は IError
インターフェイスを介してカスタムエラー型を定義できるようになっています。このインターフェイスは、msg() string
と code() int
の 2 つのメソッドを要求します。これらのメソッドを実装するすべての方はエラーとして利用できます。
カスタムエラー型を定義するときは、組み込みの Error
デフォルト実装を埋め込むことを推奨します。これは両方の必須メソッドに対する空のデフォルト実装だけでなく、将来追加されるユーティリティ関数も提供します。
struct PathError {
Error
path string
}
fn (err PathError) msg() string {
return 'Failed to open path: ${err.path}'
}
fn try_open(path string) ! {
// 自動的に IError 型にキャストされます
return PathError{
path: path
}
}
fn main() {
try_open('/tmp') or { panic(err) }
}
ジェネリクス
struct Repo[T] {
db DB
}
struct User {
id int
name string
}
struct Post {
id int
user_id int
title string
body string
}
fn new_repo[T](db DB) Repo[T] {
return Repo[T]{db: db}
}
// これはジェネリック関数です。V は使用される型ごとに関数を生成します。
fn (r Repo[T]) find_by_id(id int) ?T {
table_name := T.name // この例では型の名前をテーブル名にしています
return r.db.query_one[T]('select * from $table_name where id = ?', id)
}
db := new_db()
users_repo := new_repo[User](db) // 戻り値 Repo[User]
posts_repo := new_repo[Post](db) // 戻り値 Repo[Post]
user := users_repo.find_by_id(1)? // find_by_id[User]
post := posts_repo.find_by_id(1)? // find_by_id[Post]
現在のところ、ジェネリック関数の定義では型引数を宣言しなければなりません。しかし将来的には、実行時の引数型の中にある一文字の型名からジェネリック型引数を推測するようになります。レシーバー引数 r
がジェネリック型の T
であるため、find_by_id
では [T]
を省略できます。
他の例も示します。
fn compare[T](a, b T) int {
if a < b {
return -1
}
if a > b {
return 1
}
return 0
}
// compare[int]
println(compare(1,0)) // 出力: 1
println(compare(1,1)) // 0
println(compare(1,2)) // -1
// compare[string]
println(compare('1','0')) // 出力: 1
println(compare('1','1')) // 0
println(compare('1','2')) // -1
// compare[f64]
println(compare(1.1, 1.0)) // 出力: 1
println(compare(1.1, 1.1)) // 0
println(compare(1.1, 1.2)) // -1
並行計算
並行タスクの生成
V の並行処理モデルは Go のものと非常によく似ています。現在では、spawn foo()
とすると foo()
を異なるスレッドで並行的に実行します。
import math
fn p(a f64, b f64) { // 戻り値がない普通の関数
c := math.sqrt(a * a + b * b)
println(c)
}
fn main() {
spawn p(3, 4)
// p は並列スレッドでじっこうされます
// これは以下のように書くこともできます
// spawn fn (a f64, b f64) {
// c := math.sqrt(a * a + b * b)
// println(c)
// }(3, 4)
}
スレッドはマシンの CPU (スレッドあたりのコア数) に依存します。spawn
で生成した OS スレッドには、リソースのオーバーヘッドやスケーラビリティの問題など、同時実行性に関する制限があり、スレッド数が多い場合にはパフォーマンスに影響を与え得ることに注意してください。
go
キーワードも存在しています。現在では go foo()
は vfmt によって自動的に spawn foo()
へと名称変更され、go
はコルーチン (ランタイムが管理する軽量スレッド) を実行する方法になる予定です。
時折、並列スレッドが終了するまで待機する必要がることがあります。これは開始したスレッドへの ハンドル を割り当てて、後でそのハンドルの wait()
メソッドを呼び出すことでできます。
import math
fn p(a f64, b f64) { // 戻り値がない普通の関数
c := math.sqrt(a * a + b * b)
println(c) // `5` が出力されます
}
fn main() {
h := spawn p(3, 4)
// p() を並列スレッドで実行します
h.wait()
// p() は確実に終了します
}
このアプローチは、並列スレッドが実行している関数から戻り値を取得する場合にも利用できます。並行呼び出しできるように関数自体を修正する必要はありません。
import math { sqrt }
fn get_hypot(a f64, b f64) f64 { // 戻り値がある普通の関数
c := sqrt(a * a + b * b)
return c
}
fn main() {
g := go get_hypot(54.06, 2.08) // スレッドを生成してそのハンドルを得ます
h1 := get_hypot(2.32, 16.74) // ここで他の計算もします
h2 := g.wait() // 生成したスレッドから結果を取得します
println('Results: ${h1}, ${h2}') // `Results: 16.9, 54.1` と出力されます
}
値を返さないタスクが多数ある場合は、待ちグループを使って管理する方が簡単かもしれません。
import time
fn task(id int, duration int) {
println('task ${id} begin')
time.sleep(duration * time.millisecond)
println('task ${id} end')
}
fn main() {
mut threads := []thread{}
threads << spawn task(1, 500)
threads << spawn task(2, 900)
threads << spawn task(3, 100)
threads.wait()
println('done')
}
// 出力:
// task 1 begin
// task 2 begin
// task 3 begin
// task 3 end
// task 1 end
// task 2 end
// done
さらに、同じ型を返すスレッドの配列に対して wait()
を呼び出すと、計算されたすべての値が返されます。
fn expensive_computing(i int) int {
return i * i
}
fn main() {
mut threads := []thread int{}
for i in 1 .. 10 {
threads << spawn expensive_computing(i)
}
// Join all tasks
r := threads.wait()
println('All jobs finished: ${r}')
}
// 出力: All jobs finished: [1, 4, 9, 16, 25, 36, 49, 64, 81]
チャンネル
チャンネルは、コアーチン間の通信に適した方法です。V のチャンネルは基本的に Go のチャンネルと同じように動作します。一方からオブジェクトをチャンネルに押し込み、他方からオブジェクトを取り出すことができます。チャンネルはバッファしたりしなかったり、複数のチャンネルから select
したりできます。
構文と使用方法
チャンネルの型は chan objtype
です。宣言では、オプションでバッファの長さを cap
プロパティとして指定することができます。
ch := chan int{} // バッファされない - "同期的"
ch2 := chan f64{cap: 100} // バッファ長 100
チャンネルは mut
として宣言する必要はありません。バッファの長さは型の一部ではなく、個々のチャンネルオブジェクトのプロパティです。チャンネルは、通常の変数のようにコアーチンに渡すことができます。
fn f(ch chan int) {
// ...
}
fn main() {
ch := chan int{}
spawn f(ch)
// ...
}
オブジェクトはアロー演算子を使ってチャンネルに押し込むことができます。同じ演算子で他方からオブジェクトを取り出すことができます。
// バッファリングされたチャンネルを作り、(バッファに余裕がある場合) push がブロックされないようにする
ch := chan int{cap: 1}
ch2 := chan f64{cap: 1}
n := 5
// push
ch <- n
ch2 <- 7.3
mut y := f64(0.0)
m := <-ch // 新しい変数を作って取り出す
y = <-ch2 // 既存の変数に取り出す
チャンネルを閉じると、それ以上オブジェクトを押し込めなくなります。それでも押し込もうとすると、実行時にパニックが発生します (select
と try_push()
は例外です - 以下を参照)。関連するチャンネルが閉じられバッファが空になっている場合は、取り出そうとすると即座に return
します。このような状況を処理するには、or
節を使用します (Option/Result のハンドリング を参照)。
ch := chan int{}
ch2 := chan f64{}
// ...
ch.close()
// ...
m := <-ch or {
println('channel has been closed')
}
// エラーの伝播
y := <-ch2 ?
チャンネルの select
select
コマンドを使うと、CPU に顕著な負荷をかけることなく複数のチャンネルを同時に監視することができます。これは match 命令のように、転送可能なステートメントのリストと関連する節で構成されています。
import time
fn main() {
mut c := chan f64{}
mut ch := chan f64{}
mut ch2 := chan f64{}
mut ch3 := chan f64{}
mut b := 0.0
c := 1.0
// ... ch/ch2 に送信する go スレッドのセットアップ
spawn fn (the_channel chan f64) {
time.sleep(5 * time.millisecond)
the_channel <- 1.0
}(ch)
spawn fn (the_channel chan f64) {
time.sleep(1 * time.millisecond)
the_channel <- 1.0
}(ch2)
spawn fn (the_channel chan f64) {
_ := <-the_channel
}(ch3)
//
select {
a := <-ch {
// `a` でなにかする
eprintln('> a: ${a}')
}
b = <-ch2 {
// 事前に宣言した `b` でなにかする
eprintln('> b: ${b}')
}
ch3 <- c {
// `c` が送られた場合になにかする
time.sleep(5 * time.millisecond)
eprintln('> c: ${c} was send on channel ch3')
}
500 * time.millisecond {
// どのチャンネルも準備状態にならずに 0.5 秒経過したらなにかする
eprintln('> more than 0.5s passed without a channel being ready')
}
}
}
タイムアウト節は任意です。省略した場合、select
は無制限に待機します。また、else { ... }
節を追加することで、select
が呼ばれた瞬間にチャネルの準備ができていない場合でもすぐに処理を進められます。else
と > timeout
を同時に両方定義することはできません。
select
コマンドは、すべてのチャンネルが閉じている場合に false
になる bool
型の式として利用できます。
if select {
ch <- a {
...
}
} {
// チャンネルが開いていた
} else {
// チャンネルが閉じられた
}
特殊なチャンネルの機能
特別な目的のために、いくつかの組み込みプロパティとメソッドがあります。
struct Abc {
x int
}
a := 2.13
ch := chan f64{}
res := ch.try_push(a) // `ch <- a` を試みる
println(res)
l := ch.len // キューの最大要素数
c := ch.cap // キューの最大長
is_closed := ch.closed // bool のフラグ - `ch` が閉じているかどうか
println(l)
println(c)
mut b := Abc{}
ch2 := chan Abc{}
res2 := ch2.try_pop(b) // `b = <-ch2` を試みる
try_push/pop()
メソッドは即座に .succeed
、.not_ready
、.closed
のいずれかの結果を返しますが、これはオブジェクトが転送されたかどうかや転送されなかった理由に依存します。これらのメソッドやプロパティを本番環境で使用することは推奨されません。これらのメソッドやプロパティに基づくアルゴリズムはしばしば競合状態に陥ります。代わりに select
を使用してください (上記の 構文と使用方法 及び チャンネルの select
を参照)。
共有オブジェクト
データは、コルーチンと呼び出し元のスレッドの間で shared
変数を介して交換できます。このような変数は共有変数として作成してからコルーチンに渡す必要があります。基底となる struct
は暗黙の mutex を保持します。これは rlock
を用いて読み込み専用のアクセス、lock
を用いて読み込み/書き込みのアクセスをロックできます。
struct St {
mut:
x int // 共有するデータ
}
fn (shared b St) g() {
lock b {
// b.x の読み込み/変更/書き込み
}
}
fn main() {
shared a := St{
x: 10
}
spawn a.g()
// ...
rlock a {
// a.x の読み込み
}
}
共有変数は構造体、配列、辞書配列のいずれかでなければなりません。
JSON
JSON はよく普及しているため、V では組み込みで直接これをサポートしています。
V は JSON のエンコードとデコードのためのコードを生成します。実行時リフレクションはありません。これはより良いパフォーマンスを発揮します。
JSON のデコード
import json
struct Foo {
x int
}
struct User {
// [required] 属性を追加すると、入力にそのフィールドが
// 存在しなければデコードに失敗します。
// [required] でないフィールドが存在しない場合では、数値では 0、
// 文字列では '' などのデフォルト値としてみなされ、
// デコードは失敗しません。
name string [required]
age int
// `skip` 属性を使用することで指定のフィールドをスキップできます
foo Foo [skip]
// フィールド名が JSON と異なる場合は、名前を指定できます
last_name string [json:lastName]
}
data := '{ "name": "Frodo", "lastName": "Baggins", "age": 25 }'
user := json.decode(User, data) or {
eprintln('Failed to decode json, error: ${err}')
return
}
println(user.name)
println(user.last_name)
println(user.age)
// JSON 配列もデコードできます
sfoos := '[{"x":123},{"x":456}]'
foos := json.decode([]Foo, sfoos)!
println(foos[0].x)
println(foos[1].x)
json.decode
関数は引数を 2 つ取ります。json.decode
関数の第 1 引数はデコード後に変換される型で、第 2 引数は JSON データの入った文字列です。
JSON のエンコード
import json
struct User {
name string
score i64
}
mut data := map[string]int{}
user := &User{
name: 'Pierre'
score: 1024
}
data['x'] = 42
data['y'] = 360
println(json.encode(data)) // {"x":42,"y":360}
println(json.encode(user)) // {"name":"Pierre","score":1024}
json モジュールは匿名構造体フィールドもサポートしており、構造が深い複雑な JSON api を取り扱うのに便利です。
テスト
アサーション
mut v := 2
foo(mut v)
assert v < 4
assert
文は式の評価結果が true
であることを確認します。アサーションに失敗すると、プログラムは中断します。アサーションはプログラムの整合性を検知するためだけに使用するべきです。アサーションに失敗すると 標準エラー に報告され、比較演算子 (<
や ==
) の両側の値を可能であれば出力します。これにより予期せぬ値を簡単に見つけることができます。assert
文はテスト用の関数だけでなくあらゆる関数内で使用できます。これは新機能の開発や確認したい不変条件の維持に役立つでしょう。
すべての assert
文は、プログラムを -prod
フラグでコンパイルするときに 取り除かれます。
メッセージ付きのアサーション
以下のように assert
文を書くと、アサーションに失敗したときにメッセージを表示することができます。このメッセージには、string を返すすべての式を用いることができます(文字列リテラル、string を返す関数、文字列内挿を用いたもの等)。
fn test_assertion_with_extra_message_failure() {
for i in 0 .. 100 {
assert i * 2 - 45 < 75 + 10, 'assertion failed for i: ${i}'
}
}
プログラムを停止しないアサーション
機能のプロトタイピングやテストの初期段階において、アサーションでプログラムが停止する代わりにその失敗の出力だけをしてほしいことがあります。これはそのようなアサーションが含まれる関数に [assert_continues]
タグを付けることでできます。例えば以下のプログラムを実行するとき、
[assert_continues]
fn abc(ii int) {
assert ii == 2
}
for i in 0 .. 4 {
abc(i)
}
は以下のような出力になります。
assert_continues_example.v:3: FAIL: fn main.abc: assert ii == 2
left value: ii = 0
right value: 2
assert_continues_example.v:3: FAIL: fn main.abc: assert ii == 2
left value: ii = 1
right value: 2
assert_continues_example.v:3: FAIL: fn main.abc: assert ii == 2
left value: ii = 3
right value: 2
V はコマンドラインフラグ -assert continues
をサポートしており、これは全てのアサーションの動作を全体的に、すべての関数に [assert_continues]
とタグ付けされているかのように変更します。
テストファイル
module main
fn hello() string {
return 'Hello world'
}
fn main() {
println(hello())
}
module main
fn test_hello() {
assert hello() == 'Hello world'
}
上記のテストを実行するには、v hello_test.v
を使います。これは関数 hello
が正しい出力を生成しているかどうかを確認するものです。V はこのファイル内のすべてのテスト関数を実行します。
すべての _test.v
ファイル (外部と内部の両方) は、それぞれ別個のプログラムとしてコンパイルされます。つまり、たくさんの _test.v
ファイルとそれぞれに多くのテストがあっても、.v
ファイルの他のコードのコンパイルには通常まったく影響しません。ただし v file_test.v
または v test .
を明示的に実行するときは影響することがあります。
- すべてのテスト関数は、名前が
_test.v
で終わるテストファイル内に存在する必要があります。 - テスト関数名は、実行する印として
test_
で始まる必要があります。 - 通常の関数もテストファイルで定義できますが、これは手動で呼び出す必要があります。その他のシンボルもテストファイルで定義できます。
- テストには外部と内部の 2 種類があります。
- 内部テストは、同モジュールの他すべての
.v
ファイルのようにモジュールを 宣言 しなければなりません。内部テストでは、同じモジュール内のプライベート関数を呼び出すことができます。 - 外部テストは、テストするモジュールを インポート しなければなりません。外部テストはモジュールのプライベートな関数/型にはアクセスできません。モジュールが提供する外部/公開 API のみをテストすることができます。
上の例では、test_hello
は内部テストで、プライベート関数 hello()
を呼び出すことができます。また、注意として、module main
は他のモジュールと同様に通常のモジュールなので、内部テストはメインプログラムの .v
ファイル内のプライベート関数もテストできます。
また、テストファイルに以下の特殊なテスト関数も定義できます。
-
testsuite_begin
は、他のすべてのテスト関数の前に実行されます。 -
testsuite_end
は、他のすべてのテスト関数の後に実行されます。
テスト関数がエラーの戻り値型を持つ場合、そこから伝播されたエラーはテストを失敗させます。
import strconv
fn test_atoi() ! {
assert strconv.atoi('1')! == 1
assert strconv.atoi('one')! == 1 // test will fail
}
テストの実行
個々のテストファイルに対してテスト関数を実行するには、v foo_test.v
を使います。
モジュール全体をテストするには、v test mymodule
とします。また、現在のフォルダ (およびサブフォルダ) 内のすべてをテストするには v test .
でもできます。オプション -stats
を渡すと、実行された個々のテストの詳細を見られます。
.v のソースファイルを含む追加のテストデータを、_test.v ファイルと同階層の testdata
フォルダに置くことができます。V のテストフレームワークは、そのようなフォルダを無視して実行するテストをスキャンします。これは、無効な V ソースコードを含む .v ファイルや、既知の失敗テストを含む上位階層の _test.v ファイルによって特定の方法/オプションで実行されるべきテストを置きたい場合に便利な機能です。
V コンパイラへのパスは @VEXE で取得できるので、_test.v ファイルでは以下ように他のテストファイルを簡単に実行できます。
import os
fn test_subtest() {
res := os.execute('${@VEXE} other_test.v')
assert res.exit_code == 1
assert res.output.contains('other_test.v does not exist')
}
メモリ管理
V は、値型や文字列バッファを使用することで、そもそもの不要なアロケーションを避け、シンプルで抽象のないコードスタイルを推奨しています。
V でのメモリ管理には 4 つの選択肢があります。
既定はミニマルでよく振る舞うトレーシング GC です。
2 つ目の手段は autofree で、これは -autofree
で有効化できます。これはほとんどのオブジェクト (~90-100%) の面倒を見ます。コンパイラがコンパイル時に自動で必要な解放呼び出しを挿入します。残りの数%のオブジェクトは GC によって解放されます。デベロッパーがコードを変更する日強はありません。すべてを追跡する重い GC や高コストな各オブジェクトの参照カウントなしに、Python、Go、Java のように「そのまま動きます」。
より低水準な制御をしたいデベロッパーの方は、-gc none
とすることでメモリを手動管理できます。
アリーナ型アロケーションは V の -prealloc
で利用できます。
制御
V の autofree エンジンを利用して、カスタムデータ型に free()
メソッドを定義できます。
struct MyType {}
[unsafe]
fn (data &MyType) free() {
// ...
}
コンパイラが C のデータ型を C の free()
で解放するのと同じように、データ型ごとの free()
呼び出しを各変数の寿命の終わりに静的に挿入します。
よりローレベルでの制御を望む開発者のために、-manualfree
で自動解放を無効にしたり、手動メモリを管理したい各関数に[manualfree]
を追加したりできます (属性 を参照)。
Autofree はまだ WIP です。安定化でき次第デフォルトとなるため、この属性は使用を避けてください。現時点では、V の autofree エンジンが実用レベルになるまで、小さくてよく動作する GC によりアロケーション制御されています。
例
import strings
fn draw_text(s string, x int, y int) {
// ...
}
fn draw_scene() {
// ...
name1 := 'abc'
name2 := 'def ghi'
draw_text('hello ${name1}', 10, 10)
draw_text('hello ${name2}', 100, 10)
draw_text(strings.repeat(`X`, 10000), 10, 50)
// ...
}
文字列は draw_text
より外に出ていないので、これらは関数を抜けるときに解放されます。
実際に最初の 2 つの呼び出しはすべて、結果的にメモリ確保をしていません。これら 2 つの文字列は小さいので、V はこれらのために事前確保したバッファを使用します。
struct User {
name string
}
fn test() []int {
number := 7 // スタック変数
user := User{} // スタック上に確保された構造体
numbers := [1, 2, 3] // ヒープ上に確保された配列、関数を抜けるときに解放される
println(number)
println(user)
println(numbers)
numbers2 := [4, 5, 6] // 配列は返されるので、ここでは解放されない
return numbers2
}
スタックとヒープ
スタックとヒープの基礎
他の多くのプログラミング言語と同様に、データを保存できる場所が 2 つあります。
- スタック は、管理上のオーバーヘッドをほぼゼロにして、高速な割り当てを可能にします。スタックは関数の呼び出し回数に応じて大きくなったり小さくなったりします。したがって、呼び出された関数はすべて、その関数が戻るまで有効なスタックセグメントを持ちます。解放する必要はありませんが、このことは、スタックオブジェクトへの参照が関数の戻り時に無効になることを意味します。さらに、スタックスペースには制限があります (通常、1 スレッドあたり数メガバイト)。
- ヒープ は、オペレーティングシステムによって管理される大規模なメモリ領域 (通常は数ギガバイト) です。ヒープオブジェクトは、管理タスクを OS に委ねる特別な関数コールによって割り当てられ、解放されます。そのため、数回の関数呼び出しを経ても有効な状態を保つことができますが、管理にはコストがかかります。
V の既定の挙動
パフォーマンスを考慮して、V は可能な限りオブジェクトをスタックに置くようにしていますが、明確に必要な場合はヒープに割り当てます。以下はその例です。
struct MyStruct {
n int
}
struct RefStruct {
r &MyStruct
}
fn main() {
q, w := f()
println('q: ${q.r.n}, w: ${w.n}')
}
fn f() (RefStruct, &MyStruct) {
a := MyStruct{
n: 1
}
b := MyStruct{
n: 2
}
c := MyStruct{
n: 3
}
e := RefStruct{
r: &b
}
x := a.n + c.n
println('x: ${x}')
return e, &c
}
ここで、a
はそのアドレスが関数 f()
から離れることがないので、スタックに格納されます。しかし、b
への参照は返される e
の一部です。また、c
への参照も返されます。これにより、b
と c
はヒープに割り当てられます。
オブジェクトへの参照が関数の引数として渡される場合には、事態はより明白になります.
struct MyStruct {
mut:
n int
}
fn main() {
mut q := MyStruct{
n: 7
}
w := MyStruct{
n: 13
}
x := q.f(&w) // `q` と `w` の参照が渡される
println('q: ${q}\nx: ${x}')
}
fn (mut a MyStruct) f(b &MyStruct) int {
a.n += b.n
x := a.n * b.n
return x
}
ここでは、 q.f(&w)
という呼び出しが q
と w
への参照を渡しています。これは f()
の宣言で a
が mut
で b
が &MyStruct
型であるためで、技術的にはこれらの参照は main()
から離れることになります。しかし、これらの参照の ライフタイム は main()
のスコープ内にあるので、 q
と w
はスタック上に割り当てられます。
スタックとヒープの手動制御
先程の例で V コンパイラが q
と w
をスタックに置くことができたのは、q.f(&w)
の呼び出しにおいて、これら参照が値を読み込んだり変更したりするためにのみ使用され、参照自体をどこか他の場所に渡すためには使用されないと仮定したからです。これは、q
と w
への参照が f()
にのみ借用されるという形で見ることができます。
f()
が参照そのものを使って何かをしている場合には、状況が変わってきます。
struct RefStruct {
mut:
r &MyStruct
}
// 下記の議論を参照
[heap]
struct MyStruct {
n int
}
fn main() {
m := MyStruct{}
mut r := RefStruct{
r: &m
}
r.g()
println('r: ${r}')
}
fn (mut r RefStruct) g() {
s := MyStruct{
n: 7
}
r.f(&s) // `r` 内で `s` への参照を `main()` へ戻して渡す
}
fn (mut r RefStruct) f(s &MyStruct) {
r.r = s // `[heap]` なしだとエラーを起こす
}
ここでは f()
は何の変哲もないように見えますが、厄介なことをしています。この問題は、s
は g()
が実行されている間だけ生きていますが、r
はその後 main()
で使用されるということです。このため、コンパイラは f()
での代入について、s
が 「スタックに格納されているオブジェクトを参照している可能性がある」 という理由で文句を言います。g()
における r.f(&s)
の呼び出しは s
への参照を借用するだけである、という仮定が間違っています。
このジレンマを解決する方法として、struct MyStruct
の宣言に [heap]
属性を設定する方法があります。これはコンパイラに、MyStruct
オブジェクトを常にヒープ上に確保するように指示します。こうすることで、g()
が戻ってきた後も s
への参照が有効になります。コンパイラは f()
をチェックする際に MyStruct
オブジェクトが常にヒープ上に確保されていることを考慮して、s
への参照を r.r
フィールドへ代入することを許可します。
以下のような他のプログラミング言語でよく見られるパターンがあります。
fn (mut a MyStruct) f() &MyStruct {
// a でなにかする
return &a // 借用したオブジェクトのアドレスを返す
}
ここでは、f()
はレシーバとして参照 a
を渡され、それが呼び出し側に返されると同時に結果として戻ります。このような宣言は、y = x.f().g()
のようなメソッドチェーンを意図しています。しかし、この方法には a
に対する 2 つ目の参照 a
が作成されるという問題点があります。つまり、単なる借用にできないため MyStruct
を [heap]
として宣言しなければなりません。
V では、以下のようなより良いアプローチを取ります。
struct MyStruct {
mut:
n int
}
fn (mut a MyStruct) f() {
// `a` でなにかをする
}
fn (mut a MyStruct) g() {
// `a` で他のなにかをする
}
fn main() {
x := MyStruct{} // スタックに確保
mut y := x
y.f()
y.g()
// `mut y := x.f().g() の代わり
}
このように [heap]
属性を回避でき、結果的にパフォーマンスが向上します。
しかし、上述のようにスタックスペースは非常に限られています。よって、上述のようなユースケースでは必要ないとしても、非常に大きな構造体には [heap]
属性が適しているかもしれません。
ケースバイケースで割り当てを手動制御する方法もあります。この方法は推奨されていませんが、念のためにここで紹介します。
struct MyStruct {
n int
}
struct RefStruct {
mut:
r &MyStruct
}
// シンプルな関数 - g() が使うスタックセグメントの上書き
fn use_stack() {
x := 7.5
y := 3.25
z := x + y
println('${x} ${y} ${z}')
}
fn main() {
m := MyStruct{}
mut r := RefStruct{
r: &m
}
r.g()
use_stack() // 無効なスタックの中身を消去する
println('r: ${r}')
}
fn (mut r RefStruct) g() {
s := &MyStruct{ // `s` がヒープのオブジェクトを参照すると明示
n: 7
}
// 上書きされるスタックセグメント内のデータを見には
// 上を `&MyStruct` -> `MyStruct`、下を `r.f(s)` -> `r.f(&s)` に変更
r.f(s)
}
fn (mut r RefStruct) f(s &MyStruct) {
r.r = unsafe { s } // コンパイラのチェックを回避
}
ここでは、コンパイラのチェックは unsafe
ブロックによって抑制されています。[heap]
属性がなくても s
をヒープに割り当てるようにするために、構造体リテラルの前にアンパサンドを付けています。
この最後のステップはコンパイラでは必要ありませんが、これがないと r
内部の参照が無効になり (指されたメモリ領域が use_stack()
で上書きされてしまう)、プログラムがクラッシュする (あるいは少なくとも予測できない最終出力を生成する) 可能性があります。これが、この方法が安全でなく、避けるべき理由です。
ORM
(これはまだアルファ段階です)
V は組み込みの ORM サポートがあります。Postgres をサポートしており、まもなく MySQL と SQLite もサポートされます。
このアプローチには以下のようなメリットがあります。
- すべての SQL 方言を一つの構文で。(異なるデータベースの統合がより簡単になります。)
- クエリは V の構文で構築します。(他の構文を学ぶ必要はありません。)
- 安全性。(もう SQL クエリの構築でインジェクションはありえません。)
- コンパイル時チェック。(実行時にしか現れない打ち間違いがなくなります。)
- 読みやすくシンプルに。(もう結果を手動でパースしたりオブジェクトを構築する必要はありません。)
import db.sqlite
// カスタムのテーブル名を設定します。デフォルトは構造体の名前 (大文字小文字はそのまま) です。
[table: 'customers']
struct Customer {
id int [primary; sql: serial] // `id` という名前の整数型のフィールドが最初に存在しなければなりません
name string [nonull]
nr_orders int
country string [nonull]
}
db := sqlite.connect('customers.db')!
// 構造体宣言からテーブルを作成します。例えば次のクエリはこれと同様の SQL を発行します。
// CREATE TABLE IF NOT EXISTS `Customer` (
// `id` INTEGER PRIMARY KEY,
// `name` TEXT NOT NULL,
// `nr_orders` INTEGER,
// `country` TEXT NOT NULL
// )
sql db {
create table Customer
}!
// 新しい顧客情報を挿入します:
new_customer := Customer{
name: 'Bob'
country: 'uk'
nr_orders: 10
}
sql db {
insert new_customer into Customer
}!
us_customer := Customer{
name: 'Martin'
country: 'us'
nr_orders: 5
}
sql db {
insert us_customer into Customer
}!
// 顧客情報を更新します:
sql db {
update Customer set nr_orders = nr_orders + 1 where name == 'Bob'
}!
// select count(*) from customers
nr_customers := sql db {
select count from Customer
}!
println('number of all customers: ${nr_customers}')
// V の構文はクエリの構築にも利用できます
uk_customers := sql db {
select from Customer where country == 'uk' && nr_orders > 0
}!
println('We found a total of ${uk_customers.len} customers, that match the query.')
for customer in uk_customers {
println('${customer.id} - ${customer.name}')
println('customer: ${customer.id}, ${customer.name}, ${customer.country}, ${customer.nr_orders}')
}
// 顧客情報を削除します
sql db {
delete from Customer where name == 'Bob'
}!
他のサンプルについては、vlib/orm をご参照ください。
ドキュメントを書く
やり方は Go と非常に似通っており、とてもシンプルです。コードにドキュメントを書く必要がなければ、vdoc がソースコードから生成します。関数/型/定数のドキュメントはこのように宣言の前に正しく配置されなければなりません。
// clearall は配列の全ビットを消去します
fn clearall() {
}
そのコメントは定義される名前から始まる必要があります。
時々、その関数が何をするのかを一行だけで説明するのは不十分かもしれません。その場合は一行コメントを連ねるとよいでしょう。
// copy_all は再帰的に配列内の値をすべてコピーし、
// `dupes` が false ならば重複した値は処理中に除去します。
fn copy_all(dupes bool) {
// ...
}
利便性のため、コメントの時制は現在形にすることが好まれます。
モジュールの概要は、モジュール名の直後の最初のコメントに配置する必要があります。
ドキュメントを生成するには vdoc
を、例えば v doc net.http
のように実行します。
ドキュメントコメント内での改行
複数行に連ねたコメントは、以下の条件のいずれかを満たさない限り、スペースを入れて 1 つの行へと結合されます。
- 行が空である
- 行が
.
で終わる (文末) - 行が
-
、=
、_
、*
、~
のうちの一種類を 3 文字以上並べたものである (水平線) - 行が 1 つ以上の
#
とスペースで始まる (見出し) - 行が
|
に始まり|
で終わる (表) - 行が
-
で始まる (リスト)
ツール
vfmt
もうコードフォーマットやスタイルガイドラインを気にする必要はありません。vfmt
はこれだけでうまくやってくれます。
v fmt file.v
vfmt が保存するたびに実行されるように、エディタをセットアップすることを推奨します。vfmt の実行はとても速い (30ms 未満) です。
コードをプッシュする前に、常に v fmt -w file.v
を実行しましょう。
局所的なフォーマットの無効化
数行のコードブロックに対してフォーマットを無効化するには、コードを // vfmt off
と // vfmt on
で囲みます。
// vfmt の影響を受けなくなります
// vfmt off
... your code here ...
// vfmt on
// vfmt の影響を受けるようになります
... your code here ...
v シェーダー
V のグラフィカルアプリでは GPU シェーダーを利用できます。注釈付き GLSL 方言 で書かれたシェーダーを, v shader
を使ってすべてのサポートされているターゲットプラットフォーム向けにコンパイルできます。
v shader /path/to/project/dir/or/file.v
現在はコード内でシェーダーを使用する前に、ヘッダーをインクルードしてグルー関数を宣言する 必要があります。
プロファイリング
v -profile profile.txt run file.v
を実行すると、profile.txt
ファイルが生成され、これを分析できます。
生成された Pprofile.txtファイル`には、次の 4 つの列を持つ行が含まれています。
- 関数が呼び出された回数
- 関数での総時間 (単位:ms)
- 関数呼び出しにかかった平均時間 (単位:ns)
- v の関数名
sort -n -k3 profile.txt|tail
で、3 列目 (関数ごとの平均時間) をソートできます。
以下のように、ストップウォッチを使用してコードの一部だけを明示的に測定することもできます。
import time
fn main() {
sw := time.new_stopwatch({})
println('Hello world')
println('Greeting the world took: ${sw.elapsed().nanoseconds()}ns')
}
パッケージの管理
V のモジュールは .v ファイルが中に含まれている1つのフォルダです。V のパッケージは1つまたは複数の V のモジュールを含めることができます。V のパッケージはトップレベルフォルダの中にパッケージについての説明を書くための v.mod
ファイルを含む必要があります。
V のパッケージは通常 ~/.vmodules
フォルダにインストールされます。インストールされる場所は VMODULES
環境変数を設定することで上書きすることができます。
パッケージのコマンド
パッケージの管理には、V のコマンドを用いることができます。
v [モジュールのオプション] [引数]
モジュールのオプション:
install VPM からモジュールをインストールする。
remove VPM からインストールしたモジュールを取り除く。
search VPM からモジュールを検索する。
update VPM からインストールしたモジュールを更新する。
upgrade すべての outdated なモジュールを更新する。
list すべてのインストールしたモジュールを列挙する。
outdated インストールしたモジュールのうち更新が必要なものを表示する。
他人が作成したモジュールを VPM でインストールすることもできます。
v install [module]
例
v install ui
モジュールは git や mercurial のレポジトリから直接インストールすることもできます。
v install [--once] [--git|--hg] [url]
例
v install --git https://github.com/vlang/markdown
インストールされていない依存関係 のみ をインストールしたい場合は以下のようにします。
v install --once [module]
v でモジュールを取り除くには以下のようにします。
v remove [module]
例
v remove ui
VPM からインストールしたモジュールを更新するには以下のようにします。
v update [module]
例
v update ui
または以下のようにすべてのモジュールを更新できます。
v update
インストールしたモジュールをすべて表示するには、以下を使用します。
v list
``
#### 例
```sh
> v list
Installed modules:
markdown
ui
更新が必要なすべてのモジュールを見るには、以下のようにします。
v outdated
例
> v outdated
Modules are up to date.
パッケージの公開
-
v.mod
ファイルをモジュールのトップレベルフォルダ内に置いてください (v new mymodule
またはv init
コマンドでモジュールを作成した場合は、すでにv.mod
ファイルがあります)。v new mymodule Input your project description: My nice module. Input your project version: (0.0.0) 0.0.1 Input your project license: (MIT) Initialising ... Complete!
v.mod
の例:Module { name: 'mymodule' description: 'My nice module.' version: '0.0.1' license: 'MIT' dependencies: [] }
ファイルの最小構成:
v.mod mymodule.v
そのモジュール名が
mymodule.v
で使用されていることを確認します。module mymodule pub fn hello_world() { println('Hello World!') }
-
v.mod
ファイルのあるフォルダに git リポジトリを作成します (v new
やv init
を使用した場合は不要です)。git init git add . git commit -m "INIT"
-
github.com 上に公開レポジトリを作成します。
-
ローカルリポジトリとリモートリポジトリを接続し、変更をプッシュします。
-
パブリック V モジュールレジストリ VPM: https://vpm.vlang.io/new にモジュールを追加します。
モジュールを登録するには、GitHub アカウントでログインする必要があります。警告: 現在、投稿後にエントリーを編集できません。モジュール名と github の URL は後から変更できないので、ダブルチェックしてください。
-
最終的なモジュール名は、github アカウントと、指定のモジュール名を組み合わせたものになります。
任意: V モジュールに vlang
と vlang-module
のタグを付けると、github.com で検索しやすくなります。
発展的なトピック
属性
Vには、関数や構造体の動作を変更する属性がいくつかあります。
属性は、関数/構造体の宣言の直前の []
内で指定するとその定義にのみ適用されます。
// [flag] は列挙型をビットフィールドとして使えるようにします
[flag]
enum BitField {
read
write
other
}
fn main() {
assert 1 == int(BitField.read)
assert 2 == int(BitField.write)
mut bf := BitField.read
assert bf.has(.read | .other) // フラグのうち *少なくとも 1 つ* がセットされているかテスト
assert !bf.all(.read | .other) // フラグのうち *全て* がセットされているかテスト
bf.set(.write | .other)
assert bf.has(.read | .write | .other)
assert bf.all(.read | .write | .other)
bf.toggle(.other)
assert bf == BitField.read | .write
assert bf.all(.read | .write)
assert !bf.has(.other)
}
構造体のフィールドの非推奨:
module abc
// 注意として *他のモジュール* で Xyz.d に *直接* アクセスするときのみ、非推奨の通知/警告を生成します。
pub struct Xyz {
pub mut:
a int
d int [deprecated: 'use Xyz.a instead'; deprecated_after: '2999-03-01'] // 注意と, かなり先の非推奨の日付を生成します
}
関数とメソッドの非推奨:
// この関数を呼び出すと非推奨の警告が出ます
[deprecated]
fn old_function() {
}
// カスタムの非推奨メッセージも表示できます
[deprecated: 'use new_function() instead']
fn legacy_function() {}
// また、日付を指定することもでき、その日付以降、
// その関数は廃止されたとみなされます。その日より前での
// その関数の呼び出しは、コンパイラ通知として表示されますが、
// コンパイルには影響しません。その日以降は呼び出しが警告に
// なるため、通常のコンパイルは動作しますが、-prod での
// コンパイルは動作しません (-prod ではすべての警告が
// エラーとして扱われます)。非推奨日から 6 ヶ月後には、
// 呼び出しはハードコンパイラエラーになります。
[deprecated: 'use new_function2() instead']
[deprecated_after: '2021-05-27']
fn legacy_function2() {}
// この関数の呼び出しはインライン化されます。
[inline]
fn inlined_function() {
}
// この関数の呼び出しはインライン化されません。
[noinline]
fn function() {
}
// この関数は呼び出し元に戻りません。
// このような関数は、exit/1 や panic/1 のように、or ブロックの最後に使用することができます。 このような関数は戻り値を持つことができず、for{} で終わるか、他の `[noteturn]` 関数を呼び出して終了しなければなりません。
[noreturn]
fn forever() {
for {}
}
// 次の構造体はヒープ上に割り当てられなければなりません。そのため、参照 (`&Window`) として、または別の参照 (`&OuterStruct{ Window{...} }`) の内部でのみ使用できます。
// 「スタックとヒープ」の章もご覧ください。
[heap]
struct Window {
}
// フラグが false になっていると、この関数とその呼び出しのコードを生成しなくなります。
// フラグを使うには、`v -d flag` のようにします
[if debug]
fn foo() { }
fn bar() {
foo() // `-d debug` が渡されていないと呼び出されない
}
// この関数のポインタ引数が指すメモリは、この関数が戻る前にガベージコレクタによって
// 解放されません (使用されている場合)。
[keep_args_alive]
fn C.my_external_function(voidptr, int, voidptr) int
// 以下の関数は unsafe{} ブロック内で呼び出さなければなりません。
// なお、`risky_business()` 本体のコードは、`unsafe {}` ブロックで
// 包まない限りチェックされます。
// これは、ある安全ではない操作の前後にチェックを行う `[unsafe]` 関数を
// 作りたいときに便利で、V の安全機能の恩恵も受けられます。
[unsafe]
fn risky_business() {
// チェックコード、おそらく事前条件のチェック
unsafe {
// ポインタ演算、ユニオンフィールドへのアクセス、他の `[unsafe]` 関数の呼び出し
// などがチェック *されない* コードです。
// 通常は、unsafe{} で囲まれたコードをできるだけ少なくするようにするのがよいでしょう。
// [メモリ安全でないコード](#メモリ安全でないコード) もご覧ください
}
// チェックコード、おそらく事後条件のチェックや不変性の保持
}
// V の autofree エンジンはこの関数でメモリ管理を行いません。
// あなたは、その中でメモリの解放を手動で行う責任があります。
[manualfree]
fn custom_allocations() {
}
// C との相互運用向けのみですが,この構造体が C 内で `typedef struct` によって定義されていると V に伝えます。
[typedef]
struct C.Foo {
}
// 関数へのカスタム呼び出し規約を追加するために用いられ、stdcall、fastcall、cdecl の呼び出し規約が利用できます。
// このリストは型エイリアス (下記) においても同様です。
[callconv: "stdcall"]
fn C.DefWindowProc(hwnd int, msg int, lparam int, wparam int)
// 関数の型エイリアスに呼び出し規約を追加するために用いられます。
[callconv: "fastcall"]
type FastFn = fn (int) bool
// Windows 専用:
// デフォルトのグラフィックライブラリ (gg、ui など) がインポートされている場合、グラフィカルウィンドウ
// が優先され、コンソールウインドウは作成されず、println() ステートメントが効果的に無効になります。
// コンソールウインドウを明示的に作成する際にこれを使用します。main() の前でのみ有効です。
[console]
fn main() {
}
条件付きコンパイル
コンパイル時疑似変数
V はまた、コンパイル時に置換される疑似文字列変数セットへのアクセスも提供します。
-
@FN
=> 現在の V 関数の名前に置換 -
@MOD
=> 現在の V モジュールの名前に置換 -
@STRUCT
=> 現在の V 構造体の名前に置換 -
@FILE
=> この V ソースファイルのパスに置換 -
@LINE
=> これが (文字列としての) ソースファイルに表れている V 行数に置換 -
@COLUMN
=> これが (文字列としての) ソースファイルに表れているその行での文字数に置換 -
@VEXE
=> V コンパイラへのパスに置換 -
@VHASH
=> V コンパイラの短縮コミットハッシュ (文字列) に置換 -
@VMOD_FILE
=> 最も近い位置の v.mod ファイルの内容 (文字列) に置換
これらによって以下の例のようなことができ、コードのデバッグ/ログ/トレースに便利です。
eprintln( 'file: ' + @FILE + ' | line: ' + @LINE + ' | fn: ' + @MOD + '.' + @FN)
以下に別の例として、v.mod のバージョン/名前を実行ファイルの中に埋め込みたい場合を示します。
import v.vmod
vm := vmod.decode( @VMOD_FILE ) or { panic(err) }
eprintln('${vm.name} ${vm.version}\n ${vm.description}')
コンパイル時リフレクション
$
はコンパイル時 (あるいは「comptime」) の操作の接頭辞として用います。
組み込み JSON サポートはありますが、V では以下のように任意のものに対する効果的なシリアライザを作成することもできます。V のコンパイル時 if
と for
により以下のようなものが作成できます。
struct User {
name string
age int
}
fn main() {
$for field in User.fields {
$if field.typ is string {
println('$field.name is of type string')
}
}
}
// 出力:
// name is of type string
より具体的なサンプルは examples/compiletime/reflection.v
をご覧ください。
コンパイル時のコード
$
はコンパイル時操作の接頭辞として使われます。
$if
条件
// 一つの節で複数の条件をサポート
$if ios || android {
println('Running on a mobile device!')
}
$if linux && x64 {
println('64-bit Linux.')
}
// 式として利用
os := $if windows { 'Windows' } $else { 'UNIX' }
println('Using $os')
// $else-$if 節
$if tinyc {
println('tinyc')
} $else $if clang {
println('clang')
} $else $if gcc {
println('gcc')
} $else {
println('different compiler')
}
$if test {
println('testing')
}
// v -cg ...
$if debug {
println('debugging')
}
// v -d option ...
$if option ? {
println('custom option')
}
コンパイル時に評価される分岐をしたい場合、$
が前に付いた if
を使います。現在は OS または -debug
コンパイラ引数の検出にしか使用できません。
コンパイル時に if
を評価したい場合は、前に $
を付ける必要があります。現在は、OS、コンパイラ、プラットフォーム、コンパイルオプションを検出するために使われています。$if debug
は $if windows
や $if x32
のような特別なオプションです。自作の ifdef
を使うには、$if option ? {}
と書いて、v -d option
のようにしてコンパイルする必要があります。以下はビルトインオプションの完全なリストです。
OS | コンパイラ | プラットフォーム | その他 |
---|---|---|---|
windows , linux , macos
|
gcc , tinyc
|
amd64 , arm64 , aarch64
|
debug , prod , test
|
mac , darwin , ios , |
clang , mingw
|
i386 , arm32
|
js , glibc , prealloc
|
android , mach , dragonfly
|
msvc |
x64 , x32
|
no_bounds_checking , freestanding
|
gnu , hpux , haiku , qnx
|
cplusplus |
little_endian , big_endian
|
no_segfault_handler , no_backtrace
|
solaris , termux
|
no_main , 'fast_math' |
$embed_file
import os
fn main() {
embedded_file := $embed_file('v.png')
os.write_file('exported.png', embedded_file.to_string())!
}
V ではコンパイル時に $embed_file(<path>)
を呼び出して任意のファイルを実行ファイルに埋め込むことができます。パスはソースファイルに対する絶対パスまたは相対パスを指定できます。
注意として既定では、$embed_file(file)
を使用すると常にファイル内容全体が埋め込まれますが、プログラムのコンパイル時に -d embed_only_metadata
を渡すことでその動作を変更できます。 その場合、ファイルは埋め込まれません。 代わりに、実行時にプログラムが embedded_file.data()
を呼び出したときに 初めて ロードされるため、プログラムを再コンパイルする必要がなくなり、外部エディタープログラムでの変更が容易になります。
実行可能ファイル内にファイルを埋め込むと、ファイルのサイズは大きくなりますが、より自己完結型になるため、配布が容易になります。この (既定の) 場合、embedded_file.data()
は IO をせず、常に同じデータを返します。
$embed_file
は -prod
でコンパイルするときに埋め込んだファイルの圧縮をサポートしています。現在サポートされている圧縮方式は zlib
のみです。
import os
fn main() {
embedded_file := $embed_file('x.css', .zlib) // compressed using zlib
os.write_file('exported.css', embedded_file.to_string())!
}
注: png ファイルや zip ファイルなどのバイナリアセットを圧縮しても、通常はあまり効果がありません。これらはすでに圧縮されているため、最終的な実行形式がより多くの容量を必要としてしまう場合があります。
$embed_file
は EmbedFileData を返しま。これはファイル内容を string
や []u8
として取得できます。
$tmpl
で V テンプレートファイルの埋め込みとパース
V にはテキストや html テンプレート用のシンプルなテンプレート言語があります。これは、$tmpl('path/to/template.txt')
で簡単に埋め込むことができます。
fn build() string {
name := 'Peter'
age := 25
numbers := [1, 2, 3]
return $tmpl('1.txt')
}
fn main() {
println(build())
}
name: @name
age: @age
numbers: @numbers
@for number in numbers
@number
@end
出力:
name: Peter
age: 25
numbers: [1, 2, 3]
1
2
3
$env
module main
fn main() {
compile_time_env := $env('ENV_VAR')
println(compile_time_env)
}
V はコンパイル時に環境変数から値を取り込むことができます。$env('ENV_VAR')
は、#flag linux -I $env('JAVA_HOME')/include
のようにトップレベルの #flag
や #include
文でも使用できます。
$compile_error
と $compile_warn
これら 2 つのコンパイル時関数は、カスタムのエラー/警告をコンパイル時に表示するときに有用です。
どちらも表示するメッセージが入っている文字列リテラル 1 つのみを引数として受け取ります。
module main
$if linux {
$compile_error('Linux is not supported')
}
fn main() {
}
$ v run x.v
x.v:4:5: error: Linux is not supported
2 |
3 | $if linux {
4 | $compile_error('Linux is not supported')
| ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
5 | }
6 |
コンパイル時型
コンパイル時型は、複数の型をより一般的な上位レベルの型にグループ化します。これは、入力型に特定のプロパティ (配列の .len
属性など) が必要な、ジェネリック引数を持つ関数で役立ちます。
V は以下のコンパイル時型をサポートします。
-
$alias
=> 型エイリアス にマッチします。 -
$array
=> 配列 と 固定長配列 にマッチします。 -
$enum
=> 列挙体 にマッチします。 -
$float
=>f32
、f64
と浮動小数点数リテラルにマッチします。 -
$function
=> 関数型 にマッチします。 -
$int
=>int
,i8
,i16
,i32
,i64
,u8
,u16
,u32
,u64
,isize
,usize
と整数リテラルにマッチします。 -
$interface
=> インターフェイス にマッチします。 -
$map
=> 辞書配列 にマッチします。 -
$option
=> Option 型 にマッチします。 -
$struct
=> 構造体 にマッチします。 -
$sumtype
=> 直和型 にマッチします。
環境固有ファイル
ファイル名に環境固有指定のサフィックスが付いている場合は、その環境でのみコンパイルされます。
-
.js.v
=> JS バックエンドでのみ使用されます。これらのファイルには JS コードを含められます。 -
.c.v
=> C バックエンドでのみ使用されます。これらのファイルには C コードを含められます。 -
.native.v
=> V のネイティブバックエンドでのみ使用されます。 -
_nix.c.v
=> Unix システム (非 Windows) バックエンドでのみ使用されます。 -
_${os}.c.v
=> 特定 OS システムでのみ使用されます。例えば_windows.c.v
は Windows 上でコンパイルする場合または-os windows
でコンパイルする場合にのみ使用されます。 -
_default.c.v
=> 特定プラットフォームのファイルがない場合にのみ使用されます。例えばfile_linux.c.v
とfile_default.c.v
の両方があって Linux 向けにコンパイルしている場合、file_linux.c.v
だけが使用されてfile_default.c.v
は無視されます。
以下はより完全なサンプルです。
module main
fn main() { println(message) }
module main
const ( message = 'Hello world' )
module main
const ( message = 'Hello linux' )
module main
const ( message = 'Hello windows' )
上記のサンプルは以下のようになっています。
-
Windows 向けにコンパイルすると、'Hello windows' が
-
Linux 向けにコンパイルすると、'Hello linux' が
-
他のプラットフォーム向けにコンパイルすると、固有のものではない 'Hello world' メッセージが表示されます。
-
_d_customflag.v
=> これは$if customflag ? {}
に相当しますが、単一のブロックではなくファイル全体に対して使用されます。customflag
は snake_case の識別子でなければならず、任意の文字を含むことはできません (小文字のラテン文字 + 数字 +_
のみ)。注意:_d_customflag_linux.c.v
のような接尾子の組み合わせは動作しません。プラットフォーム依存のコードを含むカスタムフラグファイルが必要な場合は、_d_customflag.v
接尾子を使用し、その中で$if linux {}
などのようなプラットフォーム依存のコンパイル時分岐ブロックを使用します。
メモリ安全でないコード
効率化のために、メモリを破損させたりセキュリティ攻撃を受けたりしかねない低水準のコードを書きたくなることがあるでしょう。V はこのようなコードの記述をサポートしていますが、デフォルトではサポートされていません。
V では、メモリ安全性に問題がある可能性のある操作には意図的にマーキングすることを要求しています。また、これらの操作にマーキングすることで、コードを読んでいる人に、もし間違いがあった場合はメモリ安全性に違反しうると示すことができます。
メモリが安全でない可能性のある操作の例としては、以下のようなものがあります。
- ポインタ演算
- ポインタの配列扱い
- 非互換型からポインタへの変換
-
free
、strlen
、strncmp
などの C 関数の呼び出し
メモリが安全でない可能性のある操作をマークするには、unsafe
ブロックで囲みます。
// 未初期化バイトを 2 つ確保 & その参照を返す
mut p := unsafe { malloc(2) }
p[0] = `h` // エラー: ポインタの配列扱いは `unsafe` ブロック内でのみ可能
unsafe {
p[0] = `h` // OK
p[1] = `i`
}
p++ // エラー: ポインタ演算は `unsafe` ブロック内でのみ可能
unsafe {
p++ // OK
}
assert *p == `i`
ベストプラクティスとしては、unsafe
ブロックの中にメモリセーフな式を入れないようにし、unsafe
ブロックを使用する理由をできるだけ明確にすることです。一般的に、メモリ安全と思われるコードは、コンパイラがそれを検証できるように unsafe
ブロック内に入れないようにしましょう。
プログラムがメモリ安全性に違反していると思われる場合は、その原因を見つけるための足がかりとして unsafe
ブロック (とそれに接する周り) を見ればよいのです。
- 注: これは作業中です。
参照のフィールドを持つ構造体
参照を持つ構造体は、構造体がすでに独自の初期値を定義していない限り、初期値を明示的な参照値に設定する必要があります。
ゼロ値参照や nil ポインタは将来的にサポート しません が、今のところ参照フィールドは値 0 を使用できます。これに依存するリンクリストや二分木のようなデータ構造は、それが安全ではないことを理解し、パニックを引き起こす可能性があることを理解してください。
struct Node {
a &Node
b &Node = unsafe { nil } // 自動で nil に初期化されます、使用の際はご注意ください!
}
// 初期値が宣言されていない場合、参照フィールドは初期化が必要です。
// ゼロ (0) は OK ですが気をつけてください。これは nil ポインタです。
foo := Node{
a: 0
}
bar := Node{
a: &foo
}
baz := Node{
a: 0
b: 0
}
qux := Node{
a: &foo
b: &bar
}
println(baz)
println(qux)
sizeof と __offsetof
-
sizeof(型)
は型のサイズをバイト単位で与えます。 -
__offsetof(構造体, フィールド名)
は構造体のフィールドのオフセットをバイト単位で与えます。
struct Foo {
a int
b int
}
assert sizeof(Foo) == 8
assert __offsetof(Foo, a) == 0
assert __offsetof(Foo, b) == 4
制限付き演算子オーバーロード
struct Vec {
x int
y int
}
fn (a Vec) str() string {
return '{${a.x}, ${a.y}}'
}
fn (a Vec) + (b Vec) Vec {
return Vec {
a.x + b.x,
a.y + b.y
}
}
fn (a Vec) - (b Vec) Vec {
return Vec {
a.x - b.x,
a.y - b.y
}
}
fn main() {
a := Vec{2, 3}
b := Vec{4, 5}
println(a + b) // "{6, 8}"
println(a - b) // "{-2, -2}"
}
演算子オーバーロードは、V のシンプルさと可視性の哲学に反しています。しかし科学的かつ視覚的なアプリケーションは V の得意分野なので、演算子オーバーロードは以下のような可読性の向上のためにとても重要です。
a.add(b).add(c.mul(d))
は a + b + c * d
と比べて読みづらいですしね。
安全性と保守性を促進するため、演算子オーバーロードには以下のようないくつかの制限があります。
- オーバーロードできるのは
+, -, *, /, %, <, >, ==, !=, <=, >=
の演算子だけです。 -
==
と!=
はコンパイラによって自己生成されますが、オーバーライドできます。 - 演算子関数の中で他の関数を呼び出せません。
- 演算子関数の引数は変更できません。
- <
と
==演算子を使用する場合、戻り値の型は
bool` でなければなりません。 -
!=
、>
、<=
、>=
は、==
と<
が定義されているときに自動生成されます。 - 両方の引数は同じ型でなければなりません(V のすべての演算子と同じです)。
- 代入演算子 (
*=
、+=
、/=
など) は、演算子が定義されているときに自動生成されますが、同じ型を返さなければなりません。
パフォーマンスのチューニング
生成された C 言語のコードは、-prod
を使ってコンパイルすれば、通常は十分に高速です。しかし、状況によっては、C コンパイラに追加のヒントを与えて、コードブロックをさらに最適化できるようにしたい場合もあるでしょう。
注意: これはめったに必要なく、使うべきでもありません。プロファイリングした結果、大きなメリットがあることが確認できれば構いませんが。
gcc のドキュメントから引用すると、"プログラマは書いたプログラムが実際にどう動くのか予測しがたいものだ。"
[inline]
- 関数に [inline]
タグを付けられるので、C コンパイラはそれらをインライン化しようとします。大抵の場合パフォーマンスが改善しますが、実行形式のサイズは膨らみます。
[direct_array_access]
- [direct_array_access]
のタグを付けた関数では、コンパイラが配列操作を直接 C 言語の配列操作に変換します。これはその関数における配列の反復処理時間を大幅に節約できますが、その代償としてユーザが境界を確認しない限り関数を非安全にしてしまうことになります。
if _likely_(真偽値の式) {
は C コンパイラのヒントで、渡された真偽値の式が真である可能性が非常に高いことを示しています。JS バックエンドでは何もしません。
if _unlikely_(真偽値の式) {
は likely_(x)
に似ていますが、真偽値の式が偽である可能性が高いことを示唆しています。JS バックエンドでは何もしません。
アトミック
atomic_store()
のような C11 の標準的なアトミック関数は、通常、マクロや C コンパイラのマジックの助けを借りて定義され、オーバーロードされた C 関数のようなものを提供しています。V は意図的に関数のオーバーロードをサポートしていないので、V コンパイラのインフラストラクチャの一部である atomic.h
という名前の C ヘッダで定義されたラッパー関数があります。
すべての符号なし整数型とポインターに専用のラッパーがあります。例えば、C.atomic_load_ptr()
や C.atomic_fetch_add_u64()
のように、関数名には型名がサフィックスとして含まれています。
これらの関数を使用するには,使用 OS の C ヘッダをインクルードし,使用する関数を宣言する必要があります。以下は例です。
$if windows {
#include "@VEXEROOT/thirdparty/stdatomic/win/atomic.h"
} $else {
#include "@VEXEROOT/thirdparty/stdatomic/nix/atomic.h"
}
// 使いたい関数の宣言 - V は C ヘッダを解析しません
fn C.atomic_store_u32(&u32, u32)
fn C.atomic_load_u32(&u32) u32
fn C.atomic_compare_exchange_weak_u32(&u32, &u32, u32) bool
fn C.atomic_compare_exchange_strong_u32(&u32, &u32, u32) bool
const num_iterations = 10000000
// 下記の「グローバル変数」の章を参照
__global (
atom u32 // アトミックとして使用する普通の変数
)
fn change() int {
mut races_won_by_change := 0
for {
mut cmp := u32(17) // 比較対象となるアドレス値と、発見された値を格納する
// `if atom == 17 { atom = 23 races_won_by_change++ } else { cmp = atom }` のアトミック版
if C.atomic_compare_exchange_strong_u32(&atom, &cmp, 23) {
races_won_by_change++
} else {
if cmp == 31 {
break
}
cmp = 17 // atom の値が上書きされたので再代入する
}
}
return races_won_by_change
}
fn main() {
C.atomic_store_u32(&atom, 17)
t := go change()
mut races_won_by_main := 0
mut cmp17 := u32(17)
mut cmp23 := u32(23)
for i in 0 .. num_iterations {
//`if atom == 17 { atom = 23 races_won_by_main++ }` のアトミック版
if C.atomic_compare_exchange_strong_u32(&atom, &cmp17, 23) {
races_won_by_main++
} else {
cmp17 = 17
}
desir := if i == num_iterations - 1 { u32(31) } else { u32(17) }
// `for atom != 23 {} atom = desir` のアトミック版
for !C.atomic_compare_exchange_weak_u32(&atom, &cmp23, desir) {
cmp23 = 23
}
}
races_won_by_change := t.wait()
atom_new := C.atomic_load_u32(&atom)
println('atom: $atom_new, #exchanges: ${races_won_by_main + races_won_by_change}')
// `atom: 31, #exchanges: 10000000`) と出力する
println('races won by\n- `main()`: $races_won_by_main\n- `change()`: $races_won_by_change')
}
この例では、main()
と生成されたスレッド change()
の両方が、グローバル変数 atom
の 17
の値を 23
の値に置き換えようとします。逆方向への置換はちょうど 10000000 回行われます。最後の置換は 31
で行われ、生成されたスレッドは終了します。
どのスレッドで何回置換が行われるかは予測できませんが、合計は常に 10000000 になります (コメントにある非アトミック版では、この値はもっと大きくなるか、プログラムがハングアップします。これは使用するコンパイラの最適化に依存します)。
グローバル変数
V はデフォルトではグローバル変数を許可していません。しかし、ローレベルのアプリケーションではグローバル変数を使用する必要があるため、コンパイラフラグ -enable-globals
を使用してその使用を有効化できます。グローバル変数の宣言は、上記 の例のように、__global ( ... )
で囲まなければなりません。
グローバル変数の初期化子は,明示的に目的の型に変換しなければなりません。初期化子が指定されていない場合は,デフォルトの初期化が行われます。セマフォやミューテックスのようないくつかのオブジェクトは、明示的な初期化を必要とします。つまり、関数呼び出しから返される値ではなく、参照によるメソッド呼び出しで初期化されます。この目的のために、別の init()
関数を使用することができます。
import sync
__global (
sem sync.Semaphore // `init()` 内で初期化が必要
mtx sync.RwMutex // `init()` 内で初期化が必要
f1 = f64(34.0625) // 明示的に初期化
shmap shared map[string]f64 // 空の `shared` 辞書配列として初期化
f2 f64 // `0.0` に初期化
)
fn init() {
sem.init(0)
mtx.init()
}
マルチスレッドのアプリケーションでは、グローバル変数へのアクセスにレースコンディションが発生することに注意してください。この問題に対処するにはいくつかの方法があります。
- 変数の宣言には
shared
を使用し、アクセスにはlock
ブロックを使用します。この方法は、構造体や配列、マップなどの大きなオブジェクトに最適です。 - 特別な C 関数 (上記 参照) を使って、プリミティブなデータ型を「アトミック」として扱います。
- アクセスを制御するために、ミューテックスのような明示的な同期プリミティブを使用する。この場合、コンパイラは助けられないので、自分の行いを把握する必要があります。
- 気にしない - このアプローチは可能ですが、グローバル変数の正確な値が本当に重要ではない場合にのみ意味があります。例えば、
rand
モジュールでは、グローバル変数を使って(暗号ではない)疑似乱数を生成しています。この場合、データレースにより、異なるスレッドの乱数が多少相関することになりますが、同期化プリミティブを使用することによるパフォーマンスの低下を考慮すると、これは許容範囲内です。
クロスコンパイル
プロジェクトをクロスコンパイルするには、単に
v -os windows .
や
v -os linux .
を実行します。(macOS 向けのクロスコンパイルは現在は不可能です。)
C 依存ファイルがなければ、それだけなんです。これは、ui モジュールを使用した GUI アプリや、gg を使用したグラフィカルアプリをコンパイルするときも動作します。
Windows や Linux では、Clang、LLD リンカをインストールし、ライブラリとインクルードファイルの入った zip ファイルをダウンロードする必要があります。 V ではそのリンクを提供しています。
デバッグ
C バックエンドバイナリ (既定)
生成されたバイナリ (フラグ: -b c
) の問題をデバッグするには、以下のフラグを渡すことができます。
-
-g
- より多くのデバッグ情報を含む、最適化なしの実行ファイルを生成します。通常、V に -g が渡されると、パニック時に生成するスタックトレースに、.v ファイルの行番号を表示するよう実行ファイルに強制します。低レベルのコードを書いている場合には、次のオプション-cg
を使用してください。 - cg
- より多くのデバッグ情報を含む、最適化されていない実行ファイルを生成します。この場合、実行ファイルは C 言語のソース行番号を使用します。パニックが発生したときに生成された C プログラムを確認したり、デバッガ (
gdb、
lldbなど) で生成された C ソースコードを表示できるようにするために、
-keepc` と組み合わせてよく使われます。 -
-showcc
- プログラムのビルドに使用された C コマンドを表示します。 -
-show-c-output
- プログラムのコンパイル時に C コンパイラが生成した出力を表示します。 -
-keepc
- コンパイルが成功した後も、生成された C ソースコードファイルを削除しません。また、同じファイルパスを使い続けることで、より安定し、エディタや IDE で開いたままにしておくことができます。
よりよいデバッグをするには、v -cg -keep_c -pretty_c -show_c_cmd yourprogram.v
のように同時にすべて渡すとよいでしょう。そして、生成された実行形式に対してデバッガ (gdb/lldb) や IDE を実行するだけです。
生成された C 言語のコードを、それ以上コンパイルせずに検査したいだけならば、-o file.c
という方法もあります。こうすると、V は file.c
を生成して停止します。
main
のような単一の C 関数に対して生成された C のソースコードを見たい場合は、-printfn main -o file.c
としてください。
V の実行ファイル自体をデバッグするには、ソースから ./v -g -o v cmd/v
でコンパイルする必要があります。
テストをデバッグするには、例えば v -g -keepc prog_test.v
のようにします。-keepc
フラグが必要なのは、実行ファイルが作成されて実行された後、削除されないようにするためです。
V がサポートする全フラグの詳細なリストは、v help
、v help build
、v help build-c
を参照してください。
コマンドラインでのデバッグ
トラブルシューティング (デバッグ) は GDB で V が作成した実行ファイルを
視覚的なデバッグのセットアップ:
ネイティブバックエンドバイナリ
現在、ネイティブバックエンド (フラグ: -b native
) で作成されたバイナリのデバッグはサポートされていません。
JavaScript バックエンド
生成された JavaScript の出力をデバッグするために、ソースマップを有効化できます。v -b js -sourcemap hello.v -o hello.js
サポートされているすべてのオプションについては、最新のヘルプを確認してください。v help build-js
V と C
V から C 関数を呼び出す
例
#flag -lsqlite3
#include "sqlite3.h"
// さらなるサンプルは https://www.sqlite.org/quickstart.html を参照
struct C.sqlite3 {
}
struct C.sqlite3_stmt {
}
type FnSqlite3Callback = fn (voidptr, int, &&char, &&char) int
fn C.sqlite3_open(charptr, &&C.sqlite3) int
fn C.sqlite3_close(&C.sqlite3) int
fn C.sqlite3_column_int(stmt &C.sqlite3_stmt, n int) int
// ... さらに引数型を定義して C. 接頭詞の省略もできる
fn C.sqlite3_prepare_v2(&C.sqlite3, &char, int, &&C.sqlite3_stmt, &&char) int
fn C.sqlite3_step(&C.sqlite3_stmt)
fn C.sqlite3_finalize(&C.sqlite3_stmt)
fn C.sqlite3_exec(db &C.sqlite3, sql &char, cb FnSqlite3Callback, cb_arg voidptr, emsg &&char) int
fn C.sqlite3_free(voidptr)
fn my_callback(arg voidptr, howmany int, cvalues &&char, cnames &&char) int {
unsafe {
for i in 0 .. howmany {
print('| ${cstring_to_vstring(cnames[i])}: ${cstring_to_vstring(cvalues[i]):20} ')
}
}
println('|')
return 0
}
fn main() {
db := &C.sqlite3(unsafe { nil }) // `sqlite3* db = 0` と同義
// C 関数の呼び出しに文字列リテラルを渡すと、V 文字列ではなく C 文字列になります。
C.sqlite3_open('users.db', &db)
// C.sqlite3_open(db_path.str, &db)
query := 'select count(*) from users'
stmt := &C.sqlite3_stmt(unsafe { nil })
// 注意: V 文字列の `.str` フィールドを使用して、
// C 方式のゼロ終端表現の取得もできます。
C.sqlite3_prepare_v2(db, query.str, -1, &stmt, 0)
C.sqlite3_step(stmt)
nr_users := C.sqlite3_column_int(stmt, 0)
C.sqlite3_finalize(stmt)
println('There are $nr_users users in the database.')
//
error_msg := charptr(0)
query_all_users := 'select * from users'
rc := C.sqlite3_exec(db, query_all_users.str, my_callback, 7, &error_msg)
if rc != C.SQLITE_OK {
eprintln(cstring_to_vstring(error_msg))
C.sqlite3_free(error_msg)
}
C.sqlite3_close(db)
}
C から V を呼び出す
V は C にコンパイルできるので、C から V のコードを呼び出すのはとても簡単です。
デフォルトでは、すべての V の関数は、C では次のような命名法になっています: [モジュール名]__[fn名]
.
例えば、モジュール bar
で fn foo() {}
とすると、bar__foo()
となります。
カスタムのエクスポート名を使用するには,[export]
属性を使用します。
[export: 'my_custom_c_name']
fn foo() {
}
C コンパイラフラグを渡す
#flag
ディレクティブを V ファイルの始めに加えることで、以下のような C コンパイルフラグを指定できます。
-
-I
で C インクルードファイルの探索パスを追加 -
-l
でリンクしたい C ライブラリの名前を追加 -
-L
で C ライブラリファイルの探索パスを追加 -
-D
でコンパイル時変数の設定
ターゲットによって異なるフラグが使用できます。現在 linux
、darwin
、windows
をサポートしています。
注意: (現在は) 以下のように、一行につきフラグ一つを書く必要があります。
#flag linux -lsdl2
#flag linux -Ivig
#flag linux -DCIMGUI_DEFINE_ENUMS_AND_STRUCTS=1
#flag linux -DIMGUI_DISABLE_OBSOLETE_FUNCTIONS=1
#flag linux -DIMGUI_IMPL_API=
コンソールのビルドコマンドでは、以下のようにできます。
-
-cflags
はバックエンド C コンパイラにカスタムフラグを渡します。 -
-cc
は、デフォルトの C バックエンドコンパイラを変更します。 - 例えば、
-cc gcc-9 -cflags -fsanitize=thread
のようになります。
ターミナルで VFLAGS 環境変数を定義して、-cc
と -cflags
の設定を保存できます。
#pkgconfig
#pkgconfig
ディレクティブを追加すると、それぞれの依存関係で提供される pkg-config ファイルを使って、コンパイルやリンクに使うモジュールをコンパイラに伝えることができます。
V では #flag
でバッククォートが使用できず、プロセスを spawn することがセキュリティや移植性の観点から望ましくないため、標準 freedesktop と互換性のある独自の pkgconfig ライブラリを使用しています。
これにフラグが渡されなかった場合は、以下のような --cflags
と --libs
を追加します。
#pkgconfig r_core
#pkgconfig --cflags --libs r_core
.pc
ファイルは、ハードコードされた pkg-config のデフォルトパスのリストを参照します。複数のモジュールを渡すこともできます。
pkg-config が存在するかどうかを確認するには、コンパイル時の if 条件として $pkgconfig('pkg')
を使用します。存在すればその節が作成されます。それ以外の場合は、$else
や $else $if
で対応します。
$if $pkgconfig('mysqlclient') {
#pkgconfig mysqlclient
} $else $if $pkgconfig('mariadb') {
#pkgconfig mariadb
}
C コードのインクルード
V モジュールの中に C コードを追加することもできます。例として、C のコードがモジュールフォルダ内の 'c' という名前のフォルダにあるとすると、以下のような手順で追加します。
- モジュールの最上位フォルダに v.mod ファイルを配置します (
v create
でモジュールを作成した場合はすでにあります)。これは、以下のようになっています。
Module {
name: 'mymodule',
description: 'My nice module wraps a simple C library.',
version: '0.0.1'
dependencies: []
}
- モジュールの一番上にこれらの行を加えます。
#flag -I @VROOT/c
#flag @VROOT/c/implementation.o
#include "header.h"
注意: @VROOT は、 V によって v.mod ファイルが存在する一番近い親フォルダに置き換えられます。v.mod ファイルがあるフォルダと同じかそれ以下に存在する .v ファイルには、#flag @VROOT/abc を使うとそのフォルダを参照するようにできます。@VROOT フォルダはモジュールのルックアップパスの前に 付加されている ので、@VROOT フォルダ下に他のモジュールをインポートできます。
上の手順では、モジュールフォルダ/c/implementation.o
へコンパイルされた .o ファイルを V に探してもらっています。V が見つけた場合、.o ファイルはモジュールを使用するメイン実行ファイルにリンクされます。見つからなければ、V は @VROOT/c/implementation.c
ファイルがあると仮定して、それを .o ファイルにコンパイルして使おうとします。
これで、簡単に V モジュールへ C 言語のコードを同梱して配布できるようになります。V のラッパーモジュールで C コードを使用している方向けに、完全なサンプル 最小でモジュールを使った C コードのある V プロジェクト をご用意しています。他の例としましては、C から V へ構造体を渡して戻すデモ C から V から C への相互運用 もあります。
-cflags
を使用すると、カスタムフラグをバックエンドの C コンパイラに渡すことができます。また、-cc
を使用してデフォルトの C バックエンドコンパイラを変更することもできます。例えば、-cc gcc-9 -cflags -fsanitize=thread
のようになります。
C の型
よくあるゼロ終端の C 文字列は unsafe { &char(cstring).vstring() }
、 長さが分かっている場合は unsafe { &char(cstring).vstring_with_len(len)
で V 文字列に変換できます。
注意: .vstring()
と .vstring_with_len()
は cstring
のコピーを作成 しない ので、.vstring()
を呼び出した後にそれを解放しないでください。C 文字列のコピーを作成する必要がある場合 (getenv
のような libc API のいくつかは、libc の内部メモリへのポインタを返すので大抵こうする必要がある)、cstring_to_vstring(cstring)
のようにします。
Windows では、C API が wide
文字列 (UTF-16 エンコーディング) を返すことがあります。これらは string_from_wide(&u16(cwidestring))
で V 文字列に変換できます。
C との相互運用性を高めるため、V には以下の型があります。
- C の
void*
であるvoidptr
- C の
byte*
である&u8
- C の
char*
である&char
- C の
char**
である&&char
voidptr
を V の参照にキャストするには、user := &User(user_void_ptr)
というふうにします。
voidptr
は、user := User(user_void_ptr)
のようにキャストによって V 構造体へ参照外しすることもできます。
V から C コードを呼び出す例であれば、socket.v を確認してみてください。
C 宣言
C 言語の識別子にアクセスするには、モジュール固有の識別子にアクセスするのと同様に、C
という接頭辞を付けます。関数は使用する前に V で再宣言する必要があります。どんな C 言語の型でも C
というプレフィックスを付けて使用できますが、型のメンバーにアクセスするには V で型を再宣言する必要があります。
複雑な型を再宣言するには、以下の C コードのようにします。
struct SomeCStruct {
uint8_t implTraits;
uint16_t memPoolData;
union {
struct {
void* data;
size_t size;
};
DataView view;
};
};
サブデータ構造のメンバーは、以下のように中の構造体で直接宣言できます。
struct C.SomeCStruct {
implTraits u8
memPoolData u16
// これらのメンバーは、現在Vでは表現できないサブデータ構造の一部です。
// このように直接宣言すれば、アクセスするには十分です。
// union {
// struct {
data voidptr
size size_t
// }
view C.DataView
// }
}
データメンバーの存在が V に知らされれば、元の構造を正確に再現することなく使用できます。
また、サブデータ構造を 埋め込んで 並列コード構造を維持することもできます。
C を V へ変換する
V は C のコードを可読性のある V のコードへ変換し、C ライブラリをラップした V を生成します。
まずは簡単なプログラム test.c
を作成してみましょう。
#include "stdio.h"
int main() {
for (int i = 0; i < 10; i++) {
printf("hello world\n");
}
return 0;
}
v translate test.c
を実行すると、V は test.v
を生成します。
fn main() {
for i := 0; i < 10; i++ {
println('hello world')
}
}
C ライブラリのラッパを生成するには、以下のコマンドを使用します。
v wrapper c_code/libsodium/src/libsodium
これにより V モジュールに libsodium
ディレクトリが生成されます。
以下は C から V へ生成された libsodium
のラッパのサンプルです。
C コードを翻訳したり、V から C コードを呼び出したりする判断はどのようにするべきでしょうか?
よく書かれていてよくテストされている C コードであれば、もちろん単に V からこの C コードを呼び出せます。
V へ翻訳するメリットを以下にいくつか述べます。
- そのコード基盤を開発する予定の場合、一つの言語ですべてできているので、C でそれを開発するよりは安全で簡単です。
- クロスコンパイルがもっと簡単になります。なにも心配することはありません。
- ビルドフラグとインクルードファイルはもうありません。
C の問題への対処
時には、C との相互運用が非常に難しい場合があります。ヘッダーが互いに衝突している場合などがあげられます。例えば、クロスプラットフォームで動かすために V に Windows のヘッダーライブラリをインクルードをしたとしましょう。
しかしながら、Windows のヘッダライブラリが非常に一般的な、例えば Rectangle
という名前を使っていて、自分が使っている C のコードでも同様に Rectangle
という名前の何かしらを使いたいとすると、ここで衝突が起こります。
このような特殊な場合、#preinclude
を用いることができます。
これにより、V がビルトインのライブラリを足す前に設定することができます。
例:
// ビルトインライブラリが使われる前にインクルードされる
#preinclude "pre_include.h"
// ビルトインライブラリが使われた後でインクルードされる.
#include "include.h"
pre_include.h
を用いた例は こちら で見ることができます。
これは発展的な機能であり、C との相互作用以外では必要にはならないでしょうし、この機能が解決する問題よりも生む問題の方が大きくなることもあるでしょう。
最終手段として考えてください!
その他の V の機能
インラインアセンブリ
fn main() {
a := 10
asm x64 {
mov eax, [a]
add eax, 10
mov [a], eax
}
}
さらなる例は、https://github.com/vlang/v/tree/master/vlib/v/slow_tests/assembly/asm_test.amd64.v をご覧ください。
ホットコードリロード
module main
import time
import os
[live]
fn print_message() {
println('Hello! Modify this message while the program is running.')
}
fn main() {
for {
print_message()
time.sleep_ms(500)
}
}
この例を v -live message.v
でビルドします。
リロードされてほしい関数には、その定義の手前に [live]
属性をつける必要があります。
現在は実行中に型を変更することはできません。
他に、グラフィカルアプリケーションの例があります。
https://github.com/vlang/v/tree/master/examples/hot_code_reloading
V でクロスプラットフォームなシェルスクリプト
V は Bash の代替として、デプロイスクリプト、ビルドスクリプトなどを書くことに使用できます。
このために V を使うメリットは、言語のシンプルさ、読みやすさ、クロスプラットフォームサポートです。「V スクリプト」は UNIX 系システム上でも Windows と同じようにうまく動作します。
プログラムを .vsh
という拡張子にすると、os
モジュール内のすべての関数がグローバルになります (例えば os.mkdir()
の代わりに mkdir()
が使えます)。
以下はサンプルの deploy.vsh
です。
#!/usr/bin/env -S v run
// 上のシバンは、Unix 系システムでファイルを V に関連付け、
// `chmod +x` で実行可能にしてからファイルパスを指定すれば
// 実行できるようにしています。
// コマンドを出力してから実行します
fn sh(cmd string){
println("❯ $cmd")
print(execute_or_exit(cmd).output)
}
// build/ が有れば削除し、無い場合はそのエラーを無視します
rmdir_all('build') or { }
// build/ を作成し、build/ が無いことで失敗しないようにします
mkdir('build')!
// *.v ファイルを build/ へ移動します
result := execute('mv *.v build/')
if result.exit_code != 0 {
println(result.output)
}
sh('ls')
// 以下と同じ:
// files := ls('.')!
// mut count := 0
// if files.len > 0 {
// for file in files {
// if file.ends_with('.v') {
// mv(file, 'build/') or {
// println('err: $err')
// return
// }
// }
// count++
// }
// }
// if count == 0 {
// println('No files')
// }
これで、これを通常の V プログラムのようにコンパイルし、どこでもデプロイし実行できる実行形式が得られます。
v deploy.vsh && ./deploy
もしくは伝統的な bash スクリプトのようにそのまま実行します。
v run deploy.vsh
Unix 系プラットフォームでは、chmod +x
で実行可能にすれば ./deploy.vsh
でファイルを直接実行できます。
拡張子のない Vsh スクリプト
V は通常、指定された拡張子のない vsh スクリプトを許可しませんが、このルールを回避して、完全なカスタム名とシバンを持つファイルを作成する方法があります。この機能は、パスに配置されるスクリプトのような特定の用途にのみ推奨され、ビルドやデプロイのスクリプトなどには使用しないでください。この機能を利用するには、#!/usr/bin/env -S v -raw-vsh-tmp-prefix tmp
(tmp
はビルドした実行ファイルのプレフィックス)でファイルを起動します。これは crun モードで実行されるので、スクリプトに変更があった場合のみ再ビルドされ、バイナリは tmp.<scriptfilename>
として保持されます。注意: このファイル名がすでに存在する場合、そのファイルは上書きされます。毎回再ビルドし、このバイナリを保持しないようにするには、#!/usr/bin/env -S v -raw-vsh-tmp-prefix tmp run
を使用します。
補遺
補遺 I: キーワード
V には 44 の予約キーワード (うち 3 個はリテラル) があります。
as
asm
assert
atomic
break
const
continue
defer
else
enum
false
fn
for
go
goto
if
import
in
interface
is
isreftype
lock
match
module
mut
none
or
pub
return
rlock
select
shared
sizeof
spawn
static
struct
true
type
typeof
union
unsafe
volatile
__global
__offsetof
[型](#V の型) もご参照ください。
補遺 II: 演算子
このリストの演算子は プリミティブ型 専用です。
記号 | 意味 | オペランド |
---|---|---|
+ | 和 | integers, floats, strings |
- | 差 | integers, floats |
* | 積 | integers, floats |
/ | 商 | integers, floats |
% | 剰余 | integers |
~ | ビット NOT | integers |
& | ビット AND | integers |
| | ビット OR | integers |
^ | ビット XOR | integers |
! | 論理 NOT | bools |
&& | 論理 AND | bools |
|| | 論理 OR | bools |
!= | 論理 XOR | bools |
<< | 左シフト | integer << unsigned integer |
>> | 右シフト | integer >> unsigned integer |
優先度
優先度 | 演算子 |
---|---|
5 | * / % << >> & |
4 | + - | ^ |
3 | == != < <= > >= |
2 | && |
1 | || |
代入演算子
+= -= *= /= %=
&= |= ^=
>>= <<=