Open11

JVMで動くRustっぽい言語のアイデア

todeskingtodesking

JVMというGC前提の環境においては自前のメモリプールやoff-heapメモリを使わない限りは所有権ベースのメモリ管理を模倣する必要がないわけだが、GCが関与しないファイルハンドル等のリソース管理については所有権という仕組みが有用。
RustはRefCellとかあるんで、mutはどちらかというと安全性のためのものだと思いますが、JVMベース言語においては可変性を明示することによる設計上のメリットのほうがありそう。
所有権に比べるとトリビアルな話としては、struct A{}struct (A) があるやつとか enum 、パターンマッチで{a, b, ..}によるフィールドの部分マッチができる等が便利。

todeskingtodesking

静的に解決されるtraitというのは必要だろうか?(サブタイピングと相性悪そう)
具体化されるタイプのgenericsは有用。

todeskingtodesking

ぜんぶ静的に解決してやろうという思想は大変かっこよいが、動的なメソッド解決が一級市民であるJVMにそれを持ち込むとバランスが難しそう。しかしsubtypingを捨てるとJVMらしさが……

todeskingtodesking
  • プリミティブの値型: int, boolean, ...
  • 不変な参照型: String, ...
  • 可変な参照型: String[], ...
  • リソースを表現する参照型: FileInputStream, ...

大部分は非リソース型なので、Tと書けばプリミティブ/通常の参照型を指すとして、リソース型はmove T/&T/&mut Tと表記する。とか。

fn string_array_len(arr: String[]) -> int = arr.len()

fn string_array_set(arr: mut String[], i: int, value: String) {
  ar[i] = value
}

fn read_all(fis: move FileInputStream) -> byte[] {
  fie.readAll()
  // fis.close() called automatically.
}

fn read_some(fis: &mut FileInputStream) -> byte[] {
  // ...
}
todeskingtodesking

struct + trait + class + interface + type classがあると最強っぽい。OCamlのような強いモジュールシステムがあるとさらに良いが、その方面にはいま興味がないので……

todeskingtodesking

Rustが想定する過酷な環境においては、静的に解決される呼び出しとvtable経由の呼び出しを明示的に区別したいという要求があったわけだが、JVM言語においてそこまでは必要なさそう(記法は統一し、可能なときはコンパイラが裏で最適化すればよい)。ただgenericsの特殊化を入れる場合は考慮する必要があるか?

todeskingtodesking

ウーム

// Error: FIS is "owned class"  beacuse it implements AutoCloseable
new FileInputStream("a")
// OK
new own FileInputStream("a")
enum own? Option<+T: ?Own> {
  Some(T), None
}
impl Option<T> {
  fn map<U: ?Own, F: FnOnce(T) -> U>(self, f: F) {
    self.match { None => None, Some(t) => Some(f(t)) }
  }
}

fn read_file(file: Option<String>) {
  let ifs: own Option<own FileInputStream> = file.map { f => new own FileInputStream(f) }
  let reader =  ifs.map { a => new own InputStreamReader(a) }
  // ifs is unusable here
  reader.map { r => r.read() }
  // Call reader.close() implicitly
}

fn make_reader(is: own InputStream) {
  new own InputStreamReader(is)
}

// if T <: U, own T <: own U
make_reader(new own FileInputStream("foo"))

fn read_byte(is: &mut InputStream) { is.read_byte() }
read_byte(&mut is)
extern own class InputStream {
  fn close(self);
}

let is = new own FileInputStream("foo")

// all extern methods are unsafe unless extern type definition available
unsafe { is.read() }

is.close()
// `is` unusable here
todeskingtodesking

Scalaのvarlet mut x: &mut T相当、vallet x: &mut T相当。

Rustだと&mut Tに対して*ref = another_valueできるが、JVMでこれをやるのは無理がある(オーバーヘッドを許容すればいけるが……)

C++のconst参照的なもののほうがJVMには合っている。

todeskingtodesking

値の性質について:

リソース

使用されなくなった時点で開放される必要がある = 所有権が必要

プリミティブ

RustのCopy相当。所有権不要。

不変クラス

所有権不要。

可変クラス

Rustの場合、

let v = vec![0];
let i = &v[0];
v.clear(); // 所有権チェックによりこの操作を防がないと、iの参照先が無効となる
dbg!(i);

として容易に領域外参照が可能だが、GCが前提なら参照先のメモリが開放されることはない。
よってシングルスレッド操作において所有権は不要。

ただし可変参照/不変参照の区別があると表現力が上がって便利。

スレッドセーフなメソッド呼び出しがあるオブジェクト

スレッド間で安全に共有可能

非スレッドセーフなメソッド呼び出しがあるオブジェクト

マルチスレッドで共有する場合、排他処理を強要する機構が必要。

todeskingtodesking

所有権があると何がうれしいのか、GCを前提とした場合はどうか。

  • ライフタイムにより、値の生存期間を静的に決定できる
    • GCがあるなら、ヒープに割り当てられた領域の生存期間は気にしなくてよい
    • メモリ以外のリソース管理など、「生存期間終了時に処理をする必要がある」という状況では依然有効
  • Mutable xor sharedな借用により、所有権と可変性を明示できる
    • GC管理下 + シングルスレッドという条件なら、可変参照が複数あっても危険はない
    • メモリ以外のリソース管理には有益
    • 可変性を明示することはドキュメントとして有益
  • Send/Syncと組み合わせることで、スレッド安全性を強制できる
    • GCの有無に関わらず有益
todeskingtodesking
  • 変更不能な参照: T
  • 変更可能な参照: mut T
  • 所有権を持つ型: *T
  • *T に対する変更不能な参照: &T
  • *T に対する変更可能な参照: &mut T
class ArrayList[T] { ... }

// 所有権なしで扱えないクラス
class *InputStream { ... }

let a = new ArrayList[Int]()
let *b = new *ArrayList[Int]()
let c = new InputStream() // Error
let *d = new *InputStream()

let *m = new *Mutex[ArrayList[Int]](*b) // 所有権の移転
newThread {
  let m = &mut m
  m.synchronized { b: &mut ArrayList[Int] =>
    b.add(1)
  }
  let *d = *d;
  d.write("aaa")
}
newThread {
  let m = &mut m // これができても問題なし
  m.synchronized { b => b.add(2) }
}