🌰

ムダを省き安全性を備えた新言語「Ches」の紹介

2021/10/15に公開

はじめに

初投稿のがーねっとです。
本日 10/15 は私が開発するプログラミング言語「Ches」(チェス) の誕生日です。
今日は二周年を記念し、この言語の特徴や仕様などについて幅広く紹介していきたいと思います。

❗❗ 共同開発者を募集しています。もし興味がありましたら ## 開発者募集 をご覧ください。

言語概要

Ches は安全性を求めるプログラマとプログラミング初学者に向けて開発されています。
安全性に関してはプログラミング言語 Rust による影響を大きく受けています。
これらの詳細は ## おすすめポイント で。
ちなみに Ches の名前はボードゲームの Chess とは関係ありません・・・

おすすめポイント

Ches の「ここがいい」というおすすめしたいポイントを紹介します。

内容をまとめて図解してみました:

高い安全性

安全性を実現するための機能や記法を取り入れることでプログラム中のバグを防ぎます。
また、コンパイラのチェックを厳格にすることで事前に対策可能なバグを最大限防止してくれます。
( これで C/C++ のバグ地獄から逃れられる... )

具体的にはこのような仕様になっています:

  • 強力な静的型付けを採用 → コンパイラによる強力な型検査を可能にする
  • 暗黙の型変換を極力排除 → プログラマの予期しない型変換を起こさない
  • 変数の利用に初期化を必須とする → 予期しないデフォルト値を自動で使わせない
  • 演算子の重複を排除 → "0" + 1 などの演算子が曖昧な式をなくす

特にコンパイラが厳格な言語 Rust ではコンパイルエラーに悩まされている人々 (私もです) の嘆きを見かけますが、これに関してはコンパイル時にエラーで悩むか実行時にバグで悩むかの違いだと思います。
後でわけのわからない挙動に頭を悩ますよりも事前にコンパイラが問題点を教えてくれたほうほうがプログラマ側の負担は少ないですしね。

書きやすさ & 読みやすさ

書きやすさや読みやすさはプログラミング言語の基本だと思います。
Ches では冗長な記述をできる限り排除するため、記号を極力使用せずキーワードの文字数も短くしています。
構文が省略されとも書きたいコードが楽に書ければいいのです。

ちなみに型のサイズを直感的に把握できるよう数値型の名前は s32 f64 というような形式になっています。

以下は Rust との構文比較です:

// Rust コード
pub fn add(i1: i32, i2: i32) -> i32 {
    return i1 + i2;
}
# Ches コード
# 余計な記号を省略; return を ret と省略
# 少し慣れれば楽に書けますし読みやすくなります
fn add(i1 s32, i2 s32) s32
    ret i1 + i2
end

Ches では「同じことを二度書かない」ということを徹底しています。
以下は C# との比較です。

// C# コード
List<String> list = new List<String>();
// 必要に応じて型推論を利用できる
var list = new List<String>();
# Ches コード
# 型を複数記述しない
let vec = Vec<str>()

学びやすさ

安全性を実現するためにさまざまな概念を取り入れたことで学習コストは少々高くなっています。
しかしながらバグを防ぐ仕組みや複雑さを排除した構造は十分学びやすさに繋がっています。
コンパイラが主導してバグの元となる箇所を特定してくれるところも易しいポイントですね。
また、初学者向けの入門コースも整備する予定なのでさらに学びやすい言語になると思います。

ちなみに入門コースは初学者のみに向けたものではありません。
「ビギナコース」と「エキスパートコース」の 2 コースを用意し、プログラミング経験者にも対応した教材となる予定です。

オブジェクト指向

厳密には Ches はオブジェクト指向言語ではありませんが、オブジェクト指向的なプログラミングをすることは十分可能です。
データと振る舞いの分離やカプセル化などの概念が導入されているためです。
Ches ではクラスやインタフェイスの代わりに構造体や特性体、継承の代わりに実装という方式を用います。

継承と実装を比較する図解を作りました:

開発過程

Ches を開発する上でこれまでどのような出来事があったのか紹介します。
言語解説には直接関係しないため参考程度にどうぞ。

開発を開始

以前から「初学者に易しい言語」として構想を重ね、2019 年に本格的な開発を開始しました。
この方針は部分的に転換することになります。(後述)

Ches に改名

もともと Chestnut ( チェスナット = 栗 ) という言語名でしたが、わかりやすさのため Ches に改名しました。

開発言語の移行

開発言語を C++ から Rust に移行しました。
その際、開発途中だったコンパイラやコマンドラインツールなどは作り直すことに。

方針転換

この言語は前述のとおり「初学者に易しい言語」、つまり学習コストの低い言語を目指していました。
しかし Rust の影響で言語の安全性を実現する仕組みを取り入れたことで Ches の学習コストが上がってしまい・・・
そのため安全性を高く保ちながら公式の Ches 学習コースを拡充することで、安全性と学びやすさを両立していくという方針に転換しました。

現在の開発状況

現在は人手不足のため未着手の作業も多いです。
もし開発に興味があれば記事最後の ## 開発者募集 をご覧ください。

内容 進度
言語仕様の策定
仕様のドキュメント化
コンパイラ開発
PEG パーサの開発
バイトコーダの開発
インタプリタの開発
  • ⭕ ... ある程度完了
  • ➖ ... 作業途中
  • ❌ ... 未着手

言語仕様

ここでは Ches の仕様をコード例を交えながら解説します。

※ コード例のシンタックスハイライトは Ruby で代用しています。
※ ここに記述する仕様は今後一部変更する可能性があります。

命名規則

先頭に _ をつけるとアクセス範囲を private に設定できます。

要素 規則の種類 その他規則
構造体 PascalCase -
特性体 PascalCase 接頭辞は T
列挙体 PascalCase 接頭辞は E
マクロ snake_case -
フィールド snake_case -
メソッド snake_case -
ローカル変数 snake_case -

組込型

数値型の名前にはすべて接頭辞がついています。
Rust と異なり符号付き整数は s から始まるため注意が必要です。

各接頭辞の意味はこのとおりです:

  • s ... 符号付き整数; signed integer
  • u ... 符号なし整数; unsigned integer
  • f ... 浮動小数点数; floating-point

組込型一覧

型名 種類 バイト数 他言語との対応
bool 真偽値 1 bool
s8 整数 1 byte
u8 整数 1 unsigned byte
s16 整数 2 short
u16 整数 2 unsigned short
s32 整数 4 int
u32 整数 4 unsigned int
s64 整数 8 long
u64 整数 8 unsigned long
f32 浮動小数点数 4 float
f64 浮動小数点数 8 double
char 文字 - char
str 文字列 - string

基本的な書き方

文を終わらせるには改行を使います。
セミコロンを使うと改行が不要になりますがこの記法は基本的に推奨されません。

# 通常は改行
let mut num = 0s32
num += 1

# セミコロンは非推奨
let mut num = 0s32; num += 1

ブロックは中括弧 {} でなく end キーワードを用いて表現します。

for i in 10
    println("{} time(s)", i)
end

構造体

構造体はデータ (フィールド) の集まりで、実装 (メソッド) を含めることもできます。
継承ができないことを除けば他言語のクラスとおおむね同じです。
継承ができない代わりに特性体 (≒インタフェイス) を「実装」することで継承に似たことが実現できます。
詳細は ### オブジェクト指向 の図解を参照ください。

※ 構造体などはブロックでなく宣言文に近いため、インデントや end キーワードの記述は必要ありません。

struct MyStruct

# データブロック = フィールドの集まり
data
    # フィールド名 フィールド型
    field_1 str
end

# 実装ブロック = メソッドの集まり
impl
    # メソッド名(引数) 返り値の型
    # 返り値の型は省略可
    fn method_1(name str) str
        ret "Hello, {}!", name
    end
end

コンストラクタは実装ブロック内でこのように記述します。
返り値の型を指定する必要はありません。

※ 静的メソッドと動的メソッドを区別する方法は未定

fn @const(field str)
    # フィールドの初期化などを行う
    self.field = field
end

特性体

Rust でいうトレイト、他言語でいうインタフェイスの代わりになります。
注意点として構造体を用いて構造体に実装することはできないため、特性体を定義して構造体への実装に使う必要があります。

特性体で抽象メソッドを定義すると実装側でその実装を強制することができます。

# 特性名の接頭辞は T
trait TTest

data
    field str
end

impl
    # abs キーワードによる抽象メソッドの定義
    # 抽象メソッドでは関数の実装をしないこと
    abs fn my_method()
    end
end
# 実装側の構造体
struct MyStruct

# TTest 特性体の実装
impl TTest
    fn my_method()
        println("Hello, world!")
    end
end

標準ライブラリ内の特性体

標準ライブラリでは基本的な特性体が用意されています。
例えば文字列フォーマットを可能にする TStrFormat や、等価演算子による等価比較を可能にする TEqualComp などがあります。

列挙体

識別子と定数のペアを複数作ることができます。
実は列挙体にも実装をもたせることができます。

# 列挙名の接頭辞は `E`
# ここでは u32 型を指定しているが未指定の場合は s32 型となる
enum ETest u32

data
    # フィールド名
    value_1
    # フィールド名 = フィールド値
    value_2 = 10
end

# 実装をもたせることもできる
impl
    fn is_value_1(enum_val ETest)
        ret enum_val == ETest.value_1
    end
end

変数

可変性

Rust と同様、変数には可変性という概念が存在し、「可変な変数」「不変な変数」という表現をします。
不変な変数は初期化後に値を変更できません。要は定数です。
誤って変数値を変更しないよう、値を変更する必要がない変数は不変にします。
初期化後に値を変更するには mut キーワードにより変数を可変にします。

# 不変な変数
let immutable = 0s32
# 可変な変数
let mutable = mut 0s32

# エラー: 不変であるため変更不可
immutable += 1
# 成功: 可変であるため変更可
immutable += 1

所有権

所有権とは変数が値にアクセスする権利です。
所有権をもつ変数を「所有者」、値の所有権をもつことを「値を所有する」と表現します。

以下は所有権の基本的なルールです。

  • 1 つの値を所有する変数はただ 1 つ ... 重要
  • 代入や引数渡しなどにより所有権が移動する ... 組込型でも同様
  • 所有者のスコープを外れれば値は破棄 (デストラクト) される

この仕組みにより GC (ガベージコレクタ) を使わずにメモリの安全性を確保することができます。

コードを交えて解説します。

# この変数値を値Aとします
# 変数 num_1 が値Aの所有権をもつため、この変数から値にアクセスできます
let num_1 = 0s32

# num_1 が num_2 に代入されたためこの所有権が num_2 に移動します
# 1 つの変数しかある値の所有権をもつことができないためです
let num_2 = num_1

# エラー: num_1 はどの値の所有権ももたないため値にアクセスできません
println(num_1)
# 成功: num_2 は値Aの所有権をもつため変数値 0 が表示されます
println(num_2)

変数 num_1, num_2 と値 0s32 を図にしてみました:

( この時点で図を作る気力が尽きてしまった・・・ )

Rust の所有権とおおむね同様なのでこちらも参照ください。

所有権とは? - The Rust Programming Language 日本語版
https://doc.rust-jp.rs/book-ja/ch04-01-what-is-ownership.html

参照

1 つの変数しかある値の所有権をもつことができないことを前述しました。
しかし所有権が一切共有できなければ不便であるため参照という機能が用意されています。
参照を用いるとある値の所有権を共有した複数の参照変数を作ることができます。

※ 参照をもった変数がすべて破棄されるまでは所有権をもった変数を破棄できません。

ref キーワードを用いて参照を作成します。

# 所有権をもつ
let owner = 0s32
# 参照をもつ
let ref_owner_1 = ref owner
let ref_owner_2 = ref owner

# 成功: ref_owner_1, ref_owner_2 は owner と所有権を共有しているため値にアクセスできる
println(owner)
println(ref_owner_1)
println(ref_owner_2)

メソッド

Ches では構造体などでしか関数を定義できないため「関数」よりも「メソッド」と呼ぶことのほうが多いです。

特殊メソッド

コンストラクタやデストラクタなどの特別な役割をもったメソッドを特殊メソッドと呼びます。
特殊メソッドの名前には @ という接頭辞をつけます。( 例: @const() @dest() )

演算子

Ches では演算子の用途が重複しないよう工夫されています。
特に文字列の結合は "1" + "2" でなく "1" ~ "2" と記述するため直感的に演算子の役割を把握できます。

比較演算子に関しては a == b == ca < b < c というような括弧を使わない記法を導入するか検討中です。

処理系

現在は Rust による処理系 (Rustnut) を開発しています。
パーサや仮想マシンなどはすべてオリジナルです。

コンパイラ

構文は Parsing Expression Grammar (PEG) という形式文法で記述されます。
構文解析時にレクサを通す必要がないため PEG パーサさえあれば楽に構文解析をすることができます。
PEG については PEG基礎文法最速マスター が参考になります。

現在は 佐藤陽花 氏と共同で PEG 方言 FCPEG を開発しています。
( 基本的な機能はほぼ完成したため構文記述に移行中; 以後リリース予定 )

FCPEG コードの一部:

インタプリタ (仮想マシン)

仮想マシンはまだ構想段階ですがレジスタマシンとして開発予定です。
マシンが完成すれば処理系の開発もおおよそ完了となります。

おわりに

最後までお読みいただきありがとうございます。
ここまでの紹介で Ches の特徴や仕様がおおまかにつかめたと思います。
今後も言語開発を継続し、できれば今年度中に軽いプログラムを動かせるくらいまで作り進めたいと考えています。

この記事や言語に対するご意見を募集しています。

最新の更新情報は公式 Twitter をご覧ください。

公式 Twitter: 【公式】汎用プログラミング言語「Ches」 (@Ches_lang)
GitHub オーガナゼーション: ChesLang
FCPEG リポジトリ: FunCobal-family/FCPEG

開発者募集

最後に宣伝 (?) 失礼します。

Ches を共同で開発してくださる方を募集しています。
プログラミング言語開発の経験がある方はもちろん、プログラミング経験のない方でも歓迎です。

例えばこのような活動を行います:

  • 言語仕様の策定
  • 公式ドキュメントの整備
  • ソフトウェアの設計や実装
  • 学習教材の制作

※ スキルによって役割分担あり

継続的に開発に参加できるのであればプログラミングスキルは問いません。

詳しくはこちらの記事をご覧ください:

開発参加者必読 (Scrapbox): [お知らせ] 共同開発者を募集しています (初学者可)

Discussion