和訳
V_Programming_Language

V プログラミング言語


はじめに

The V Programming Language の公式ドキュメントの和訳だよ

情報はすべて↑から

コンパイルが速いこととC/C++との相互変換がウリの模様 (C/C++ 勢なので注目せざるを得ない

2019 年 4 月にアーリーアクセス、同年 6 月にオープンソースで公開予定

すでに V 自身で V コンパイラは作成されている

もともと GUI アプリ用につくられたので

クロスプラットフォームな GUI ライブラリとかもあったりする

私も和訳で閲覧数を稼ぐコスいことしてみる

ただドキュメントが完全ではないのでまだ把握しきれてない

以下から訳文


導入

V は保守しやすいソフトウェアを構築するために設計された静的型付け言語です。

Go にとてもよく似通っており、Oberon、Rust、Swift にも影響されています。

V は非常にシンプルな言語です。

30 分ほどかけてこのドキュメントを読めば、言語の全貌がちょっとつかめるでしょう。

シンプルですが、開発者には大きな力を与えます。

他の言語でできることなら、V もできます。


Hello World

fn main() {

println('hello world')
}

関数は fn で宣言します。戻り値の型は関数名の後ろに来ます。この場合 main は何も返さないので型は省かれています。

C やその類の言語と同じく、main はエントリーポイントです。

println はいくつかある組み込み関数の一つです。これは標準出力へ値を出力します。


コメント

// これは単行コメント 

/* これは複行コメント
/* ネストできる */
*/


関数

fn add(x int, y int) int {

return x + y
}

fn sub(x, y int) int {
return x - y
}

fn main() {
println(add(77, 33))
println(sub(100, 50))
}

更に、型は実引数名の後に来ます。


変数

fn main() {

name := 'Bob'
age := 20
large_number := i64(0)
println(name)
println(age)
}

変数は := で宣言と初期化が行われます。これは V で変数を宣言する唯一の手段です。つまり、変数は必ず初期値があるということです。

変数の型は右側の値から推論されます。他の型に強制するには、型変換を使います。T(v) と書くと値 v を型 T に変換します。

他の多くの言語とは異なり、V は関数内でしか変数の定義を許可しません。グローバル (モジュールレベルな) 変数は使用できません。V に大域な状態はないのです。

fn main() {

mut age := 20
println(age)
age = 21
println(age)
}

変数の値を変更するには = を使用します。V での変数はデフォルトでイミュータブルです。変数の値を変えられるようにするには、mut をつけて宣言する必要があります。

:== の違いに注意してください。

:= は宣言と初期化に用いられますが、= は代入に用いられます。

fn main() {

age = 21
}

このコードはコンパイルできません。変数 age は宣言されていないからです。V ではすべての変数は宣言されている必要があります。


基本の型

bool

string

i8 i16 i32 i64
u8 u16 u32 u64

byte // u8 の別名
int // i32 の別名
rune // i32 の別名、Unicode コードポイントに相当

f32 f64

注意として、C や Go と異なり、int は常に 32 ビット整数になります。


文字列

fn main() { 

name := 'Bob'
println('Hello, $name!')

println(name.len)
bobby := name + 'by' // + は文字列連結に使われる
println(bobby) // ==> Bobby

println(bobby.substr(1, 3)) // ==> ob
// println(bobby[1:3]) // この構文は substr() メソッドとよく置き換えられる
}

V では、文字列は読み出し専用のバイトの配列です。文字列データは UTF-8 を用いてエンコードされます。

文字列はイミュータブルです。これは部分文字列関数がとても効率的ということであり、コピーは実行されず余分な割り当ても必要ありません。

結合演算子 + は両辺に文字列が必要です。この以下のコードは ageint の場合はコンパイルされません。

println('age = ' + age)

以下のように age を文字列に変換するか、

println('age = ' + age.str())

以下の文字列挿入をします (こちらを推奨) 。

println('age = $age')


配列

fn main() {

nums := [1, 2, 3]
println(nums)
println(nums[1]) // ==> "2"

mut names := ['John']
names << 'Peter'
names << 'Sam'
// names << 10 <-- これはコンパイルされない。`names` は文字列の配列である。
println(names.len) // ==> "3"
println(names.contains('Alex')) // ==> "false"

// 更に要素を任意の量だけ事前に確保できます。
nr_ids := 50
mut ids := [0 ; nr_ids] // これで 50 個のゼロの配列を作成する
for i := 0; i < nr_ids; i++ {
ids[i] = i // これは下のより効率的
// ids << i
}
}

配列の型はその最初の要素で決定されます。[1, 2, 3] は整数の配列 ([]int) です。

['a', 'b'] は文字列の配列 ([]string) です。

配列のすべての要素は同じ型でなければなりません。[1, 'a'] はコンパイルされません。

<< は配列の末尾に値を追加する演算子です。

len フィールドは配列の長さを返します。注意として、これは読み出し専用のフィールドで、ユーザーによって変更できません。V ではすべての外から見えるフィールドはデフォルトで読み出し専用です。

.contains(val) メソッドは配列に val が含まれていれば true を返します。


辞書配列

fn main() {

mut m := map[string]int{} // 現在の辞書配列は文字列のみをキーにできる
m['one'] = 1
println(m['one']) // ==> "1"
println(m['bad_key']) // ==> "0"
// TODO: キーが存在するかどうか確認する手段を実装する

numbers := { // TODO: この構文はまだ実装されていません
'one': 1,
'two': 2,
}
}


If

fn main() {

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) // ==> "even"


For ループ

V でのループ構造は for しかありません。

fn main() {

numbers := [1, 2, 3, 4, 5]
for num in numbers {
println(num)
}
names := ['Sam', 'Peter']
for i, name in names {
println('$i) $name') // 出力: 0) Sam
}  1) Peter
}

for .. in ループは配列の要素を取っていくのによく用いられます。インデックスが必要な場合、もう一つの書き方として index, value in が使用できます。

fn main() {

mut sum := 0
mut i := 0
for i < 100 {
sum += i
i++
}
println(sum)
}

このループの書き方は他の言語での while ループと同様です。

このループは条件式が false に評価されると繰り返しを止めます。

同じく、条件式は括弧で囲まず、波括弧は常に必要です。

fn main() {

mut sum := 0
for {
sum++
}
println('これは表示されない')
}

条件式は省略でき、これは無限ループになります。

fn main() {

for i := 0; i < 10; i++ {
println(i)
}
}

最後に、C スタイルの for ループもあります。これは while より安全な書き方です。なぜなら、後者の場合はカウンターの更新を忘れて無限ループに陥りやすいからです。


Match

import os

fn main() {
print('V runs on ')
match os.OS_NAME {
case 'darwin':
println('macOS.')
case 'linux':
println('Linux.')
default:
println(os.OS_NAME)
}
}

match 文は if - else 文の羅列を書くお手軽な手段です。条件式の値と等しい最初の case を実行します。

C とは異なり、break 文を各ブロックの終わりにつける必要はありません。


構造体

struct Point {

x int
y int
}

fn main() {
p := Point{10, 20}
println(p.x) // 構造体のフィールドはドットを用いてアクセスする
p2 := Point{ // フィールドはこの構文でもセットできる
x: 20
y: 30
}
// & を前につけるとその構造体の値のポインタを返します。
// これはヒープ領域上に確保され、V によって自動で破棄されます。
// これは外に持ち出さないので関数の終わりで破棄されます。
pointer := &Point{10,10}
println(pointer.x, pointer.y) // ポインタも同じ構文でフィールドにアクセスする
}


メソッド

struct User {

age int
}

fn (u User) can_register() bool {
return u.age > 16
}

fn main() {
user := User{age: 10}
println(user.can_register()) // ==> "false"

user2 := User{age: 20}
println(user2.can_register()) // ==> "true"
}

V にはクラスがありません。しかし、型にメソッドを定義できます。

メソッドは特殊なレシーバー引数のある関数です。

レシーバーの引数リストは fn キーワードとメソッド名の間に出てきます。

この例では、can_register メソッドには User 型の u と命名されたレシーバーがあります。慣習として selfthis のようなレシーバー名は使わずに、短めで、一文字長の名前が好まれます。


ミュータブルレシーバー & 純粋関数

struct User {

is_registered bool
}

fn (u mut User) register() {
u.is_registered = true
}

fn main() {
user := User{}
println(user.is_registered) // ==> "false"
user.register()
// TODO: レシーバーを変更するメソッドは `!` と印をつけることを強制されるかも
// user.register()!
println(user.is_registered) // ==> "true"
}

関数はレシーバーの値しか変更できないことに注意してください。

fn register(u mut User) はコンパイルされません。

V の関数は部分的に純粋です。関数の実引数はその関数によって変更されることはありません。

他にオブジェクトを変更する方法としては、単に変更版を返す方法があります。

// TODO: この構文はまだ実装されていません 

fn register(u User) User {
return { u | is_registered: true }
}

user = register(user)


定数

const (

PI = 3.14
WORLD = '世界'
)

fn main() {
println(PI)
println(WORLD)
}

定数は const キーワードで宣言されます。これらはモジュールレベル (関数の外側) で定義できます。

定数はすべて大文字でなければなりません。これは変数の分別を促します。

定数は変更できません。

V の定数はほとんどの言語よりも柔軟です。以下の複雑な値も代入できます。

struct Color {

r int
g int
b int
}

fn (c Color) str() string { return '{$c.r, $c.g, $c.b}' }

fn rgb(r, g, 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)
)

fn main() {
println(NUMBERS)
println(RED)
println(BLUE)
}

グローバル変数は許可されていないので、これは非常に役立ちます。


モジュール

V はとてもモジュール方式な言語です。再利用可能なモジュールを作成することは、推奨されており、非常に簡単です。新しいモジュールを作成するには、モジュールの名前のディレクトリを作成してコードを書いた .v ファイルを入れます。

cd ~/code/modules

mkdir mymodule
vim mymodule/mymodule.v

// mymodule.v

module mymodule

// 関数を export するには `pub` を使用します
pub fn say_hi() {
println('hello from mymodule!')
}

mymodule/ には好きなだけ .v ファイルを入れることができます。

これは v -lib ~/code/modules/mymodule でビルドできます。

それでは、これを以下のようにコード内で使うことができます。

module main

import mymodule

fn main() {
mymodule.say_hi()
}

注意として外部の関数を呼ぶには毎回モジュールを指定する必要があります。これは煩わしく思えるかもしれませんが、コードはより可読性があり理解しやすいようになります。どのモジュールによるどの関数が呼ばれているのか常に明らかだからです。特に巨大なコードにおいては。

モジュールの名前は 10 文字以下くらいに短くするべきです。循環 import は許可されていません。

今現在はモジュールをどこにでも作成できます。おそらくこれは (Go の GOPATH のように) 標準化されるべきでしょう。

すべてのモジュールは静的に単一の実行形式にコンパイルされます。


インターフェイス

struct Dog {}

struct Cat {}

fn (d Dog) speak() string { return 'woof' }
fn (c Cat) speak() string { return 'meow' }

interface Speaker {
speak() string
}

fn perform(s Speaker) { println(s.speak()) }

fn main() {
dog := Dog{}
cat := Cat{}
perform(dog) // ==> "woof"
perform(cat) // ==> "meow"
}

型がインターフェイスを実装するには、そのメソッドを実装することでできます。意図を明示的に宣言することはできず、"implements" キーワードもありません。

V のインターフェイスはとても効率的です。動的ディスパッチはありません。


列挙

enum Color {

red, green, blue
}

fn main() {
mut color := Color.red
println(color) // ==> "red"
color = .green
is_green := color == .green
}


Option 型とエラー制御

struct User {

id int
}

struct Repo {
users []User
}

fn (r Repo) find_user_by_id(id int) User? {
for user in r.users {
if user.id == id {
// V は自動的にこれを Option 型にラップする
return user
}
}
return error('User $id not found')
}

fn main() {
repo := // ... ここでレポジトリーを初期化してユーザーのデータを読み込む
user := repo.find_user_by_id(10) or {
eprintln(err)
return
}
println(user.id) // 10
}

他に知ってほしいこととして、関数を Optional な関数へ「アップグレード」する必要のある作業が山のようにあっても、その変更は小さくなります。戻り値の型に ? を加えて、何かがおかしいときにエラーを返します。

エラーを返す必要が無い場合は、単に None を返せます。(TODO: None はまだ実装されていません)

これは V でエラーを制御する主要な手段です。これらは値でもあり、Go のようですが、エラーを無視できないという利点があり、かつこれらを制御することはそこまで煩わしくありません。

以下のようにエラーを伝播できます。

resp := http.get(url)?

println(resp.body)

http.get?http.Response を返します。? を付けて呼び出すので、エラーは呼び出した関数から伝播されます。この場合は main でパニックが起こります。

基本的に上記のコードは以下のコードを短くしたものになります。

resp := http.get(url) or {

panic(err)
}
println(resp.body)


ジェネリクス (7月)

struct Repo <T> {

db DB
}

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)
}

fn main() {
db := new_db()
users_repo := new_repo<User>(db)
posts_repo := new_repo<Post>(db)
user := users_repo.find_by_id(1)?
post := posts_repo.find_by_id(1)?
}


並列計算

並列計算モデルは Go に酷似しています。foo() を並列実行するには、go foo() と呼ぶだけです。即座に新しいシステムスレッド内で実行されます。ゴルーチン (goroutines) とスケジューラーはまもなく実装されます。


JSON のデコード

struct User {

name string
age int
}

fn main() {
const input = '{ "name": "Frodo", "age": 25 }'
user := json.decode(User, input) or {
eprintln('Failed to decode json')
return
}
println(user.name)
println(user.age)
}

JSON は今日ではとても広く使われているため、組み込みで JSON をサポートしています。

json.decode 関数の第一引数はデコードした型です。第二引数は JSON 文字列です。

V は JSON エンコードやデコードするコードを生成します。リフレクションは使われていません。そのため、それなりのパフォーマンスを発揮します。


codegen を介したリフレクション

組み込み JSON サポートはありますが、V は以下のように任意のものに対する効果的なシリアライザを作成することも許可します。

// TODO: 5月中を予定

fn decode<T>(data string) T {
mut result := T{}
for field in T.fields {
if field.typ == 'string' {
result.$field = get_string(data, field.name)
} else if field.typ == 'int' {
result.$field = get_int(data, field.name)
}
}
return result
}

// 以下が生成される
fn decode_User(data string) User {
mut result := User{}
result.name = get_string(data, 'name')
result.age = get_int(data, 'age')
return result
}


制限付き演算子オーバーロード

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 よりとても読みづらいですしね。

安全性と保守性を促進するため、演算子オーバーロードには以下のようないくつかの制限があります。


  • オーバーロードできる演算子は +-*/ だけです。

  • 他の関数を演算子関数の中で呼び出すことはできません。

  • 演算子関数の引数は変更できません。


テスト


hello.v

fn hello() string {

return 'Hello world'
}


hello_test.v

fn test_hello() {

assert hello() == 'Hello world'
}

すべてのテスト関数は *_test.v ファイルに配置され、test_ で始まっている必要があります。テストを実行するには v hello_test.v をします。モジュール全体をテストするには v test 自分のモジュール とします。


メモリ管理

ガベージコレクションや参照カウントはありません。V はコンパイル時で解放できるようにします。以下はその例です。

fn draw_text(s string, x, y int) {

...
}

fn draw_scene() {
draw_text('hello $name1', 10, 10)
draw_text('hello $name2', 100, 10)
draw_text(strings.repeat('X', 10000), 10, 50)
}

文字列は draw_text より外に出ていないので、これらは関数を抜けるときに解放されます。

実際に最初の 2 つの呼び出しはすべて、結果的にメモリ確保をしていません。これら 2 つの文字列 ('hello''world') は小さいので、V はこれらのために事前に確保されたバッファを使用します。

より複雑な場合にではメモリ管理が必要です。これはまもなく修正されます。

V は実行時にメモリリークを検知し、それらを報告します。解放する例として、配列において、free() メソッドを使用してみます。

numbers := [0; 1000000] 

...
numbers.free()


V から C 関数を呼び出す

#flag -lsqlite3

#include "sqlite3.h"

struct C.sqlite3
struct C.sqlite3_stmt

fn C.sqlite3_column_int(C.sqlite_stmt, int) int

fn main() {
path := 'sqlite3_users.db'
db := &C.sqlite3{}
C.sqlite3_open(path.cstr(), &db)

query := 'select count(*) from users'
stmt := &C.sqlite3_stmt{}
C.sqlite3_prepare_v2(db, query.cstr(), - 1, &stmt, 0)
C.sqlite3_step(stmt)
nr_users := C.sqlite3_column_int(res, 0)
C.sqlite3_finalize(res)
println(nr_users)
}


C/C++ を V へ変換する

V は C/C++ のコードを可読性のある V のコードへ変換します。まずは簡単なプログラム test.cpp を作成してみましょう。


test.cpp

#include <vector>

#include <string>
#include <iostream>

int main() {
std::vector<std::string> s;
s.push_back("V is ");
s.push_back("awesome");
std::cout << s.size() << std::endl;
return 0;
}

v translate test.cpp を実行すると V は test.v を生成します。


test.v

fn main {

mut s := []string
s << 'V is '
s << 'awesome'
println(s.len)
}


補遺 I: キーワード

V には 21 のキーワードがあります。

break

const
continue
defer
else
enum
fn
for
go
goto
if
import
in
interface
match
module
mut
or
return
struct
type


ここまで訳文

ここからは、ドキュメントには載っていないいくつかの情報を載せます


ホットリロード

ファイルの最初に #!hot と付けるとホットリロードするらしい


Playground

ここ

2019 年 3 月 20 日現在停止中

ほとんどの機能は壊れていて使えない模様

2019 年 4 月 17 日現在稼働中

まだ一部機能は制限中