erikamaker / pickaxe

Study notes and exercises from Programming Ruby 3.2
0 stars 0 forks source link

Chapter 2 Notes Feedback #2

Closed ianterrell closed 1 year ago

ianterrell commented 1 year ago

I think it's neat that, just like any spoken or written language, its conventions, syntax, words, and definitions are often derivative.

You're not the only one who thinks it's neat!

https://ccrma.stanford.edu/courses/250a-fall-2005/docs/ComputerLanguagesChart.png

The text says that Ruby has no basic type. For instance, in Java, you'd find the absolute value of a number by calling a separate function that passes in the number, whereas Ruby has absolute value built into the numbers class, so you can send the message abs to any number object after defining it. In Ruby, it's being called directly on the number variable like this: num.abs, whereas with Java it would be num = Math.abs(num);.

This is confusing without knowing other languages to compare it to, and I would prefer the term "primitive type" to basic type.

I think the difference is that, with Java, you have to predefine the effect on a number you want, whereas with Ruby, any number already has these properties baked into it. In that way, Ruby is more flexible.

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!

irb(main):011:0> 1.class
=> Integer
irb(main):012:0> 1.class.superclass
=> Numeric
irb(main):013:0> 1.class.superclass.superclass
=> Object
irb(main):014:0> "string".class
=> String
irb(main):015:0> "string".class.superclass
=> Object
irb(main):016:0> :symbol.class
=> Symbol
irb(main):017:0> :symbol.class.superclass
=> Object

Can an instance also technically be a variable? At first I thought, no, it's an object, but then I remembered that EVERYTHING in Ruby is an object, even simple variable assignments to integer values.

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 variables f, foo, and still_the_same:

irb(main):024:1* class Foo
irb(main):025:0> end
=> nil
irb(main):026:0> f = Foo.new
=> #<Foo:0x0000000109735708>
irb(main):027:0> f.object_id
=> 337620
irb(main):028:0> foo = f
=> #<Foo:0x0000000109735708>
irb(main):029:0> foo.object_id
=> 337620
irb(main):030:0> still_the_same = foo
=> #<Foo:0x0000000109735708>
irb(main):031:0> still_the_same.object_id
=> 337620

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.)

Also, I'm surprised to learn on page 20 that the indentation is not significant. I use tabs and feel emotionally attached to it, but it says the convention is two spaces.

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.

I learned that parameters don't need parantheses. I've seen that before and figured it was interchangeable, and it sounds like it doesn't matter but that the convention is to use them unless there is only one argument.

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.

I was reminded today that strings are made more than one way, but that the common way is a string literal. I've been absentmindedly assigning string literals for a couple years and I forgot the "literal" part. I learned there is a difference in the quotation marks used: single-quoted does very little, but with double it looks for substitution sequences that start with a backslash character and replaces them with a binary value. I'm not TOTALLY sure what that means (though I know \n represents line breaks). I read about expression interpolation.

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...

irb(main):037:0> 'a'.ord
=> 97
irb(main):038:0> 'b'.ord
=> 98
irb(main):039:0> 'c'.ord
=> 99

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.

Someone once criticized my code for having underscores for multi_word_methods, but also CamelCase constants. I'm seeing in this text that that's pretty normal, actually, thank you very much shakes fist.

Ruby style is pretty set on this.

class CamelCaseClass
  SCREAMING_SNAKE_CASE_CONSTANT = 1

  def with_snake_case_methods
    and_variables = 1
  end
end

I also learned that 'nil' is still an object. It's just a special kind of... NOTHING object. Somehow. I love that. Default behavior of a has indexed by a non-existent key is to return nil. The NOTHING object. Hahaha. Symbols are next.

Everything is an object!

irb(main):043:0> nil.class
=> NilClass
irb(main):044:0> nil.class.superclass
=> Object

Ruby's nil is both similar to but also very different from other languages' null.

Admittedly, this is a newer concept I recently found out about. So, it's worth digesting: a statement modifier is a shortcut if the body of a controlled structure is a single expression (like the above).

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.

Regular expressions are used for pattern matching strings. I've never used these before, and am just learning about them, so bear with me!

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

I learned that the braces "bind more tightly" than the do/end pairs (whatever that means--it says it will almost never make a difference in my code). Single line blocks conventionally use braces, where as multiline blocks typically use the do/end pairs.

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 as 1 + (2 * 3). In standard mathematics the convention is the latter way; we might say that * "binds more tightly" than +.

So, I'm interpreting that sentence from earlier as: The block is a second method that can be passed (or invoked) by the first. I ran the first example through an interpreter, and played around a little bit. The code in the block is executed each time yield is called. I think that "passing control" is the part I struggled with, but now I see that it means, at the start of the method, the method itself is in control. But, when yield is called, just like the literal word can mean, the method YIELDS its control to defer instead to what is INSIDE the block (curly braces). It's like method chaining, kind of?

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 then bar.

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.

erikamaker commented 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?

ianterrell commented 1 year ago

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:

Breaking 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
erikamaker commented 1 year ago

"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!

ianterrell commented 1 year ago

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:

Screenshot 2023-02-16 at 10 53 55 AM

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:

Screenshot 2023-02-16 at 11 00 57 AM

Person.new "Alice", 30 reserved some memory at location 8, simplified as three slots:

  1. it has to know what type of class it is so it knows what code to run when you call a method on it
  2. it stores the name
  3. it stores the age

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:

Screenshot 2023-02-16 at 11 01 34 AM

Now we assign one to the other...

bob = alice

and it might look like this:

Screenshot 2023-02-16 at 11 02 09 AM

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:

Screenshot 2023-02-16 at 11 04 21 AM

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.

erikamaker commented 1 year ago

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