Several months ago, I posted on here a draft of a video intro to Odin. Several people gave me helpful feedback, but I got distracted from making a final version. Coming back to this months later, I decided to retry from a different angle, this time focusing on Odin’s data types and related features: video 1: Odin Data Types (55 min)
Anyone who can spare the time, please critique any mistakes or other issues. There are definitely a few areas where the info might not be fully correct or complete, but rather than bias the feedback, I’ll let others identify them. (There are also some sound issues which I’ll try to address for the final version.)
Belated thanks to everyone who commented on the earlier video. I promise this time that I’ll actually publish a final draft!
Good video. Good flow. I actually did pick up something new for me. The shorthand on slices: “s = arr[offset:][:length]” instead of “s = [offset:offset + length]”. I’ll be looking for an opportunity to use that
May want to update the bit about enum arrays “can only be contiguous”. They can be non-contiguous if the #sparse directive is used:
Sparce_Enum :: enum {
ONE = 1,
THREE = 3,
FIVE = 5,
}
sparce_array := #sparse [Sparce_Enum]string {
.ONE = "one",
.THREE = "three",
.FIVE = "five",
}
Last thing, and I apologize in advance. I’m not dogmatic about this, but since this is meant to be learning video, I thought this is something worth considering. I heard the word “function” used a lot. I did not go back to scrutinize whether it was accurate in each individual case. The Odin FAQ states the following:
I think the distinction is important to understand. I’ve gotten the impression from reading the forums and git that Odin is intentionally designed as a procedural language. Since “procedure is a superset of functions and subroutines”, you would always be correct when using the word “procedure”, since it includes the other possibilities.
11:30 and 12:26 typo, casting pointers require parens, so ip = (^int)(r) or ip = cast(^int)r
slices, you can emit both start and end, not just one or the other. s := arr[:] is how you convert a fix buffer to a slice
anonymous structs, can be casted to a named struct if fields are declared in the same order (missed), with the same name and type
enumerated arrays, #sparse can be used to have non-contiguous enum range as index.
unions, AFAIK do not store a typeid. Imagine that you have #no_nil then the zero value of that union should be the first type (not nil), which wouldn’t work because every variable is basically just "memzero"ed when declared (so it would not be initialised to the proper typeid), The hint is in reflect.union_variant_typeid implementation as the tag is not used directly as a typeid but as an index into the variant table.
In many cases when you say what’s inside a type it might be good to just show what it desugars to (e.g. any -> runtime.Raw_Any).
I agree that “procedure” and “function” should never have been conflated, but sadly that ship sailed at some point in the 70’s and 80’s (blame C?). “Function” has long been established as the catch-all term to encompass procedure, routine,subroutine, method, and actual mathematical function.
More recent corruptions are still worth fighting, though, e.g. the way “functional” has become abused in the last decade.
My general complaint (not talking about the minor mistakes which have already been mentioned) is how much time is being focused on any.
I would highly recommend either NOT talking any at all or only in passing.
We highly recommend virtually no one using any unless they know EXACTLY how it works. And the problem is, most people don’t actually bother to learn that and wonder why they have problems with any.
Most people should not be any whatsoever. At most, just state it is used for things like fmt.println for that to have runtime type-safe formatted printing.
I know you were showing how it was used with polymorphism in the second video, but I think it was confusing because it was treating the open-set approach with a switch statement still when in reality, open-set approaches use a vtable so that you don’t need to know the type.
mentioning of complex numbers, but no mention of quaternions
misses bit_set which is much more common that bit_field
3:27
The zero value for a string/string16 is "" not nil
The zero value for cstring/cstring16 is nil
Zero value for enums is also nil
5:20
I don’t recommend distinct types for things like units most of the time, so I am not sure if this would be more confusing than not, as people would get the wrong idea.
10:20
rawptr isn’t not “untyped” like other constants, but I understand what you meany this.
11:03
Casting syntax for pointers needs parentheses to prevent ambiguity when parsing
(^int)(r) and (^string)(r) is the correct syntax
12:13
Same problem with syntax. Use `(^int)(p)
I’d also mention that this isn’t the common way to do “pointer arithmetic” since slices or multi-pointers exist.
13:51
As I said in a previous comment, I’d recommend just briefly talking about any and not really referring to it as a pointer either, unless you say “fat pointer” or something that effect.
Most people do not need nor do they understand any, so I’d recommend just pretending it doesn’t exist when teaching it.
But if you really want to explain how it works, don’t just explain it in words, show it with code too.
i: int = 123
a: any = i
// equivalent to
a: any
a.data = &i
a.id = typeid_of(type_of(i))
// or in the case of a non-addressable value
a: any = 123
// equivalent to
a: any
tmp: int = 123
a.data = &tmp
a.id = typeid_of(type_of(tmp))
23:13
You do this throughout, but I think it’s probably going to confuse more people than now when using the underlying procedures rather than the overloaded/grouped names. e.g. please prefer make([]int, 10) and delete(s) over make_slice([]int, 10) and delete_slice(s)
24:47
delete_slice(s, context.temp_allocator) will not necessarily result in a segmentation fault and if it is the default temp_allocator, it’ll just be a no-op. Make it clear what the default behaviour is and that any custom allocators are allocator-specific behaviour
27:07
Same as previously said, please prefer make([dynamic]int, 4, 7) and append(&elems, 100, 101, 102) over the overloaded/grouped names
36:10
Mentioning reflection at all at this stage seems a little weird to me since it is a bit of a high-level construct which not everyone will need directly nor understand.
38:51
Unions do not store a typeid, they just store an integer to represent the tag of the variant. By default 0 represents the nil state and then starting from 1, the number represents the variant. The size of the integer chose is the smallest needed to represent the variants (e.g. u8 when the variant count < 255, u16 when larger, etc).
typeid isn’t used for loads of reasons, but mainly because it’s not needed, and typeid is meant purely for reflection needs.
typeid is also a u64 sized (not pointer sized) hash of the type from its canonical textual form.
40:48
I’d also make it clear WHY all unions by default have a nil state, as it makes sense with the rest of Odin’s semantics.
I do get people who do not use Odin saying they don’t understand why it needs a nil state or say it isn’t a “proper” union type. It’s mainly not thinking through the logic of default implicit initialization of variables.
41:24
You’ve forgot to mention the most common use of a parametric polymorphic union type: Maybe
Maybe is a user-level @builtin type which has the following definition: Maybe :: union($T: typeid) { T }
Because of the layout of a union when there is only one variable and it is pointer-like, no tag is stored and the nil value itself is shared as the nil value of the union, allowing for things like Maybe(^T) to be pointer-sized.
I’d mention that this is more useful for interfacing with foreign code or optional parameters than as a general construct in Odin where multiple return values are preferred.
43:20
intrinsics.overflow_add doesn’t necessarily return an error but rather just returns a boolean which indicates if an overflow happened, which may or may not be an error
45:46
This is the OPPOSITE of the or_return behaviour. or_return will return when the last value of the multiple return value list is either false if a boolean or NOT nil when other types (which support nil).
In the example you gave, you are saying it will return when things DO NOT overflow.
intrinsics.overflow_add is not a “safe add” operation, which is what you are think it is like.
50:33
clamp is already a “builtin” procedure so this might be a little confusing to people assuming it doesn’t already exist.
It’s built-in to allow for compile-time evaluation and better error checking
I’d recommend doing ^$T/Stack($E) instead because it is much more like people are going to pass a possibly distincted Stack than a distincted pointer to a Stack. That’s what the / effectively does: strip the distinctness from a type.
Stack is probably not the best example since append and pop exist for dynamic arrays already, but I understand the example.
8:43
This is less a mistake more going to be one of those gotchas, Odin doesn’t need a Result type because of multiple return values, and the idiom of multiple return values is much better with the rest of the language. Result in a way is an anti-pattern for Odin
I’d also mention Maybe instead.
I do agree that parapoly unions are rare, and the reason why they might be needed is correct in your case.
11:24
I would not use data: any here, like I was stating before.
when it comes to this kind of typing, I’d recommend either basic subtyping with using, vtables, a union, or an enum.
The latter two are for closed sets rather than open sets, but if you really want a properly open set, then use a vtable or something similar.
Open-sets only make sense as pure interfaces to me, and in practice when you have inheritance-like features, you pretty much only ever want a closed-set of variants.
14:25
I’d also show a way to do subtyping with using and union, but you missed the variant field in the Pet
Pet :: struct {
name: string,
age: f32,
weight: f32,
variant: union { ^Cat, ^Dog }
}
Cat :: struct {
using pet: Pet,
foo: int,
}
Dog :: struct {
using pet: Pet,
bar: f32,
}
d: Dog
d.variant = &d
// or
d := new(Dog)
d.variant = d
14:47
Again, please don’t mention any. People just misuse it all the time when they don’t understand it. It’s effectively never a good idea unless you know EXACTLY how it works AND what you are doing. Most people don’t know.