Submind YouTube summaries
Thumbnail for Odin Intro (1 / 3) - Data Types

Odin Intro (1 / 3) - Data Types

Watch on YouTube

Video summary

Bu video, Odin programlama dilinin temel veri tiplerine odaklanan bir giriş niteliğindedir ve izleyicilerin C, Go, Rust veya Zig gibi benzer dillerle olan temel bilgilerini ön koşul olarak kabul ederken, dilin daha karmaşık detaylarını resmi kaynaklara yönlendirerek konuyu başlatır. İlk bölümde sayısal, mantıksal (boolean) ve metinsel veri tipleri ile işaretçiler incelenirken, ikinci ve üçüncü bölümlerde sıralar, dilimeler, haritalar, birleşimler, sayımsallar ve polimorfizm gibi konulara geçileceği belirtilir. Odin'in çöp toplanma mekanizması olmayan bir dil olduğu vurgulanarak, bellek yönetiminin programcının sorumluluğunda olduğunu ve bu bağlamda statik olarak tahsis edilen kod alanındaki veri ile runtime'da dinamik olarak tahsis edilen veriler arasındaki farklar açıklanır. Ayrıca, tüm veri tiplerinin sıfır değerine sahip olması gerektiği ve Odin'in açık döküm (explicit casting) konusunda katı kuralları uyguladığı, özellikle küçük sayı türlerinin büyük türlere atanmasında bile bunun zorunlu kıldığı belirtilir. Metin işleme ve bellek yönetimi açısından Odin'de string tipleri, UTF-8 veya UTF-16 kodlamalı veri tutarken, C ile uyumluluk için null bit işaretli alternatif tipler de sunulur. Özellikle pointer kavramı detaylı bir şekilde ele alınarak, dilin C'deki yıldız işareti yerine kök (carrot) sembolünü tercih etmesinin, dizi indeksleme ve referans alma işlemlerinde okunabilirliği artırdığı anlatılır. Odin'de pointer aritmetiği doğrudan yapılamaz ancak unsigned integer pointer üzerinden dönüştürülerek gerçekleştirilebilirken, bu işlemlerin güvenli olmadığını ve sadece tahsis edilmiş bellek bölgeleri üzerinde kullanılması gerektiği uyarısı yapılır. Ayrıca, dinamik bellek yönetimi için kullanılan allocatörler (bellek tahsis ediciler) ve bunların nasıl kullanılacağı, özellikle geçici tahsisler ve grup halinde serbest bırakma gibi senaryolar üzerinden örneklerle açıklanır. Veri yapıları bölümünde dizi, dilimeler, dinamik diziler ve haritaların özellikleri karşılaştırılır; Odin'de dizilerin sabit boyutlu olması ve değer olarak kopyalanırken, dilimelerin alt aralıkları temsil etmesi ve dinamik dizilerin ise kapasite yönetimi sayesinde büyüme imkanı sunması vurgulanır. Birleşim (union) tipleri ve sayımsalların hata yönetimi nasıl kullanıldığı detaylı bir şekilde anlatılır; Odin'de istisna mekanizmasının bulunmaması nedeniyle hataların sıfır olmayan değerler, boolean'lar veya sayımsallar ile temsil edildiği ve bunların son döndürülen değer olarak geri verildiği açıklanır. Hata yönetimi için geliştirilmiş `or return`, `or break` ve `or else` gibi özel operatörlerin, kodun daha temiz ve hatayı ihmal etmeyi önleyici bir şekilde yazılmasını sağladığı örneklerle gösterilir. Sonuç olarak, Odin'in güvenli bellek yönetimi, açık döküm kuralları ve esnek veri yapıları ile modern sistem programlama dillerinde sağlam bir temel sunduğu özetlenir.
Read the full video transcript
This video is an intro to Odin that tackles the language primarily from the angle of data types. In the first part, we'll cover the most basic number, boolean, and text types plus pointers. In part two, we'll cover arrays, slices, maps, and strrus. In part three, we'll cover enums and unions, including how they are used as error types. Lastly, we'll look at polymorphism in two parts. The first about compile time polymorphism, and the second about runtime polymorphism. The video assumes that the viewer has at least some familiarity with similar languages such as C, Go, Rust, or Zigg, but only the basics. As long as you have a general idea of what a strruct or pointer is, you should be able to follow along. Though Odin is a relatively simple language compared to most others, we won't exhaustively cover every topic and every detail. For more complete coverage, you should look at the official Odin site. And I also recommend Carl Zalinsk's YouTube channel and his book understanding the Odin programming language. So first topic integers. The integer types come in five different sizes with both signed and unsigned variants. For example, an i16 is a signed 16- bit integer while a u 64 is an unsigned 64-bit integer. The types int and uint are generally your default choices and their sizes depend on the target platform you're compiling for. For example, when compiling for x64 ins and uints will both be 64 bits. There's also the type called bite which is actually just an alias for u8. There are three sizes of floating point 16 bit 32-bit and 64-bit. If you really need high precision, you can use an F-64, but F-32 is generally fine for most cases. A bit surprisingly, booleans also come in multiple sizes, 8, 16, 32, and 64-bit. Even though, of course, in principle, a boolean only requires a single bit. The reason Odin has these options is mainly to allow easier interop with various binary formats and to allow you to better control padding and alignment in strrus. Most of the time though, you'll simply default to using the type called bool which like B8 is 8 bits in size. The primary string type called string represents a UTF8 encoded string and the type called string 16 represents a UTF-16 encoded string. Concretely, a string value is actually a pointer to a buffer of characters plus an integer representing the length of the text. So when you assign pass or return a string value, what's actually being copied is just a pointer and an integer, not the actual character data. For ease of interrupt with C, Odin also has types C string and cring 16. These cring types have no integer to represent the length because they instead use the C convention of signaling the end of the character data with a zero bite. Lastly, Odin also has another integer type called rune that represents the Unicode code point of an individual character. Now, because Odin is not a garbage collected language, it's important to keep in mind how the character buffers pointed to by strings are allocated. For a string literal, the character buffer is statically allocated, meaning the data resides alongside the code of the executable itself. But for any string created at runtime, the character buffer must be allocated dynamically, a topic we'll discuss later. All data types in Odin have a concept of a zero value, meaning the value of the type represented by all zero bits. When a variable is left uninitialized, Odin by default will zero out its bits, giving it the zero value. For number types, the zero value is of course the number zero. For boolean types, the zero value is false. For pointers, the zero value is nil, indicating an address that points to nothing. For strings, the zero value has a nil pointer plus a length of zero. For a strruct, the zero value has fields which are all themselves zero values. And for a union, the zero value is usually nil, though there will be more to say about this when we talk about unions later. Compared to C and some other languages, Odin is much stricter about explicit casting. For example, to assign this i32 variable to an i64 variable, the cast cannot be left implicit. Even though a smaller integer type can be cast into a larger integer type without distortion, Odin wants us to make the cast explicit anyway to help prevent absent-minded mistakes. By the way, note here the declaration syntax in Odin. A declared variable's name is followed by a colon and then the variable's type. Literal in Odin have their own distinct types which a bit confusingly are called the untyped types. Integer literal are untyped integers. Floating point literal are untyped floats. Boolean literal are untyped booleans. And string literal are untyped strings. These special untyped types have a few special rules. First, they only exist at compile time. So you can't say create a variable with one of these untyped types. Second, these types can be implicitly cast to their related types. Here, for example, the literal 14 is being implicitly cast to F-32 and the literal 9 is being implicitly cast to a U8. The third special thing about these untyped types is that these implicit casts perform range checks. So here, when we try to assign 1,00 to a U8 variable, the compiler gives us an error because 1,000 exceeds the valid range of a U8. In the case of booleans, untyped boolean can be implicitly cast to any of the boolean types. And likewise, in the case of strings, untyped string can be implicitly cast to any of the string types. Again, understand that these untyped types only exist at compile time. So for the sake of variable declarations with inferred types, an untyped integer is inferred to be an int. An untyped float is inferred to be an F-64. An untyped boolean is inferred to be a bool. And an untyped string is inferred to be a string rather than a string 16 or one of the other string types. Also note here the syntax. When the type of a variable declaration is left inferred from the assignment, we put a space between the variable name and the colon and we remove any spaces between the colon and the equal sign. Be clear though that this is just a formatting convention. The colon and equal sign are actually separate symbols. The only substantive syntax change here from prior examples is that we've emitted the variable types after the colons. As mentioned at the start, this video assumes a basic familiarity with cike language concepts including pointers. So I won't give a lengthy introduction here. But in short, a pointer is a value that represents a memory address. And very importantly, pointers are typed. Meaning that say an int pointer is intended to represent the memory address of only ins while say a string pointer is intended to represent the memory addresses of only strings and so forth. By virtue of being typed in this way, the compiler can know that when you dreference a pointer, when you read the value at the memory address, the compiler can know that the dreferenced value is of the pointer's designated type. For example, if you dreference an int pointer, you should get back an int value, not a float or a boolean or anything else. And thanks to the statically declared types, the compiler can ensure at compile time that this will be the case at runtime. Anyway, hopefully that's a sufficient explanation for you to follow along. So, here is our first pointer variable. This variable p is declared to be an int pointer. And note that Odin uses the carrot symbol instead of C's traditional asterisk. If we then also create a regular int variable I, we can get a pointer value representing the address of I by using amperand the address operator. Using the address operator on an int variable gets us an int pointer value which we can assign to p. To get the value referenced by p, we can use the dreference operator which is again the carrot symbol but placed on the right side of its operand not the left. here in the first line because P references the location of I, dreerencing P gets us the int value stored in I which we here then assign to int variable X. In the second line, the target of assignment is the dreference of P. So we're assigning the value three to the location stored in P, which is still the address of I. Now, you might be wondering why Odin puts the dreference operator on the right rather than keep it on the left like in C and most other C-ike languages. Well, moving it to the right works out nicely when pointers are used in combination with arrays. We'll cover arrays later, but really briefly here, we're declaring a variable P to be a pointer to an array of five ins and then assigning the address of an actual array of five ins to P. When we then dreference P to get the array, we can also tack on the index operator to get a value from the array. And because the dreference operator and the index operator both go on the right side of their operands, they can be easily read left to right. In contrast, the equivalent in the traditional C style syntax requires us to worry about operator precedence. For instance, in Go, which uses the C style syntax, the equivalent expression requires PNS to get the right order of operations. And then in the opposite case where we instead have an array of five int pointers again Odin allows us to simply read indexing and dreference expressions left to right. So here we are getting the first pointer in the array and then dreferencing that pointer. As we've established a pointer normally has a designated type but sometimes it's useful to have an untyped pointer that can represent the memory address of anything. In C this is called a void pointer but in Odin the closest analog is called a raw pointer. Here we're creating a variable R of type raw pointer and then we can assign the address of anything such as an int variable to this pointer. This address operator expression returns an int pointer but we don't have to explicitly cast the raw pointer because as a special rule Odin allows implicit casts from any pointer type to raw pointer. Going in the other direction however always requires an explicit cast. Here we create an intpo pointer variable and then assign it to the raw pointer value using an explicit cast. Now in this case we know the raw pointer was referencing an int variable. So this cast makes sense but Odin will actually let us explicitly cast the raw pointer to any pointer type. Here for example we're casting the same raw pointer value to a string pointer. If we were to then dreference the string pointer, we would be interpreting the data at that location as if it is a string, potentially with disastrous results. Even though this is unsafe, Odin still allows it because one, there are some cases when reinterpreting binary data is actually useful. And two, the compiler cannot track the actual values of variables, only their types. So it never knows what a raw pointer actually points to. Therefore, the compiler must let us cast a raw pointer into any pointer type. And it's our responsibility to only perform casts that make sense. Similar to raw pointer, we also have uintpointer which is so named because it's an unsigned integer which is the same size as a pointer. Like raw pointer, we can cast any pointer to uint pointer but we must do so explicitly. Unlike C or other C like languages, Odin doesn't let us do arithmetic directly on pointers, but instead we can convert a pointer into a UNP pointer, perform the arithmetic, and then cast back to a pointer. In this example, we're taking the size of int, multiplying it by five, and then adding that to our Uint pointer before casting it to an int pointer. Just be clear that pointer arithmetic is potentially unsafe as in this case where we're effectively creating a pointer to a meaningless location on the call stack that is five ins up in memory from the variable I. In real use cases, you should be careful that the result of your point arithmetic always points into known allocated buffers of memory. Now, while we can do point arithmetic with pointers, more commonly in Odin, we should use instead what Odin calls a multin. A multipointer is a bit more convenient and a bit less errorprone though still fundamentally unsafe. Here the variable m is a multointer of int as denoted by the square brackets surrounding the carrot. We can assign an int pointer to m with a cast to multipointer left implicit. Instead of using the dreference operator for a multipointer we use the index operator the square brackets. In the first assignment here, the int value 100 is being assigned to the location 3 ins up in memory from the address represented by the pointer. In the last line, we're reading the int value at the location that is 5 in down in memory from the address represented by the pointer. Again, keep in mind that arbitrarily indexing memory is fundamentally unsafe as in this case where we are jumping to meaningless locations on the coal stack. In real use cases, multipointers should generally only be used to index within known allocated buffers of memory. Here in this code, we're declaring a variable R, which is an array of five ins. A local variable array in Odin is fixed in size and stored directly on the stack. So the variable R here has the size of five ins. And because arrays in Odin are fixed in size, the size must always be specified by a compile time integer expression. Arrays can be expressed as literals such as here where we assigned to R an array of five ins where the values are 1 2 3 4 and 5. Also as shorthand, we could leave the size and type implicit to be inferred from the assignment target. When declaring a new array variable, we can use a question mark where the size normally goes to have the size be inferred from the number of elements inside the curly braces. So this array nums is inferred to be size three. Another option with array literal is that we can designate values for explicit indexes in which case we don't have to specify values for every index and the values can be written in non-sequential order. The assignment here assigns apple to index 4, banana to index 1, and orange to index 3, while the emitted indexes 0 and 2 default to empty strings. The indexes in array literal can also be specified as ranges. In this case, indexes 100 up through 200 are all being assigned the string banana, while indexes 300 up to, but not including 400 are assigned the string orange. Whereas an array variable in C is actually a constant pointer value, this is not the case in Odin. An Odin array is a proper value unto itself. And so arrays are assigned past, compared, and returned by value, not by reference. So here, when we assign one array variable to another, the entire array is copied. And if we compare the two arrays for equality, all of their corresponding indexes are compared. When you do want to assign, pass or return arrays by reference, you can do so with array pointers or slices, which we'll get to shortly. By default, array index in an Odin is bounce checked both at compile time and runtime. When we try to assign to index 100 of this five bool array, the compiler gives us a compilation error because it knows that the compile time value 100 is out of bounds for this array. If though we index an array with a runtime expression, the balance check happens at runtime. So indexing this array with a variable whose value will be 100 when this assignment is reached will trigger a panic. These runtime bounds checks of course incur some degree of overhead. So in some performance critical contexts, you may wish to disable them. If we do indexing inside a pound no bounce check directive block, bounce check panics will not be thrown and this code will dangerously read the data that happens to lie 100 in up in memory from the start of the array even though that is well outside the array bounds. What Odin calls a slice is a value that represents a subrange of an array or an array-like buffer. Concretely, a slice contains a pointer to the start of the subrange and then an integer for the length of the subrange. For anyone coming from Go, it's important to note that unlike Go slices, Odin slices do not contain a capacity and there is no append operation for slices. For the nearest equivalent of Go slices in Odin, you instead want what Odin calls a dynamic array, which we'll also cover shortly. Anyway, here we're declaring the variable s to be a slice of ins as indicated by the empty square brackets. If we have an array of ins, we can use the slice operator to produce a slice of ins value. The slice operator looks like the index operator, but with a colon surrounded by two integers, which represent the start and end of the returned slices subrange. In this example, we're getting a slice representing the subrange of the array that starts at index 30 and ends at index 40, not inclusive. So the slice value produced will store a pointer to index 30 and a length of 10. If we then assign a value to index zero of the slice, this is logically the same thing as assigning to index 30 of the array. As a convenience, the first integer of a slice operation can be emitted in which case it defaults to zero. So here the slice now runs from index zero of the array up to but not including index 40 and has a length of 40. The second integer expression in the slice operator can also be emitted in which case it defaults to the length of the array. So here the slice now runs from index 20 of the array up to but not including index 100 and has a length of 80. Commonly we want to get a slice with a certain length. So often we'll compute the end index as the starting offset plus the desired length. Here for example, we want a slice that starts at index 5 and has a length of 9. So the n index is computed as offset plus length 5 + 9. A nice idiom that lets us express this a little more elegantly is to actually do two slice operations. First we get a slice running from the desired offset to the end of the array. Then we get a slice of the slice starting from its first index up to our desired length. The end result is the same. But this way we can write the offset expression just once instead of twice. Before completing our discussion of slices and before introducing dynamic arrays, we need to talk a bit about allocators in Odin. This is a larger topic than we can cover here, but we'll hit the key ideas. Because Odin is not a garbage collected language, you the programmer are responsible for allocating and deallocating any heat memory that you want to use. For example, if we want to create a slice whose reference data resides on the heap, we can call the make slice procedure from the base library, which returns a slice that references newly allocated heap memory. And then when we're done with the slice, we should call delete slice from the base library to deallocate the slices heap memory. Whereas before we were creating slices that reference the memory of stack allocated arrays, here the slice references heap allocated memory with no array involved. and be clear that the slice variable itself is still stack allocated. The hidden detail here is that make slice delete slice and all other Odin procedures that allocate and deallocate. These procedures by convention let you specify an allocator. Different allocators track their allocations in different ways and some allocators may perform better than others in different use cases. In our example, we aren't explicitly passing an allocator in these calls. So both calls use Odin's default allocator. The default allocator is normally accessible as context.allocator. So we can get the same result by passing context.allocator explicitly. The other allocator that's most commonly used is normally accessible as context.temp allocator. This temp allocator does not track each allocation individually. Instead of just tracks how much of its allotted space has been used such that its allocations can only be freed as a group instead of individually. Here, if we use the temp allocator, attempting to then deallocate the slice individually will trigger a segmentation fault. Instead, what we should do is eventually call free all on the temp allocator to deallocate all of its allocations as a group. This effectively resets the temp allocator, making all of its memory available again for subsequent allocations. In practice, temp allocations are useful in situations where you know that a group of allocations can all be safely freed at the same time. For example, video games typically allocate many objects in a frame that can be neatly deallocated all at once at the end of the frame. For these allocations, a game can use the temp allocator. But for things that need to live longer than an individual frame, a game may need to use an allocator that individually tracks allocations, such as Odin's default allocator. Now that we have some understanding of allocators, we can talk about dynamic arrays. Whereas a normal Odin array is fixed in size, a dynamic array has no fixed size and so can grow and shrink. Here we're declaring a variable R which is a dynamic array of ins as denoted by the reserved word dynamic inside the square brackets. Concretely, a dynamic array value resembles a slice in that it also consists of a pointer and an integer representing the length. But additionally, a dynamic array has an integer representing its capacity plus a reference to an allocator. This capacity and allocator reference allows a dynamic array to behave more like go's version of slices in that were able to append values to a dynamic array. Whereas Odin slices often reference subranges of stack allocated arrays. This is not an intended use case for dynamic arrays. Instead, the data referenced by a dynamic array is normally heap allocated via base library procedures such as here where this call allocates a block of seven ins and returns a dynamic array pointing to the block with a length of four, capacity of seven and a reference to the default allocator. When we later no longer need this dynamic array, we should call delete dynamic array, which uses the dynamic arrays referenced allocator to know which allocator to deallocate the block from. As a side note, the fact that slice values do not include an allocator reference can create a bit of a hassle because you must then separately track the allocators used for each of your allocated slices. In contrast, dynamic arrays conveniently reference their allocator. And this in fact is generally the recommended pattern for any data types that require allocations. They should store a reference to their allocator. Now, as promised earlier, we can append to a dynamic array as here where we use the base library procedure append elements to append three values. The procedure requires a pointer to the dynamic array so that it may update its length and also potentially update its pointer and capacity. In this case, the block referenced by the dynamic array already has sufficient capacity for three more values. So, the pointer and capacity remain the same. But in cases where the append operation exceeds the existing capacity, then a new larger block is allocated. The data is copied. The new values are appended to this new block. The original block is freed and the pointer and capacity are updated to match the new allocation. Here this second append exceeds the existing capacity. So a new allocation is made with a capacity that is at least large enough to accommodate the appended values. One more type of collection built into Odin are maps which are hashmaps of key value pairs. Concretely, a map value consists of a pointer to a block of memory where the key value pairs reside, an integer indicating the number of key value pairs and a reference to an allocator here. Now, for example, this variable m is declared to be a map of string keys with int values. Before using the map, we must allocate it. And like all allocated things, we should eventually deallocate it when we no longer need it. Once the map is allocated, we can add, set, and read key value pairs with the index operator. Here, we add a key high with the value five to our empty map, increasing its length to one. When we assign to an existing key, we replace its existing value, and the length of the map remains unchanged. To remove a key, we call the base library procedure delete key. Pass in a pointer to the map and the key we want to remove. Like in C and other Cike languages, a strct in Odin is a composite data type that consists of named members called fields. Here we're defining a type named cat, which is a strct consisting of two fields, an int named A and an F32 named B. If we declare a cat variable, we can assign to its individual fields with the dot operator. We can also create cat values with a literal syntax where each field can be provided a value by name. Any emitted fields default to their zero values and even the strruct name on the literal can be left implicit if it can be inferred from the assignment target. Here we're creating a variable whose type is an unnamed anonymous strct. We can explicitly cast to and from any named strruct type that has the exact same field names and types. Here the cat strruct has the same fields with the same types as the anonymous strruct. So we can cast between these two types though the casts must be explicit. Anonymous strcts are particularly convenient for strrus nested as fields and other strrus. Here this dog strct has a field named fu whose type is an anonymous strruct and we can read and write the fields of this anonymous nestruct individually or as a complete strruct. While using a namestruct instead wouldn't change the semantics. The anonymous strruct effectively allows us to logically group fields together without the hassle of defining a separate named strruct type. An enum in Odin is an integer type with discreetly named compile time values. Here we have a type direction which is defined as a U32 enum with four named values. North having the value zero, east having the value one, south having the value two and west having the value three. We then create a direction variable and assign it the direction value south which has the U32 value 2. When defining an enum, we can leave the integer type unspecified, in which case it defaults to int. If we omit the value for the first name, it defaults to zero and then any subsequent emitted value will default to one greater than the prior. In this example, then north defaults to zero, but south then defaults to 1338 because it is one greater than the prior value east. If we leave all the values of an enum unspecified, they effectively run from zero up to one less than the number of values. In a context where an enum type is expected, such as in an assignment to an enum variable, we can emit the name of the enum type before the dot as shorthand. Here, the compiler understands that south is shorthand for direction. South because the assignment target is a direction variable. Normally we only want to use the named values of an enum but we can actually cast any integer value into an enum type such as here where we make a direction value from 9 even though direction has no named value for 9. We can even do arithmetic with enum values though there aren't many cases where this is useful in a for loop. We can loop over every named value of an enum type in the order that they're listed in the enum definition. Here as specified by the in clause, this loop will iterate over each named value of the direction enum and each iteration will assign the direction value to the first variable d and assign the index of the iteration to the second variable index. We can also switch over enum values such as here where this switch will execute the case corresponding to the value of this direction variable. Note that we can use shorthand for the enum values in each case. By default, Odin strictly demands that an enum switch have a separate case for every named value. So here, when we emit cases for a north and west, we'll get a compilation error. However, if we add the partial directive to our switch, Odin will allow us to emit cases, and we also then can have a default case. To get an enum value name as a string, we can call a procedure from the reflect package. The procedure enum name from value returns the name of an enum value as a string. The procedure also returns a boolean that will be false if the enum value has no name. For instance, if the direction value here is 9, the returned boolean will be false because 9 has no name in the direction enum. A union is a data type defined as a set of variant types such that the union can store values of any of its variants. Here, for example, we have a union named pet, which is defined to have three variant types, cat, dog, and bird, which we haven't here written definitions for, but just assume they're strct types. If we then create a pet variable, the variable has sufficient space to store a value of the largest variant type. And we can assign values of any variant type to this variable without an explicit cast. So, first here we assign a cat value to variable pet, which stores the cat value. Next though we assign a dog value to pet which overwrites the cat value with the dog value. Again any variant type can be implicitly cast to the union type. However, we cannot cast even explicitly in the other direction. Instead to get the stored value out of a union, we need to use what's called a type assertion. This type assertion here tests if the union variable pet currently holds a cat. If so, it returns the cat value and the boolean true. If the variable doesn't currently hold a cat value, a type assertion returns the cat zero value and the boolean false. How is it known what variant type is held in a union? Well, alongside the variant value, a union also stores a tag integer which denotes the variant type. So, at runtime, a type assertion checks tag to determine if the union value currently holds the expected type. In cases where we're confident what variant a union value will hold at runtime, we can use the shorter form of type assertion that returns just the variant type. This form of type assertion still checks the tag at runtime, but instead of returning a boolean, this type assertion will trigger a panic if the held variant is not a cat. In addition to type assertions, we can get the variant values held in a union with a form of switch called a type switch. Here we're switching on the pet. If variable pet stores a cat, then the cat case is executed and variable P will have type cat. If variable pet stores a dog, then the dog case is executed and P will have type dog. And if variable pet stores a bird, then the bird case is executed and P will have type bird. By default, our type switch must cover all the variant types individually. But if we add the partial directive on our type switch, we can then emit cases and optionally add a default case. Here the default case is executed when pet is either a dog or bird and P in the default case will have type pet. By default a union type includes the special value nil as one of its variants and this nil variant is represented by the tag zero. Therefore the zero value of these union types is equal to nil here. Then when we assign nil to the pet union variable, it is implicitly cast to the pet union type. The important thing to understand is that the compiler considers the nil value of one union type to be distinct from nil pointers and distinct from the nil values of other union types. Here we can't assign a nil pointer to our pet variable. Nor can we assign a nil fruit union value. Neither of these nils are pet union values and so these assignments are invalid. Unlike many other languages, Odin has no exception mechanism. It does have runtime panics which are triggered by some operations such as failing bounce checks and these panics will unwind the call stack. But there's no way in the language to catch and recover from these panics except to do some logging and cleanup before the program terminates. Consequently, panics are not a mechanism for normal error handling. Instead, normal errors in Odin are represented as ordinary data values and these error values should follow three strong conventions. First, error values should always be represented either as boolean, enum, or union types. Second, error values should always be returned from procedures as the last return value. So, if a procedure returns other things in addition to an error, the error should always be the last of the return types. Third, success, meaning the absence of an error, should be indicated by true for booleans, the zero value for enums, and nil for unions. So for example, a procedure that returns a boolean error value should return true to indicate success and false to indicate that an error has occurred. For some examples of error values, we'll look at a few procedures from the base libraries. For an example of a boolean error, here's the parse F64 procedure from the stir conff package. This procedure attempts to parse the string argument as an F64 number and when successful, it returns true. Otherwise, if the string can't be parsed as an F64, the procedure returns false. For an example of an enum error type, a number of library procedures that perform allocations uses allocator error enum to signal allocation errors. Because allocations may fail in multiple ways, it's useful to convey that information with an enum instead of just using a boolean to signal whether an error has occurred. So here for example when we call the alloc procedure from the mem package it returns two values a pointer for the allocated data plus an allocator error. If the allocation succeeds the procedure will return the zero value of allocator error which has the named value none. Otherwise depending on the nature of the failure the procedure will return one of the other allocator error named values. Note that we don't use a partial switch here. So the compiler forces us to cover every named value of the enum. It's unwise to ignore errors. So it's generally best to avoid partial switches when processing an error enum. While enum errors provide more information than a simple boolean, you sometimes want an error value with other kinds of data such as string messages. And this is where union errors become useful. The variant types of a union can be anything such as strings, strrus, other unions, or whatever. So a union error can hold any information that we need it to. Here's an example union error type from the OS package. One called error. The procedure open from the OS package attempts to open a file and returns this union error type. What we should do in response to an error of course depends on the specific error and the specific context which is beyond our scope here. But in the general case, we can account for all the possible kinds of error by using a type switch. Again, you should generally avoid using partial type switches to handle errors as it can make errors too easy to inadvertently ignore. Very commonly with error values, we want to immediately return the error if it is non zero. Here we're getting the error returned from the procedure then immediately returning it if it is non zero. This pattern is so common that Odin provides the or return operator as shorthand for the same logic. The or return operator follows its operand which is always a procedure called returning at least one value and the or return effectively consumes the last returned value of its operand returning it when the value indicates an error. So in this case if the parse f64 call returns true the or return simply consumes the boolean and the whole expression evaluates into just the f64 returned by the call. If however the call returns false indicating an error then or return will immediately return false from the containing function. Note that because parse f64 returns a boolean error it can only be used with or return in a procedure that also returns a boolean. Likewise if we use on return with a procedure call that returns an enum or union type error the containing procedure must also return that same error type. to use or return inside a procedure that returns multiple types. Then the error type must be last and the return types must be named return variables. Here for example, the return types of procedure fu are given variable names x, y and error which allows us then to use or return in the procedure on any call that returns a bool as its last return type. So here if the call the bar returns false the or return will trigger a return from the function with the current values of x and y plus the value false. Odin also has an or break operator which is much like or return except it simply performs a break rather than a return. There's also an or continue operator which of course continues instead of breaks. Lastly, there is or else which unlike the other operators takes a second operand on its right. This right operand is only evaluated if the left operand returns a non-success error value in which case the or else expression evaluates into the right operand value instead of the left operand. In effect, an or else lets us conveniently specify a default value expression for the event of an error.