Closed ianterrell closed 1 year ago
You're not the only one who thinks it's neat!
That Stanford .png file was great. I want it framed on my wall where I work :D
I don't think that's exactly accurate, and I don't think you should take that as the lesson.
I ran a quick search on 'primitive type' as it pertains to Java. Ruby doesn't have a most basic data type like Java does. In Java, something like a char
is just a data type under the hood, whereas in Ruby it would be nested under a superclass (and so on, until it reaches Object). Is that a better way to think of it?
This sounds a little confusing to me. An instance is a specific piece of data
I see! After looking at your terminal example of foo
having the same object_id as f
, I experimented with the sam
instance. I assigned sam
to a variable named a
. I confirmed a
had the same object_id as sam
.
So, I know sam
is an object, an instance, is not a variable, and that if I assign it TO a variable, the variable will have the same object_id. The variable references the same place in memory that sam
occupies.
I'm also wondering, does that mean Jukebox.new
doesn't have a place in memory UNTIL an instance is created? Do sam
and Jukebox.new
(as it is written on that line) share the same spot in memory?
And then there's an idea that, well, okay, there's how we interpret it, but also how we communicate it: how we write it and how we read it; maybe generaly how we visualize it. Our editors and terminals would be a mess if we actually literally typed and displayed newlines, so we visualize it as
\n
.
It sounds like there are layers upon layers of coded meaning built into every language or language-based tool.
That said, this is a wonderful website to play with them: https://rubular.com
This is super cool to play around with. I bookmarked it. I have a feeling it'll be really useful to learn with.
You can think of it in terms of precedence.
So, binding essentially refers to an order of operations?
I think you're right up until the end. It's not like method chaining, at least as I interpret that.
Heard! What would the difference be between passing control and method chaining? Is passing control more about switching the control between methods, whereas method chaining completes one method and starts a new one entirely?
I ran a quick search on 'primitive type' as it pertains to Java. Ruby doesn't have a most basic data type like Java does. In Java, something like a char is just a data type under the hood, whereas in Ruby it would be nested under a superclass (and so on, until it reaches Object). Is that a better way to think of it?
I think you've got the gist of it. Language is tricky though. I would count classes as data types also; it's the primitive part that differentiates them.
You originally wrote:
whereas with Ruby, any number already has these properties baked into it
I think that does imply a good way to think about it, along with what the book said. A class is "data and behavior." Primitive types in other languages are just data and no behavior; that's why the behavior had to be defined outside of it rather than accessible via dot syntax on an instance.
Primitive type also connotes "single value of a really basic thing": integers, floats, booleans, characters. If you combine more than one of them, or start labeling them, or add behavior — it makes it user defined, non-primitive, and potentially other stuff.
But generally you've got the gist, and I'm probably making it worse by talking about it. :)
So, I know sam is an object, an instance, is not a variable, and that if I assign it TO a variable, the variable will have the same object_id. The variable references the same place in memory that sam occupies.
So, you mean this I think:
sam = Jukebox.new
I find it a helpful exercise to try to make sure you can identify and understand every character and word in a line of code in English. The most descriptive sentence I could write about that line is:
"Instantiate a new instance (.new
) of the Jukebox class (Jukebox
) and assign (=
) it to a local variable with the identifier sam
."
(Trying not to contradict myself and not to make things more confusing, but I'm only human.)
So "sam
" is a variable, and it points to the instance of Jukebox
. That's important to understand. But it's also cumbersome language, so we say "sam
is an instance of Jukebox
" If you assign sam
to a
:
sam = Jukebox.new
a = sam
Then we say sam
is an instance of Jukebox and a
is the same instance of Jukebox
, or that sam
and a
are two variables that reference the same object.
I'm also wondering, does that mean Jukebox.new doesn't have a place in memory UNTIL an instance is created?
This set of questions is a really good line of thinking!
Calling .new
on Jukebox
is what creates the place in memory! That method right there (.new
) does this:
Jukebox
instanceBreaking down the English description, that's the "Instantiate a new instance (.new
) of the Jukebox class (Jukebox
)" part.
Instantiation = creating/reserving and initializing the place in memory for the instance.
That's why constructors have special names in languages, like it has to be initialize
in Ruby: they do more than just execute the code you put inside them, they do all that memory stuff too! You can think of it as being something like the Ruby interpreter defining the new
method for you like this:
def class.new(params)
# do the memory stuff
initialize(params)
end
Do sam and Jukebox.new (as it is written on that line) share the same spot in memory?
There's the logical interpretation, the literal interpretation, and then compiler magic.
The logical interpretation is "sam
, for all intents and purposes, is the instance returned by Jukebox.new
. There's only one spot in memory where an instance is, and it's that one. We know sam
just technically points to that, but once we know that we can sort of forget it and think of sam
as the instance."
The literal interpretation is that both the instance and the reference to the instance have to live somewhere, and they do live in different places.
sam = Jukebox.new
a = sam
There are three memory locations:
sam
\
Jukebox instance
/
a
Uh, imagine those lines are arrows. The variables are little spots in memory that just hold the memory address; the Jukebox instance is a potentially big spot in memory that holds all the Jukebox data.
Here's a similar diagram from an article on the same topic:
Another interesting question is what happens with this line of code, no variable:
Jukebox.new
In that case, the memory is initialized and created and set aside and then... it can't be used. There's no reference to it. Eventually it gets reclaimed by the "garbage collector" since it's not useful to the program.
Or this case:
sam = Jukebox.new
sam = Jukebox.new
In this case, two instances are created, but the variable changes where it points from the first one to the second one. The first one is now able to be garbage collected since nothing points to it and the program can't make use of it.
It's enough for a while to have a cursory knowledge of this. Eventually you'll want to really understand it, plus the terms: allocation, heap, stack, garbage collection, pointer.
Again, probably too much, but... they were really good questions.
So, binding essentially refers to an order of operations?
Yes, in this context they mean the same thing.
What would the difference be between passing control and method chaining? Is passing control more about switching the control between methods, whereas method chaining completes one method and starts a new one entirely?
I think you've got it exactly.
# context 1 - method
def foo
yield
end
# context 2 - "calling code"
foo { puts "This puts statement is defined in context 2, but called from context 1 via yield!" }
vs
# context 1 - "calling code"
"All of the following methods are called in the same context!".length.to_s.length
"Instantiate a new instance (.new) of the Jukebox class (Jukebox) and assign (=) it to a local variable with the identifier sam."
That, in addition to the diagrams, puts it in perspective for me. I can see that sam
is a variable, it's just pointing to an instance of Jukebox, and it's more intuitive to treat it as though it's the instance itself when communicating about it (as long as you understand it's a reference).
Instantiation = creating/reserving and initializing the place in memory for the instance.
Calling new
to instantiate an instance gives it some real estate in memory. If you call the delete
method on the instance, does it free up the memory? Or does nil
take its place? Is it also garbage collected
?
Closing this one out but that's the last lingering question I have from this lesson!
Calling new to instantiate an instance gives it some real estate in memory. If you call the delete method on the instance, does it free up the memory? Or does nil take its place? Is it also garbage collected?
class Foo
end
f = Foo.new
# => #<Foo:0x000000010fbe5400>
f.delete
# (irb):4:in `<main>': undefined method `delete' for #<Foo:0x000000010fbe5400> (NoMethodError)
There is no general delete!
There are different strategies for memory management in different programming languages:
Manual memory management requires you to call both allocation and deallocation commands; in c it would be malloc
and free
. C++ has both constructors and destructors to allow for cleanup of memory.
In garbage collected languages, you just build stuff and trust the system to take care of it when it needs to.
Now, some data structures implement delete:
x = { a: :b }
# => {:a=>:b}
x.delete :a
#=> :b
x
# => {}
But that doesn't really truly free up the memory.
I have Google Docs open for something else, so that makes me think of an illustration of memory. You can think of memory as being several cells in a spreadsheet column. The cell number is the memory address.
At first, it is empty:
Now maybe we have a class like this:
class Person
def initialize(name, age)
@name = name
@age = age
end
end
And we instantiate someone:
alice = Person.new "Alice", 30
Now our memory might look like this:
Person.new "Alice", 30
reserved some memory at location 8, simplified as three slots:
Then alice =
takes that location, and assigns it to the alice
variable.
In this case, the variable alice
, which points to or is a reference to the object, is at the memory address 1, and at that address is the address of where the object's memory lives. That's the mechanism behind the pointing. The computer doesn't know it's called "alice" really, it just thinks about it as "the memory at location 1".
Now we instantiate another person:
bob = Person.new "Bob", 40
and our memory might look like this:
Now we assign one to the other...
bob = alice
and it might look like this:
That's the idea of two variables pointing to the same object. They have their own memory to store the pointer that points to the same object in memory.
Now the memory reserved that had been assigned to bob
is unreachable, so right now that memory is "garbage". After the garbage collector runs it looks like this:
That's a slight simplification but it's really really close to how things actually work.
Thanks for indulging me and let me know if that makes sense! I've long considered going into programming education in some form or another, so this is useful practice for me.
That was a really helpful illustration! I feel like I have a better understanding of what the garbage collector does. Also, the portion about bob
being unreachable since it instead references 'memory slot 8' for Person alice was a great way to pull it together for me. I think. If I've said something that illustrates I don't understand it, please call me out :D
You're not the only one who thinks it's neat!
https://ccrma.stanford.edu/courses/250a-fall-2005/docs/ComputerLanguagesChart.png
This is confusing without knowing other languages to compare it to, and I would prefer the term "primitive type" to basic type.
I don't think that's exactly accurate, and I don't think you should take that as the lesson.
It's amazing how small sentences can hide such depth of knowledge!
Programming languages can be classified into rough programming paradigms: the approaches they take toward how to program. "Object oriented programming" is but one of several programming paradigms.
Java is an object oriented language, but unfortunately not everything is an object! Some types (primitive types) are handled differently and are not objects.
Ruby is a "pure object oriented language", in which "everything is an object".
That line about Ruby is the one to internalize from this, and you can just file away the rest as "other languages are different and when I learn them I will see how". :)
Also, you can explore how everything is an object. Ruby means it quite literally!
This sounds a little confusing to me.
An instance is a specific piece of data (with associated behavior, since everything is an object). A variable is a label that helps you reference that piece of data.
Here I create a single instance of
Foo
and assign it to the variablesf
,foo
, andstill_the_same
:The variables label the instance so that we can work with it. Another very good metaphor (for a lot of reasons) is that the variables "point to" the instance.
The "pointing to" is sort of literally true. The data has to live somewhere, right? You know data on a computer is made of 1s and 0s, and so those 1s and 0s have to be present in the circuitry somewhere; in our case, the circuitry we care about is the RAM or memory. The variable points to physically where it is in memory.
If memory was a town, you could think of the
.object_id
as the street address of the instance.(In fact, the big number in
#<Foo:0x0000000109735708>
is literally its memory address.)Spaces vs tabs is pretty much the canonical holy war among programmers.
That said, every programming language has a community built up around it, and communities generally settle on style (or close). Almost all Ruby projects are written with two spaces. I've generally found it helpful to just follow along with the dominant style in a community.
And that said, everyone still presses the tab key. Your editor can be configured to output spaces.
The rule I use here is sort of... is it confusing? I will omit parentheses in simple cases but include them when I'm using multiple methods on a line, etc.
These are generally called escape sequences. And, just to point you to doc again to remind you it's all there: https://docs.ruby-lang.org/en/3.2/syntax/literals_rdoc.html#label-String+Literals
As far as getting totally sure what that means... hmm. It might not be worth it yet. :) But maybe.
Generally: you're familiar with the idea that everything stored on a computer is in 1s and 0s. Well, that's great, but we don't read English in 1s and 0s. So there's the data that is stored and then there's how we interpret that data.
So, strings aren't ultimately stored as characters, they're stored as 1s and 0s, which are more easily interpreted as numbers, so strings are numbers...
And then there's an idea that, well, okay, there's how we interpret it, but also how we communicate it: how we write it and how we read it; maybe generaly how we visualize it. Our editors and terminals would be a mess if we actually literally typed and displayed newlines, so we visualize it as
\n
.Ruby style is pretty set on this.
snake_case
for symbols, methods, variablesCamelCase
for classes and modulesSCREAMING_SNAKE_CASE
for constantsEverything is an object!
Ruby's
nil
is both similar to but also very different from other languages'null
.This is a Ruby-only thing, as far as I'm aware. They're mostly stylistic, and should only be used when it's simple and clear and makes the text easier to read. That's why
while
isn't used this way: it's not simple and it's not easier to read.I would... skim this stuff, and not try to deeply learn it yet. They're very very useful and important to know to be a senior developer. They're probably less important to know to be a junior developer.
By all means, if you find it interesting please learn as much as you want! But they can get confusing very fast.
That said, this is a wonderful website to play with them: https://rubular.com
The convention is the important part here for block syntax. But "bind more tightly" will eventually be important in other contexts.
You can think of it in terms of precedence. What is the result of
1 + 2 * 3
? Well, it depends on if you interpret it as(1 + 2) * 3
or as1 + (2 * 3)
. In standard mathematics the convention is the latter way; we might say that*
"binds more tightly" than+
.I think you're right up until the end. It's not like method chaining, at least as I interpret that.
But yes, "passing control" means what you think: at first the outer method is in control and being executed, then it "yields control" to the block that was passed in, which is then executed, before coming back to the method itself. You'll also see it described as "inversion of control". When you call a method you're handing control to it, but then it's inverted back to the calling code.
I think you mean by method chaining, something like
x.foo().bar()
. In this case, control is never inverted or yielded back; it is sequentially calling (passing control to)foo
and thenbar
.Blocks are powerful and useful! But that said, at the beginning you'll use them more when calling standard library methods than you really write usages of them yourself. But eventually you'll stumble on some problem that wants them as the solution.