Skip to content

Commit a17bba3

Browse files
authored
Merge pull request #22 from pedropark99/self
Add section to explain the difference between `self: User` and `self: *User`
2 parents 282a024 + 5056ab6 commit a17bba3

File tree

9 files changed

+445
-76
lines changed

9 files changed

+445
-76
lines changed

Chapters/02-debugging.qmd

+1-2
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,7 @@ you provide this format specifier inside a pair of curly braces. So, if you want
6969
your object using the string specifier (`s`), then, you can insert the text `{s}` in your template string.
7070
Here is a quick list of the most used format specifiers:
7171

72-
- `d`: for printing integers.
73-
- `f`: for printing floating-point numbers.
72+
- `d`: for printing integers and floating-point numbers.
7473
- `c`: for printing characters.
7574
- `s`: for printing strings.
7675
- `p`: for printing memory addresses.

Chapters/03-structs.qmd

+226-2
Original file line numberDiff line numberDiff line change
@@ -522,8 +522,7 @@ not be authorized to call this method directly in my code.
522522

523523

524524

525-
526-
## Anonymous struct literals {#sec-anonymous-struct-literals}
525+
### Anonymous struct literals {#sec-anonymous-struct-literals}
527526

528527
You can declare a struct object as a literal value. When we do that, we normally specify the
529528
data type of this struct literal by writing it's data type just before the opening curly braces.
@@ -578,6 +577,231 @@ pub fn main() !void {
578577
```
579578

580579

580+
581+
### Struct declarations must be constant
582+
583+
Types in Zig must be `const` or `comptime` (we are going to talk more about comptime at @sec-comptime).
584+
What this means is that you cannot create a new data type, and mark it as variable with the `var` keyword.
585+
So struct declarations are always constant. You cannot declare a new struct using the `var` keyword.
586+
It must be `const`.
587+
588+
In the `Vec3` example below, this declaration is allowed because I'm using the `const` keyword
589+
to declare this new data type.
590+
591+
```{zig}
592+
#| build_type: "lib"
593+
#| auto_main: false
594+
const Vec3 = struct {
595+
x: f64,
596+
y: f64,
597+
z: f64,
598+
};
599+
```
600+
601+
602+
### The `self` method argument
603+
604+
In every language that have OOP, when we declare a method of some class or struct, we
605+
usually declare this method as a function that have a `self` argument.
606+
This `self` argument is the reference to the object itself from which the method
607+
is being called from.
608+
609+
Is not mandatory to use this `self` argument. But why would you not use this `self` argument?
610+
There is no reason to not use it. Because the only way to get access to the data stored in the
611+
data members of your struct is to access them through this `self` argument.
612+
If you don't need to use the data in the data members of your struct inside your method, then, you very likely don't need
613+
a method, you can just simply declare this logic as a simple function, outside of your
614+
struct declaration.
615+
616+
617+
Take the `Vec3` struct below. Inside this `Vec3` struct we declared a method named `distance()`.
618+
This method calculates the distance between two `Vec3` objects, by following the distance
619+
formula in euclidean space. Notice that this `distance()` method takes two `Vec3` objects
620+
as input, `self` and `other`.
621+
622+
623+
```{zig}
624+
#| build_type: "lib"
625+
#| auto_main: false
626+
const std = @import("std");
627+
const m = std.math;
628+
const Vec3 = struct {
629+
x: f64,
630+
y: f64,
631+
z: f64,
632+
633+
pub fn distance(self: Vec3, other: Vec3) f64 {
634+
const xd = m.pow(f64, self.x - other.x, 2.0);
635+
const yd = m.pow(f64, self.y - other.y, 2.0);
636+
const zd = m.pow(f64, self.z - other.z, 2.0);
637+
return m.sqrt(xd + yd + zd);
638+
}
639+
};
640+
```
641+
642+
643+
The `self` argument corresponds to the `Vec3` object from which this `distance()` method
644+
is being called from. While the `other` is a separate `Vec3` object that is given as input
645+
to this method. In the example below, the `self` argument corresponds to the object
646+
`v1`, because the `distance()` method is being called from the `v1` object,
647+
while the `other` argument corresponds to the object `v2`.
648+
649+
650+
```{zig}
651+
#| eval: false
652+
const v1 = Vec3 {
653+
.x = 4.2, .y = 2.4, .z = 0.9
654+
};
655+
const v2 = Vec3 {
656+
.x = 5.1, .y = 5.6, .z = 1.6
657+
};
658+
659+
std.debug.print(
660+
"Distance: {d}\n",
661+
.{v1.distance(v2)}
662+
);
663+
```
664+
665+
```
666+
Distance: 3.3970575502926055
667+
```
668+
669+
670+
671+
### About the struct state
672+
673+
Sometimes you don't need to care about the state of your struct object. Sometimes, you just need
674+
to instantiate and use the objects, without altering their state. You can notice that when you have methods
675+
inside this struct object that might use the values that are present the data members, but they
676+
do not alter the values in the data members of structs in anyway.
677+
678+
The `Vec3` struct that we presented in the previous section is an example of that.
679+
This struct have a single method named `distance()`, and this method do use the values
680+
present in all three data members of the struct (`x`, `y` and `z`). But at the same time,
681+
this method do not change the values of these data members in any point.
682+
683+
As a result of that, when we create `Vec3` objects we usually create them as
684+
constant objects, like the `v1` and `v2` objects presented in the previous
685+
code example. We can create them as variable objects with the `var` keyword,
686+
if we want to. But because the methods of this `Vec3` struct do not change
687+
the state of the objects in any point, is unnecessary to mark them
688+
as variable objects.
689+
690+
But why? Why am I talkin about this here? Is because the `self` argument
691+
in the methods is affected depending on whether the
692+
methods present in a struct change or not the state of the object itself.
693+
More specifically, when you have a method in a struct that changes the state
694+
of the object (i.e. change the value of a data member), the `self` argument
695+
in this method must be annotated in a different manner.
696+
697+
As I described in the previous section, the `self` argument in methods of
698+
a struct is the argument that receives as input the object from which the method
699+
was called from. We usually annotate this argument in the methods by writing `self`,
700+
followed by the colon character (`:`), and the data type of the struct to which
701+
the method belongs to (e.g. `User`, `Vec3`, etc.).
702+
703+
If we take the `Vec3` struct that we defined in the previous section as an example,
704+
we can see in the `distance()` method that this `self` argument is annotated as
705+
`self: Vec3`. Because the state of the `Vec3` object is never altered by this
706+
method.
707+
708+
But what if we do have a method that alters the state of the object, by altering the
709+
values of it's data members. How should we annotate `self` in this instance? The answer is:
710+
"we should pass a pointer of `x` to `self`, and not simply a copy of `x` to `self`".
711+
In other words, you should annotate `self` as `self: *x`, instead of annotating it
712+
as `self: x`.
713+
714+
If we create a new method inside the `Vec3` object that, for example, expands the
715+
vector by multiplying it's coordinates by a factor o two, then, we need to follow
716+
this rule specified in the previous paragraph. The code example below demonstrates
717+
this idea:
718+
719+
```{zig}
720+
#| build_type: "lib"
721+
#| auto_main: false
722+
const std = @import("std");
723+
const m = std.math;
724+
const Vec3 = struct {
725+
x: f64,
726+
y: f64,
727+
z: f64,
728+
729+
pub fn distance(self: Vec3, other: Vec3) f64 {
730+
const xd = m.pow(f64, self.x - other.x, 2.0);
731+
const yd = m.pow(f64, self.y - other.y, 2.0);
732+
const zd = m.pow(f64, self.z - other.z, 2.0);
733+
return m.sqrt(xd + yd + zd);
734+
}
735+
736+
pub fn double(self: *Vec3) void {
737+
self.x = self.x * 2.0;
738+
self.y = self.y * 2.0;
739+
self.z = self.z * 2.0;
740+
}
741+
};
742+
```
743+
744+
Notice in the code example above that we have added a new method
745+
to our `Vec3` struct named `double()`. This method essentially doubles the
746+
coordinate values of our vector object. Also notice that, in the
747+
case of the `double()` method, we annotated the `self` argument as `*Vec3`,
748+
indicating that this argument receives a pointer (or a reference, if you prefer to call it this way)
749+
to a `Vec3` object, instead of receiving a copy of the object directly, as input.
750+
751+
```{zig}
752+
#| eval: false
753+
var v3 = Vec3 {
754+
.x = 4.2, .y = 2.4, .z = 0.9
755+
};
756+
v3.double();
757+
std.debug.print("Doubled: {d}\n", .{v3.x});
758+
```
759+
760+
```
761+
Doubled: 8.4
762+
```
763+
764+
765+
766+
If you change the `self` argument in this `double()` method to `self: Vec3`, like in the
767+
`distance()` method, you will get the error exposed below as result. Notice that this
768+
error message is indicating a line from the `double()` method body,
769+
indicating that you cannot alter the value of the `x` data member.
770+
771+
```zig
772+
// If we change the function signature of double to:
773+
pub fn double(self: Vec3) void {
774+
```
775+
776+
This error message indicates that the `x` data member belongs to a constant object,
777+
and, because of that, it cannot be changed. Even though we marked the `v3` object
778+
that we have created in the previous code example as a variable object.
779+
So even though this `x` data member belongs to a variable object in our code, this error
780+
message is pointing to the opposite direction.
781+
782+
```
783+
t.zig:16:13: error: cannot assign to constant
784+
self.x = self.x * 2.0;
785+
~~~~^~
786+
```
787+
788+
But this error message is misleading, because the only thing that we have changed
789+
is the `self` argument signature from `*Vec3` to `Vec3` in the `double()` method. So, just remember this
790+
general rule below about your method declarations:
791+
792+
::: {.callout-note}
793+
If a method of your `x` struct alters the state of the object, by
794+
changing the value of any data member, then, remember to use `self: *x`,
795+
instead of `self: x` in the function signature of this method.
796+
:::
797+
798+
You could also interpret the content discussed in this section as:
799+
"if you need to alter the state of your `x` struct object in one of it's methods,
800+
you must pass the `x` struct object by reference to the `self` argument of this method,
801+
instead of passing it by value".
802+
803+
804+
581805
## Type inference {#sec-type-inference}
582806

583807
Zig is kind of a strongly typed language. I say "kind of" because there are situations
+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
const std = @import("std");
2+
const math = std.math;
3+
const Vec3 = struct {
4+
x: f64,
5+
y: f64,
6+
z: f64,
7+
8+
pub fn distance(self: Vec3, other: Vec3) f64 {
9+
const xd = math.pow(f64, self.x - other.x, 2.0);
10+
const yd = math.pow(f64, self.y - other.y, 2.0);
11+
const zd = math.pow(f64, self.z - other.z, 2.0);
12+
return math.sqrt(xd + yd + zd);
13+
}
14+
15+
pub fn double(self: *Vec3) void {
16+
self.x = self.x * 2.0;
17+
self.y = self.y * 2.0;
18+
self.z = self.z * 2.0;
19+
}
20+
};
21+
22+
pub fn main() !void {
23+
var v1 = Vec3{
24+
.x = 4.2,
25+
.y = 2.4,
26+
.z = 0.9,
27+
};
28+
const v2 = Vec3{
29+
.x = 5.1,
30+
.y = 5.6,
31+
.z = 1.6,
32+
};
33+
34+
std.debug.print("Distance: {d}\n", .{v1.distance(v2)});
35+
v1.double();
36+
std.debug.print("Doubled: {d}\n", .{v1.x});
37+
}

0 commit comments

Comments
 (0)