Veri Türleri
Her Cairo değeri belirli bir veri türüne sahiptir, bu da Cairo'ya hangi tür verinin belirtildiğini söyler, böylece o veriyle nasıl çalışması gerektiğini bilir. Bu bölüm, veri türlerinin iki alt kümesini kapsar: skalerler ve bileşenler.
Cairo'nun statik tipli bir dil olduğunu unutmayın; bu, derleme anında tüm değişkenlerin tiplerini bilmesi gerektiği anlamına gelir. Derleyici, genellikle değerin ve kullanımının istenen türünü, değer ve kullanımına dayalı olarak çıkarabilir. Birden çok türün mümkün olduğu durumlarda, istenen çıktı türünü belirttiğimiz bir dönüşüm yöntemi kullanabiliriz.
fn main() {
let x: felt252 = 3;
let y: u32 = x.try_into().unwrap();
}
Diğer veri türleri için farklı tür ek açıklamaları göreceksiniz.
Skaler Tipler
Bir skaler tip tek bir değeri temsil eder. Cairo'nun üç temel skaler tipi vardır: felt, tamsayılar ve booleanlar. Bunları diğer programlama dillerinden tanıyor olabilirsiniz. Cairo'da nasıl çalıştıklarına geçelim
Felt Tipi
In Cairo, if you don't specify the type of a variable or argument, its type defaults to a field element, represented by the keyword felt252
. In the context of Cairo, when we say “a field element” we mean an integer in the range \( 0 \leq x < P \), where \( P \) is a very large prime number currently equal to \( {2^{251}} + 17 \cdot {2^{192}} + 1 \). When adding, subtracting, or multiplying, if the result falls outside the specified range of the prime number, an overflow (or underflow) occurs, and an appropriate multiple of \( P \) is added or subtracted to bring the result back within the range (i.e., the result is computed \( \mod P \) ).
En önemli fark, tam sayılar ile alan elemanları arasında bölmedir: Alan "elemanlarının bölünmesi (ve dolayısıyla Cairo'daki bölme) normal CPU bölmesinden "farklıdır, burada tam sayı bölmesi \( \frac{x}{y} \) \( \left\lfloor "\frac{x}{y} \right\rfloor \) olarak tanımlanır ve bölenin tam kısmı geri "döndürülür (bu yüzden ( \frac{7}{3} = 2 \) alırsınız) ve bu denklem ( "\frac{x}{y} \cdot y == x \) bağlı olarak x
'in y
'e bölünebilirliğine "göre tatmin edilir veya edilmez.
Cairo'da, \( \frac{x}{y} \) sonucu her zaman \( \frac{x}{y} \cdot y "== x \) denklemini tatmin edecek şekilde tanımlanır. Eğer y, x'i tam sayılar "olarak bölerse, Cairo'da beklenen sonucu alırsınız (örneğin ( \frac{6}{2} ) "gerçekten 3
sonucunu verir). Ancak y, x'i bölmüyorsa, şaşırtıcı bir sonuç alabilirsiniz: "örneğin, \( 2 \cdot \frac{P + 1}{2} = P + 1 \equiv 1 \mod P \) olduğundan, "Cairo'daki ( \frac{1}{2} ) değeri \( \frac{P + 1}{2} \) (ve 0 veya 0.5 "değil) olarak tanımlanır, çünkü bu yukarıdaki denklemi tatmin eder.
Tamsayı Türleri
felt252 türü, temel kütüphanedeki tüm türlerin oluşturulması için temel olarak hizmet veren temel bir türdür. Ancak, programcıların mümkün olduğunda felt252
türünün yerine integer
türlerini kullanmaları şiddetle tavsiye edilir, çünkü integer
türleri, kodda potansiyel zafiyetlere karşı ekstra koruma sağlayanek güvenlik özellikleri ile gelir, örneğin taşma ve taşma altı kontrolleri. Bu integer türlerini kullanarak, programcılar programlarının daha güvenli ve saldırılara veya diğer güvenlik tehditlerine karşı daha az hassas olduğundan emin olabilirler. Bir integer
, kesirli bir bileşeni olmayan bir sayıdır. Bu tür bildirimi, programcının integer'ı depolamak için kullanabileceği bit sayısını gösterir. Tablo 3-1, Cairo'dakiyerleşik integer türlerini gösterir. Bu çeşitlerden herhangi birini kullanarak integer değerinin türünü beyan edebiliriz.
Length | Unsigned |
---|---|
8-bit | u8 |
16-bit | u16 |
32-bit | u32 |
64-bit | u64 |
128-bit | u128 |
256-bit | u256 |
32-bit | usize |
Her çeşidin açık bir boyutu vardır. Şimdilik, usize
türü sadece u32
için bir takma addır; ancak, gelecekte Cairo MLIR'ye derlenebilecek olduğunda faydalı olabilir. Değişkenler işaretsiz olduğundan, negatif bir sayı içeremezler. Bu kod programın paniklemesine neden olacaktır:
fn sub_u8s(x: u8, y: u8) -> u8 {
x - y
}
fn main() {
sub_u8s(1, 3);
}
Tüm önceden bahsedilen tam sayı türleri bir felt252
içine sığar, ancak u256
saklanmak için 4 fazladan bit ihtiyacı var. Kaputun altında, u256
temelde 2 alana sahip bir yapıdır: u256{low: u128, high: u128}
.
Cairo ayrıca, i
öneki ile başlayan işaretli tam sayılar için de destek sağlar. Bu tam sayılar hem pozitif hem de negatif değerleri temsil edebilir, boyutları i8
'den i128
'e kadar değişir. Her işaretli çeşit \( -({2^{n - 1}}) \) ile \( {2^{n - 1}} - 1 \) arasında sayıları depolayabilir, burada n
, o çeşidin kullandığı bit sayısıdır. Yani bir i8 \( -({2^7}) \) ile \( {2^7} - 1 \) arasında sayıları depolayabilir, bu da -128
ile 127
'ye eşittir.
Tam sayı sabitlerini, Tablo 3-2'de gösterilen herhangi bir formatta yazabilirsiniz.Not edin ki, birden fazla sayısal tür olabilen sayı sabitleri, türü belirtmek için 57_u8
gibi bir tür soneki kullanmaya izin verir. Ayrıca, kod okunurluğunu artırmak için sayı sabitlerinde görsel bir ayırıcı _
kullanmak da mümkündür.
Numeric literals | Example |
---|---|
Decimal | 98222 |
Hex | 0xff |
Octal | 0o04321 |
Binary | 0b01 |
Bir tam sayının hangi türünü kullanacağınızı nasıl anlarsınız? Tam sayınızın alabileceği maksimum değeri tahmin etmeye çalışın ve uygun boyutu seçin. usize
kullanmanın başlıca durumu, bir tür koleksiyonu dizinlendiğinde olur
Sayısal İşlemler
Cairo, tüm tam sayı türleri için beklediğiniz temel matematik işlemlerini destekler: toplama, çıkarma, çarpma, bölme ve kalan. Tam sayı bölmesi sıfıra doğru kesirli kısmı atarak en yakın tam sayıya yuvarlar. Aşağıdaki kod, bir let
ifadesinde her bir sayısal işlemi nasıl kullanacağınızı gösterir:
fn main() {
// addition
let sum = 5_u128 + 10_u128;
// subtraction
let difference = 95_u128 - 4_u128;
// multiplication
let product = 4_u128 * 30_u128;
// division
let quotient = 56_u128 / 32_u128; //result is 1
let quotient = 64_u128 / 32_u128; //result is 2
// remainder
let remainder = 43_u128 % 5_u128; // result is 3
}
Bu ifadelerdeki her bir ifade, matematiksel bir operatör kullanır ve tek bir değere değerlendirilir, ardından bu değer bir değişkene bağlanır
Appendix B contains a list of all operators that Cairo provides.
Boolean Türü
Diğer çoğu programlama dilinde olduğu gibi, Cairo'daki bir Boolean türünün iki olası değeri vardır: true
ve false
. Booleanlar bir felt252
boyutundadır. Cairo'da Boolean türü bool
kullanılarak belirtilir. Örneğin:
fn main() {
let t = true;
let f: bool = false; // with explicit type annotation
}
When declaring a bool
variable, it is mandatory to use either true
or false
literals as value. Hence, it is not allowed to use integer literals (i.e. 0
instead of false) for bool
declarations.
The main way to use Boolean values is through conditionals, such as an if
expression. We’ll cover how if
expressions work in Cairo in the "Control Flow" section.
String Types
Cairo doesn't have a native type for strings but provides two ways to handle them: short strings using simple quotes and ByteArray using double quotes.
Short strings
A short string is an ASCII string where each character is encoded on one byte (see the ASCII table). For example:
'a'
is equivalent to0x61
'b'
is equivalent to0x62
'c'
is equivalent to0x63
0x616263
is equivalent to'abc'
.
Cairo uses the felt252
for short strings. As the felt252
is on 251 bits, a short string is limited to 31 characters (31 * 8 = 248 bits, which is the maximum multiple of 8 that fits in 251 bits).
You can choose to represent your short string with an hexadecimal value like 0x616263
or by directly writing the string using simple quotes like 'abc'
, which is more convenient.
Here are some examples of declaring short strings in Cairo:
fn main() {
let my_first_char = 'C';
let my_first_char_in_hex = 0x43;
let my_first_string = 'Hello world';
let my_first_string_in_hex = 0x48656C6C6F20776F726C64;
let long_string: ByteArray = "this is a string which has more than 31 characters";
}
Byte Array Strings
Cairo's Core Library provides a ByteArray
type for handling strings and byte sequences longer than short strings. This type is particularly useful for longer strings or when you need to perform operations on the string data.
The ByteArray
in Cairo is implemented as a combination of two parts:
- An array of
bytes31
words, where each word contains 31 bytes of data. - A pending
felt252
word that acts as a buffer for bytes that haven't yet filled a completebytes31
word.
This design enables efficient handling of byte sequences while aligning with Cairo's memory model and basic types. Developers interact with ByteArray
through its provided methods and operators, abstracting away the internal implementation details.
Unlike short strings, ByteArray
strings can contain more than 31 characters and are written using double quotes:
fn main() {
let my_first_char = 'C';
let my_first_char_in_hex = 0x43;
let my_first_string = 'Hello world';
let my_first_string_in_hex = 0x48656C6C6F20776F726C64;
let long_string: ByteArray = "this is a string which has more than 31 characters";
}
Compound Types
Tuple Türü
Bir tuple, çeşitli türlerdeki bir dizi değeri tek bir bileşik tür olarak bir araya getirmenin genel bir yoludur. Tupleların sabit bir uzunluğu vardır: bir kez tanımlandıktan sonra, boyutları büyüyemez veya küçülemez.
Bir tuple'ı, parantez içinde virgülle ayrılmış bir değer listesi yazarak oluştururuz. Tupledaki her pozisyonun bir türü vardır ve tupledaki farklı değerlerin türleri aynı olmak zorunda değildir. Bu örnekte isteğe bağlı tür annotasyonları ekledik:
fn main() {
let tup: (u32, u64, bool) = (10, 20, true);
}
tup
değişkeni, bir tuple'ın tek bir bileşik eleman olarak kabul edilmesi nedeniyle tüm tuple'ı bağlar. Bir tupledan tekil değerleri çıkarmak için, bir tuple değerini yapılandırmak için kalıp eşleştirmeyi kullanabiliriz, şöyle ki:
fn main() {
let tup = (500, 6, true);
let (x, y, z) = tup;
if y == 6 {
println!("y is 6!");
}
}
This program first creates a tuple and binds it to the variable tup
. It then uses a pattern with let
to take tup
and turn it into three separate variables, x
, y
, and z
. This is called destructuring because it breaks the single tuple into three parts. Finally, the program prints y is 6!
as the value of y
is 6
.
Aynı zamanda değer ve türlerle tuple'ı bildirebilir ve onu yapılandırabiliriz.Örneğin:
fn main() {
let (x, y): (felt252, felt252) = (2, 3);
}
The Unit Type ()
Bir birim türü, yalnızca bir değeri ()
olan bir türdür. Hiçbir elemanı olmayan bir tuple temsil edilir. Boyutu her zaman sıfırdır ve derlenmiş kodda var olmadığı garanti edilir.
You might be wondering why you would even need a unit type? In Cairo, everything is an expression, and an expression that returns nothing actually returns ()
implicitly.
The Fixed Size Array Type
Another way to have a collection of multiple values is with a fixed size array. Unlike a tuple, every element of a fixed size array must have the same type.
We write the values in a fixed-size array as a comma-separated list inside square brackets. The array’s type is written using square brackets with the type of each element, a semicolon, and then the number of elements in the array, like so:
fn main() {
let arr1: [u64; 5] = [1, 2, 3, 4, 5];
}
In the type annotation [u64; 5]
, u64
specifies the type of each element, while 5
after the semicolon defines the array's length. This syntax ensures that the array always contains exactly 5 elements of type u64
.
Fixed size arrays are useful when you want to hardcode a potentially long sequence of data directly in your program. This type of array must not be confused with the Array<T>
type, which is a similar collection type provided by the core library that is allowed to grow in size. If you're unsure whether to use a fixed size array or the Array<T>
type, chances are that you are looking for the Array<T>
type.
Because their size is known at compile-time, fixed-size arrays don't require runtime memory management, which makes them more efficient than dynamically-sized arrays. Overall, they're more useful when you know the number of elements will not need to change. For example, they can be used to efficiently store lookup tables that won't change during runtime. If you were using the names of the month in a program, you would probably use a fixed size array rather than an Array<T>
because you know it will always contain 12 elements:
let months = [
'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September',
'October', 'November', 'December',
];
You can also initialize an array to contain the same value for each element by specifying the initial value, followed by a semicolon, and then the length of the array in square brackets, as shown here:
let a = [3; 5];
The array named a
will contain 5
elements that will all be set to the value 3
initially. This is the same as writing let a = [3, 3, 3, 3, 3];
but in a more concise way.
Accessing Fixed Size Arrays Elements
As a fixed-size array is a data structure known at compile time, it's content is represented as a sequence of values in the program bytecode. Accessing an element of that array will simply read that value from the program bytecode efficiently.
We have two different ways of accessing fixed size array elements:
- Deconstructing the array into multiple variables, as we did with tuples.
fn main() {
let my_arr = [1, 2, 3, 4, 5];
// Accessing elements of a fixed-size array by deconstruction
let [a, b, c, _, _] = my_arr;
println!("c: {}", c); // c: 3
}
- Converting the array to a Span, that supports indexing. This operation is free and doesn't incur any runtime cost.
fn main() {
let my_arr = [1, 2, 3, 4, 5];
// Accessing elements of a fixed-size array by index
let my_span = my_arr.span();
println!("my_span[2]: {}", my_span[2]); // my_span[2]: 3
}
Note that if we plan to repeatedly access the array, then it makes sense to call .span()
only once and keep it available throughout the accesses.
Tip döküm
Cairo addresses conversion between types by using the try_into
and into
methods provided by the TryInto
and Into
traits from the core library. There are numerous implementations of these traits within the standard library for conversion between types, and they can be implemented for custom types as well.
Into
The Into
trait allows for a type to define how to convert itself into another type. It can be used for type conversion when success is guaranteed, such as when the source type is smaller than the destination type.
To perform the conversion, call var.into()
on the source value to convert it to another type. The new variable's type must be explicitly defined, as demonstrated in the example below.
fn main() {
let my_u8: u8 = 10;
let my_u16: u16 = my_u8.into();
let my_u32: u32 = my_u16.into();
let my_u64: u64 = my_u32.into();
let my_u128: u128 = my_u64.into();
let my_felt252 = 10;
// As a felt252 is smaller than a u256, we can use the into() method
let my_u256: u256 = my_felt252.into();
let my_other_felt252: felt252 = my_u8.into();
let my_third_felt252: felt252 = my_u16.into();
}
TryInto
Similar to Into
, TryInto
is a generic trait for converting between types. Unlike Into
, the TryInto
trait is used for fallible conversions, and as such, returns Option<T>. An example of a fallible conversion is when the target type might not fit the source value.
Also similar to Into
is the process to perform the conversion; just call var.try_into()
on the source value to convert it to another type. The new variable's type also must be explicitly defined, as demonstrated in the example below.
fn main() {
let my_u256: u256 = 10;
// Since a u256 might not fit in a felt252, we need to unwrap the Option<T> type
let my_felt252: felt252 = my_u256.try_into().unwrap();
let my_u128: u128 = my_felt252.try_into().unwrap();
let my_u64: u64 = my_u128.try_into().unwrap();
let my_u32: u32 = my_u64.try_into().unwrap();
let my_u16: u16 = my_u32.try_into().unwrap();
let my_u8: u8 = my_u16.try_into().unwrap();
let my_large_u16: u16 = 2048;
let my_large_u8: u8 = my_large_u16.try_into().unwrap(); // panics with 'Option::unwrap failed.'
}