Doğrusal Tip Sistemi Kullanarak Sahiplik

Cairo, doğrusal bir tip sistemini kullanır. Bu tip sistemde, herhangi bir değer (bir temel tip, bir yapı, bir enum) kullanılmalı ve yalnızca bir kez kullanılmalıdır. 'Kullanılmış' burada, değerin ya yok edilmesi ya da taşınması anlamına gelir.

Yok etme birkaç şekilde gerçekleşebilir:

  • a variable goes out of scope.
  • a struct is destructured.
  • explicit destruction using destruct().

Bir değeri taşımak basitçe o değeri başka bir fonksiyona geçirmek demektir.

Bu, Rust sahiplik modeline benzer bazı kısıtlamalar sonucunu doğurur, ancak bazı farklılıklar vardır. Özellikle, Rust sahiplik modeli kısmen veri yarışlarını ve bir bellek değerine yönelik eşzamanlı değişebilir erişimi önlemek için var olur. Bu, Cairo'da imkansızdır çünkü bellek değişmez. Bunun yerine, Cairo doğrusal tip sistemini iki ana amaç için kullanır:

  • Tüm kodun kanıtlanabilir ve dolayısıyla doğrulanabilir olduğunu sağlamak.
  • Cairo VM'nin değişmez belleğini soyutlamak.

Sahiplik

Cairo'da, sahiplik değerlere değil, değişkenlere uygulanır. Bir değer, değer kendisi her zaman değişmez olduğu için birçok farklı değişken tarafından güvenle atıfta bulunulabilir (hatta değişebilir değişkenler olsalar bile). Ancak değişkenler değişebilir olabilir, bu yüzden derleyici, sabit değişkenlerin yanlışlıkla programcı tarafından değiştirilmediğinden emin olmalıdır. Bu, bir değişkenin sahipliğinden bahsetmeyi mümkün kılar: sahip, değişkeni okuyabilen (ve değişebilir ise yazabilen) koddur.

Bu, değişkenlerin (değerler değil) Rust değerlerine benzer kuralları izlediği anlamına gelir:

  • Cairo'daki her değişkenin bir sahibi vardır.
  • Bir zaman da yalnızca bir sahip olabilir.
  • Sahip kapsam dışına çıktığında, değişken yok edilir.

Now that we’re past basic Cairo syntax, we won’t include all the fn main() { code in examples, so if you’re following along, make sure to put the following examples inside a main function manually. As a result, our examples will be a bit more concise, letting us focus on the actual details rather than boilerplate code.

Değişken Kapsamı

Doğrusal tip sisteminin ilk örneği olarak, bazı değişkenlerin kapsamına bakacağız. Bir kapsam, bir program içinde bir öğenin geçerli olduğu aralıktır. Aşağıdaki değişkeni alın:

let s = 'hello';

s değişkeni kısa bir metni ifade eder. Değişken, tanımlandığı noktadan itibaren geçerli olduğu mevcut kapsamın sonuna kadar geçerlidir. Liste 4-1, s değişkeninin nerede geçerli olacağını açıklayan yorumlarla bir programı gösterir.

//TAG: ignore_fmt
fn main() {
    { // s is not valid here, it’s not yet declared
        let s = 'hello'; // s is valid from this point forward
        // do stuff with s
    } // this scope is now over, and s is no longer valid
}

Listing 4-1: A variable and the scope in which it is valid

Diğer bir deyişle, burada iki önemli zaman noktası vardır:

  • s kapsama girdiğinde, geçerlidir.
  • Kapsam dışına çıkana kadar geçerli kalır.

At this point, the relationship between scopes and when variables are valid is similar to that in other programming languages. Now we’ll build on top of this understanding by using the Array type we introduced in the previous "Arrays" section.

Moving values

Daha önce belirtildiği gibi, bir değeri taşımak basitçe o değeri başka bir fonksiyona geçirmek demektir. Bu olduğunda, orijinal kapsamdaki değere atıfta bulunan değişken yok edilir ve artık kullanılamaz hale gelir ve aynı değeri tutmak için yeni bir değişken oluşturulur.

Diziler, başka bir fonksiyona geçirildiğinde taşınan karmaşık bir tipin bir örneğidir. İşte bir dizinin nasıl göründüğüne dair kısa bir hatırlatma:

fn main() {
    let mut arr: Array<u128> = array![];
    arr.append(1);
    arr.append(2);
}

Tip sistemi, Cairo programının asla aynı bellek hücresine iki kez yazmaya çalışmamasını nasıl sağlar? Aşağıdaki kodu düşünün, burada dizinin önünü iki kez kaldırmaya çalışıyoruz:

fn foo(mut arr: Array<u128>) {
    arr.pop_front();
}

fn main() {
    let arr: Array<u128> = array![];
    foo(arr);
    foo(arr);
}

Bu durumda, aynı değeri (arr değişkenindeki dizi) her iki fonksiyon çağrısına da geçirmeye çalışıyoruz. Bu, kodumuzun ilk elemanı iki kez kaldırmaya çalıştığı anlamına gelir ki bu, aynı bellek hücresine iki kez yazmaya çalışır - bu Cairo VM tarafından yasaklanmıştır ve bir çalışma zamanı hatasına yol açar. Neyse ki, bu kod aslında derlenmez. Diziyi foo fonksiyonuna geçirdikten sonra, arr değişkeni artık kullanılamaz. Bu derleme zamanı hatasını alırız, bize Array'in Copy Trait'ini uygulaması gerektiğini söyler:

$ scarb cairo-run 
   Compiling no_listing_02_pass_array_by_value v0.1.0 (listings/ch04-understanding-ownership/no_listing_02_pass_array_by_value/Scarb.toml)
warn: Unhandled `#[must_use]` type `core::option::Option::<core::integer::u128>`
 --> listings/ch04-understanding-ownership/no_listing_02_pass_array_by_value/src/lib.cairo:3:5
    arr.pop_front();
    ^*************^

error: Variable was previously moved.
 --> listings/ch04-understanding-ownership/no_listing_02_pass_array_by_value/src/lib.cairo:9:9
    foo(arr);
        ^*^
note: variable was previously used here:
  --> listings/ch04-understanding-ownership/no_listing_02_pass_array_by_value/src/lib.cairo:8:9
    foo(arr);
        ^*^
note: Trait has no implementation in context: core::traits::Copy::<core::array::Array::<core::integer::u128>>.

error: could not compile `no_listing_02_pass_array_by_value` due to previous error
error: `scarb metadata` exited with error

The Copy Trait

The Copy trait allows simple types to be duplicated by copying felts, without allocating new memory segments. This contrasts with Cairo's default "move" semantics, which transfer ownership of values to ensure memory safety and prevent issues like multiple writes to the same memory cell. Copy is implemented for types where duplication is safe and efficient, bypassing the need for move semantics. Types like Array and Felt252Dict cannot implement Copy, as manipulating them in different scopes is forbidden by the type system.

All basic types previously described in "Data Types" implement by default the Copy trait.

While Arrays and Dictionaries can't be copied, custom types that don't contain either of them can be. You can implement the Copy trait on your type by adding the #[derive(Copy)] annotation to your type definition. However, Cairo won't allow a type to be annotated with Copy if the type itself or any of its components doesn't implement the Copy trait.

#[derive(Copy, Drop)]
struct Point {
    x: u128,
    y: u128,
}

fn main() {
    let p1 = Point { x: 5, y: 10 };
    foo(p1);
    foo(p1);
}

fn foo(p: Point) { // do something with p
}

In this example, we can pass p1 twice to the foo function because the Point type implements the Copy trait. This means that when we pass p1 to foo, we are actually passing a copy of p1, so p1 remains valid. In ownership terms, this means that the ownership of p1 remains with the main function. If you remove the Copy trait derivation from the Point type, you will get a compile-time error when trying to compile the code.

Don't worry about the Struct keyword. We will introduce this in Chapter 5.

Destroying Values - Example with FeltDict

The other way linear types can be used is by being destroyed. Destruction must ensure that the 'resource' is now correctly released. In Rust, for example, this could be closing the access to a file, or locking a mutex. In Cairo, one type that has such behaviour is Felt252Dict. For provability, dicts must be 'squashed' when they are destructed. This would be very easy to forget, so it is enforced by the type system and the compiler.

No-op Destruction: the Drop Trait

Önceki örnekteki Point tipinin de Drop özelliğini uyguladığını fark etmiş olabilirsiniz. Örneğin, aşağıdaki kod, A yapısı kapsam dışına çıkmadan önce taşınmadığı veya yok edilmediği için derlenmez:

struct A {}

fn main() {
    A {}; // error: Variable not dropped.
}

Ancak, Drop özelliğini uygulayan tipler, kapsam dışına çıktıklarında otomatik olarak yok edilir. Bu yok etme işlemi hiçbir şey yapmaz, bir etkisiz işlemdir - sadece bu tipin artık yararlı olmadığında güvenle yok edilebileceğine dair derleyiciye bir ipucudur. Buna bir değeri "bırakmak" diyoruz.

Şu anda, Drop uygulaması, sözlükler (Felt252Dict) ve sözlük içeren tipler hariç, tüm tipler için türetilebilir ve bunların kapsam dışına çıktıklarında bırakılmalarına izin verilir. Örneğin, aşağıdaki kod derlenir:

#[derive(Drop)]
struct A {}

fn main() {
    A {}; // Now there is no error.
}

Destruction with a Side-effect: the Destruct Trait

Bir değer yok edildiğinde, derleyici önce o tip üzerinde drop metodunu çağırmayı dener. Eğer bu mevcut değilse, derleyici bunun yerine destruct'u çağırmayı dener. Bu metod, Destruct özelliği tarafından sağlanır.

Daha önce belirtildiği gibi, Cairo'daki sözlükler, yok edildiklerinde "ezilmeleri" gereken tiplerdir, böylece erişim sırası kanıtlanabilir. Bu, geliştiricilerin unutması kolaydır, bu yüzden sözlükler kapsam dışına çıktıklarında tüm sözlüklerin ezilmesini sağlamak için Destruct özelliğini uygularlar. Bu nedenle, aşağıdaki örnek derlenmez:

use core::dict::Felt252Dict;

struct A {
    dict: Felt252Dict<u128>,
}

fn main() {
    A { dict: Default::default() };
}

Bu kodu çalıştırmayı denerseniz, bir derleme zamanı hatası alırsınız:

$ scarb cairo-run 
   Compiling no_listing_06_no_destruct_compile_fails v0.1.0 (listings/ch04-understanding-ownership/no_listing_06_no_destruct_compile_fails/Scarb.toml)
error: Variable not dropped.
 --> listings/ch04-understanding-ownership/no_listing_06_no_destruct_compile_fails/src/lib.cairo:9:5
    A { dict: Default::default() };
    ^****************************^
note: Trait has no implementation in context: core::traits::Drop::<no_listing_06_no_destruct_compile_fails::A>.
note: Trait has no implementation in context: core::traits::Destruct::<no_listing_06_no_destruct_compile_fails::A>.

error: could not compile `no_listing_06_no_destruct_compile_fails` due to previous error
error: `scarb metadata` exited with error

When A goes out of scope, it can't be dropped as it implements neither the Drop (as it contains a dictionary and can't derive(Drop)) nor the Destruct trait. To fix this, we can derive the Destruct trait implementation for the A type:

use core::dict::Felt252Dict;

#[derive(Destruct)]
struct A {
    dict: Felt252Dict<u128>,
}

fn main() {
    A { dict: Default::default() }; // No error here
}

Şimdi, A kapsam dışına çıktığında, sözlüğü otomatik olarak ezilecek ve program derlenecek.

Copy Array Data with clone

If we do want to deeply copy the data of an Array, we can use a common method called clone. We’ll discuss method syntax in a dedicated section in Chapter 5, but because methods are a common feature in many programming languages, you’ve probably seen them before.

İşte clone metodunun eylemdeki bir örneği.

fn main() {
    let arr1: Array<u128> = array![];
    let arr2 = arr1.clone();
}

When you see a call to clone, you know that some arbitrary code is being executed and that code may be expensive. It’s a visual indicator that something different is going on. In this case, the value arr1 refers to is being copied, resulting in new memory cells being used, and a new variable arr2 is created, referring to the new copied value.

Dönüş Değerleri ve Kapsam

Değerleri döndürmek, onları taşımak ile eşdeğerdir. Liste 4-2, benzer notasyonlarla bazı değerler döndüren bir fonksiyonun bir örneğini gösterir.

Filename: src/lib.cairo

#[derive(Drop)]
struct A {}

fn main() {
    let a1 = gives_ownership();           // gives_ownership moves its return
                                          // value into a1

    let a2 = A {};                        // a2 comes into scope

    let a3 = takes_and_gives_back(a2);    // a2 is moved into
                                          // takes_and_gives_back, which also
                                          // moves its return value into a3

} // Here, a3 goes out of scope and is dropped. a2 was moved, so nothing
  // happens. a1 goes out of scope and is dropped.

fn gives_ownership() -> A {               // gives_ownership will move its
                                          // return value into the function
                                          // that calls it

    let some_a = A {};                    // some_a comes into scope

    some_a                                // some_a is returned and
                                          // moves ownership to the calling
                                          // function
}

// This function takes an instance some_a of A and returns it
fn takes_and_gives_back(some_a: A) -> A { // some_a comes into scope

    some_a                                // some_a is returned and 
                                          // moves ownership to the calling
                                          // function
}

Listing 4-2: Moving return values

Bu işe yarasa da, her fonksiyona girip çıkmak biraz yorucu. Bir değeri bir fonksiyona kullanması için vermek ama değeri taşımamak istiyorsak ne olur? İstediğimiz her şeyi içeri vermek ve tekrar kullanmak istiyorsak geri vermek zorunda olmamız oldukça can sıkıcı, fonksiyonun gövdesinden kaynaklanan herhangi bir veriyi de döndürmek isteyebiliriz.

Cairo bize bir demet kullanarak birden fazla değeri döndürme imkanı tanır, Liste 4-3'te gösterildiği gibi.

Filename: src/lib.cairo

fn main() {
    let arr1: Array<u128> = array![];

    let (arr2, len) = calculate_length(arr1);
}

fn calculate_length(arr: Array<u128>) -> (Array<u128>, usize) {
    let length = arr.len(); // len() returns the length of an array

    (arr, length)
}

Listing 4-3: Returning many values

Ama bu çok fazla tören ve olması gereken bir kavram için çok fazla iş. Neyse ki, Cairo bizim için bir değeri yok etmeden veya taşımadan geçirmek için referanslar ve anlık görüntüler adında iki özelliğe sahip.