🦔

Cadenceのかんどころ(3)参照型

2022/08/20に公開

前回

前回はCadenceにおけるストレージの扱いについて説明した。今回はCadenceにおける参照型を解説する傍らで、裏で動くCadenceインタプリタの動きにも触れる。

参照型

参照型(Reference Type)は変数に別の呼び名を付けるような意味合いであり、ポインタとは違う。そして、Cadenceの参照型はストラクト型を継承した型である(復習:Cadenceの変数の型はストラクト型(=AnyStruct型)とリソース型(=AnyResource型)の二種類に分けられる)。ストラクト型ではあるが、Storableではないためストレージに保管することはできない。

Rustの参照型のようなものを想定していたのだが、それだと理解できなかった挙動があった。以下のコードの最後のlogを見てほしい。

var a: Int = 10
var b: Int = a
var r: &Int = &a as &Int

log(a) // 10
log(b) // 10
log(r) // 10

a = 100
b = 200
log(a) // 100
log(b) // 200
log(r) // 10 !!!

参照元aの値を変更しても、参照rが指す値は変更されなかった。

インタプリタの挙動を見てみる

少し調べてみると、上の状態の前半の変数の初期化部分はCadenceインタプリタ内で下のように表現されているようだった。

ひとつ前の記事でもマニアックな話題として紹介したが、Cadenceのすべての値は内部的にはValueインターフェースで表現されている。そしてそれらの値が何かしらの変数に保持されるときにはVariable構造体に置かれる。Variable構造体のフィールドにはvalueというポインタがあり、それが、値を表すValueインターフェイスを実装した構造体を指し示すことになる。


// runtime/interpreter/value.go
// Cadenceのすべての値を表現するインターフェース
type Value interface {
  atree.Value
  fmt.Stringer
  IsValue()
  Accept(interpreter *Interpreter, visitor Visitor)
  Walk(interpreter *Interpreter, walkChild func(Value))
  StaticType(interpreter *Interpreter) StaticType
  ...
  IsResourceKinded(interpreter *Interpreter) bool
  ...
}

...

// runtime/interpreter/variable.go
// Cadenceの変数を表現する構造体
type Variable struct {
  value  Value
  getter func() Value
}

変数名と変数の結び付けはシンプルなmapで行なっている。

そして参照型はこのインターフェースを継承したEphemeralReferenceValueというgoの構造体で表現される。

type EphemeralReferenceValue struct {
  Authorized   bool
  Value        Value
  BorrowedType sema.Type
}

これを見るとEphemeralReferenceValue構造体はメンバにValue型のvalueを持つことが分かる。EphemeralReferenceValue構造体自体はValueインターフェースの実装なので、入れ子構造になっているのだ。この中に入っているValueがCadenceで参照型の値が指し示す実値になる。

最初に紹介したCadenceコードで

a = 100
b = 200

とした後にはインタプリタ内表現は以下のように変わる。この図内の三角括弧<I>はジェネリクスではなく、括弧内のインターフェースIを実装しているという意味になっている。

a=100 としたときに起こっていることは、最初にaが指していたIntValueの中身10を100に変更するのではなく、aが指していたVariableのなかのvalueに新しいIntValueを渡すことをしている。b=200の部分でも、元々持っていたIntValueの中を変更するのではなく、200という中身を持ったIntValueを渡していることが分かった。

配列型

以下のように配列型の変数と、配列型への参照型の変数を初期化する。(配列の紹介はしなかったので、配列そのものについて知りたい方は公式ドキュメントを参照していただきたい)

var a: [Int; 3] = [10, 11, 12]
var r: &[Int; 3] = &a as &[Int; 3]

log(a) // [10, 11, 12]
log(r) // [10, 11, 12]

この時のインタプリタ内のかたちは以下のようになっている。配列はArrayValue構造体というValueインターフェースの実装によって表現されている。ArrayValue自体が配列の要素のValueを読んだり書いたりしている。

この時、aやrの配列の要素を変更するとどうなるのか予想してみよう。

a[1] = 100
r[2] = 200

// どうなるか?

答え。

// 両方に変更が加わる
log(a) // [10, 100, 200]
log(r) // [10, 100, 200]

配列の場合、両者が同じArrayValueを共有しているため、配列の要素に変更を加えると、もう片方にもその変更が見える。インタプリタ内の表現は下のようになる。

ちなみに、参照ではなく、配列自体を値渡しでvar b: [Int; 3]= aという風にすると、deepcopyして同じ中身のArrayValueが作られるため、片方の変更は伝わらない、それぞれが独立した値になる。

CadenceのDictionary型はインタプリタ上のDictionaryValue型で表現されるのだが、仕組みは同じと考えてよい。ArrayValueでは要素を数字で指定するが、DictionaryValueでは要素を任意のValue型で指定できるだけの違いである。

ユーザー定義ストラクト型

ユーザー定義されるストラクト型の場合は、ArrayValueの代わりにCompositeValue型というValueインターフェースを実装した構造体が用意されている。実は配列と動きは同じで、要素(メンバ変数)に対する変更は元の値と参照値で共有し、片方への変更はもつれ合うように他方に伝わる。

値渡しするとdeepcopyされるので同じ構造体が丸ごともう一つ出来上がる。具体例を下に置いておく。

pub struct Player {
    pub(set) var age: Int

    pub init(age: Int) {
        self.age = age
    }

    pub fun happyBirthday() {
        self.age = self.age + 1
    }
}

pub fun main() {
  var p: Player = Player(age: 20)
  var pp: &Player = &p as &Player   // pass by reference
  var p2: Player = p     // pass by value = deepcopy
  var pp2: &Player = pp  // pass by reference

  p.happyBirthday()
  log(p.age)      // 21
  log(pp.age)     // 21
  log(p2.age)     // 20
  log(pp2.age)    // 21

  pp.happyBirthday()
  log(p.age)      // 22
  log(pp.age)     // 22
  log(p2.age)     // 20
  log(pp2.age)    // 22

  p.age = 100
  log(p.age)      // 100
  log(pp.age)     // 100
  log(p2.age)     // 20
  log(pp2.age)    // 100

  pp.age = 200
  log(p.age)      // 200
  log(pp.age)     // 200
  log(p2.age)     // 20
  log(pp2.age)    // 200
}

また、配列も構造体も、関数の引数として渡された場合は値渡しとなる。そのため深くネストしている構造体を渡すときには、無駄なクローン処理が発生する。もし、実引数と仮引数が値を共有していいのなら、参照渡しすることをお勧めする。

リソース型

リソース型もCompositeValue型でgoでは表されているが、IsResourceKinded()というメンバ関数がtrueを返すようにして両者の見分けを付ける。それだけの違いなので参照型がリソース型を参照している場合も、ストラクト型と考え方は同じである。つまり参照渡しはストラクト型の場合と同じ動きになる。しかしリソース型の値を別のリソース型の変数に値渡し(代入)することは出来ない。これは複製を禁じるリソース型の制約のためだ。その代わり用意されているmove演算子によってリソースが移動するときは、新しい変数にValueが移る。

さらに深い話…

実は、配列の要素やオブジェクトのメンバ変数は、atreeと呼ばれる木構造のKey-Valueマップのようなデータ構造に保管されている。上の図のようにvalueポインタで直接指し示されているわけではない。これについては記事のボリューム的に今回は話さない。

おわりに

次回はCadenceでリソース型に次いで重要な概念となるCapabilityを紹介する。

Discussion