erikamaker / pickaxe

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

Chapter 3 Notes Feedback #4

Closed ianterrell closed 1 year ago

ianterrell commented 1 year ago

QUICK UPDATES

I'm also going to begin using all my notes in README format, with titles and appropriately spaced example breaks.

That's great practice! I might note that the "README format" might more appropriately be described as "Markdown"; hence the .md file extension. There are plenty of guides online, but these two references are perhaps a tiny bit less obvious from Googling:

I'll also note just one more bit: you can assign a language for a code block so that it highlights better when viewed in GitHub and many tools. See example 112 and the text just above it.

DEFINING CLASSES

I take 'domain concepts' to be part of the 'modeling' nature of objected-oriented programs. So, the domain concept can be something in the physical world, a known and abstracted process, or any other entity that makes sense for the team working with it.

That seems spot on!

Both of these instances have different object_ids, but will otherwise behave very much the same.

I probably overemphasized object_id during that very first bug we looked at in bonecrawl. It would probably be most idiomatic to just say that they're different instances of the same type.

class BookInStock
  def initialize(isbn, price)
    @isbn = isbn
    @price = Float(price)
  end
end

In this case, the initialize method takes two parameters. They act like variables local to the method (and follow the same naming conventions). However, if they were truly just local variables, they would disappear when the initialize method returns.

To preserve them, we can pass them into instance variables as arguments.

Just minor language notes. The parameters are truly just local variables, and they do disappear. You record their values by assigning them to the instance variables (rather than "pass them into instance variables as arguments", which I think is mixing different ideas improperly). Again, just little language things, and I'm a bit of a pedant for it, which some people hate and which other people have thanked me for years later.

In the class, the Float method takes the argument price and converts it to a floating-point number. ... This parameter will accept anything for price, as long as it can be converted to a float. An integer, another float, or even a string containing (only) the representation of a float, will all work.

You can also read exactly what it does and how directly! https://ruby-doc.org/3.2.1/Kernel.html#method-i-Float

(Just encouraging good habits. And I was curious.)

It's worth noting that there are better ways to write this, because we shouldn't be holding prices in trailing floats--this is just to illustrate how the class works).

There are a few really interesting lessons underneath this, which culminate in: everything is stores in binary, 1s and 0s, and we have to interpret them in various ways to get our data, and some things are represented more easily/more difficultly or better/worse or not at all in various ways!

And you might know all of this already. But ultimately some numbers aren't representable in some formats. We can't represent 1/3 perfectly in decimal, and must approximate it: 0.3333333333... Similarly, binary can represent some numbers very well and other numbers must be approximated. You see that later:

Price = 33.8
New price = 25.349999999999998

It also recommended using BigDecimal and not Float for financial calculations

The common wisdom is to represent money in cents, since integers are exactly representable in binary. However, some financial stuff needs to go to sub-cent calculations, and BigDecimal is a good choice in Ruby.

The introduction is good! https://ruby-doc.org/3.2.1/exts/bigdecimal/BigDecimal.html

Apologies if you already know all of this.

OBJECTS AND ATTRIBUTES

The internal state is private to the instances we defined in the last exercise. No other object can access the instance variables holding that state. There is no such thing as perfect privacy in Ruby. Does this mean it's not a good language for security under some contexts, or all contexts?

To answer the question, I don't think this really relates to security in a straightforward sense. The public/private aspect here refers to access within a program, rather than any security from outside of your program. There are other concerns around languages and security, including how easy or hard languages make it to write bugs, but generally Ruby is fine for security! Even:

To prevent or mitigate the risks associated with memory safety, the NSA recommends that organizations use memory safe programming languages such as C#, Go, Java, Ruby, Rust, and Swift

https://www.securityweek.com/nsa-publishes-guidance-mitigating-software-memory-safety-issues/#:~:text=To%20prevent%20or%20mitigate%20the,memory%20safe%20actions%20or%20libraries.

The following quote felt disorienting to read:

"As far as other objects are concerned, there’s no difference between calling these attribute accessor methods and any other method. This is great because it means that the internal implementation of the object can change without the other objects needing to be aware of the change"

I've read it multiple times and I'm not following. What 'other objects' are we referring to? Other instances of the same class? Entirely separate classes?

It generally means "calling code", or code that works with your object. Generally this means entirely separate classes.

If you had a Store that had a collection of BookInStock objects, then if you choose eventually to change the representation of price, e.g. change the @price from a single float to a BigDecimal or to an integer representing cents or two variables, or a string, or anything else, as long as your price method behaves the same (returned a float representation) then Store doesn't need to know or care or change anything!

Another concrete example is your CsvReader. It has this code:

  def total_value_in_stock
    # later we'll see easier ways to sum a collection
    sum = 0.0
    @books_in_stock.each { |book| sum += book.price_in_cents }
    sum / 100.0
  end

And your book type has:

  def price_in_cents
    (price * 100).round
  end

What the book is talking about is that:

  def price_in_cents    # CsvReader depends on this line...
    (price * 100).round # But not on this one!
  end

Later you might do this:

class BookInStock
  attr_reader :isbn

  def initialize(isbn, price)
    @isbn = isbn
    @cents = (Float(price) * 100).round
  end

  def price
    @cents.to_f / 100
  end

  def price_in_cents
    @cents
  end
end

Now the internal representation is different, and the implementation is different, but... to the "outside world" (or "other objects"), it looks and behaves exactly the same!

This is part of the "separation of concerns" idea. The types can evolve and change independently of each other, without causing problems — as long as they agree on how to talk to each other.

WRITING TO ATTRIBUTES

From the text: "In Ruby, the attributes of an object can be accessed via the getter method and that access looks the same as any other method." This is another instance of the sentence that confused me on my first readthrough. I'm not sure what is meant by "looks the same as any other method".

Your type has:

attr_accessor :price
def price_in_cents
  # ...
end

From the outside world, they look the same:

book.price
book.price_in_cents

One is an accessor method for an attribute, the other is a method that computes a value. But they look the same syntactically. If you showed me that code and asked me which is which, I could not tell you.

That's not true of every language. For starters, most languages require parentheses for method calls and not for attributes. They might make it look like:

book.price
book.price_in_cents()

I'm not fully understanding why we have attr_reader for :isbn-- wouldn't it be quicker to just say:

attr_accessor :isbn, :price

I suppose ISBN isn't going to change, so it makes sense not to make it accessible to write over. Still, I'm thinking, if the same people are working on this program, and there aren't any other methods that write over :isbn, would it make sense to write it with attr_accessor referencing both? Or would that cause confusion?

You're on the right track here. A few notes maybe.

There's no such thing as "the same people" working on a program, even if it's only you working on a program. In 6 months time you might come back to debug something and you've completely forgotten everything you had in your head at the time. The code is the artifact that has to do the communication. After you've forgotten writing that class, coming back and reading attr_accessor :isbn says, "Well, I don't get it now, but I guess I needed that to be mutable for some reason, better keep an eye out."

Secondly, part of encapsulation is determining and restricting what other things can do! attr_reader :isbn says, "Phew, I don't have to worry, I can trust that no one else in this program is modifying this data. Once I assign it during initialization, I can trust that it's staying the same."

There's a corollary there, which is that restricting things to the least amount of behavior also prevents bugs! Say you copy paste a bunch of code and edit it, and you mess it up (common). You meant to write book.price = 5 but you accidentally wrote book.isbn = 5. If it's attr_reader :isbn you'll get, right there on that line, undefined methodisbn='. Then you see that and say, duh, oops, typo. But if it'sattr_accessor :isbn`, then you'll just assign it, and then maybe in your inventory report you see:

ISBN Price
"real ISBN" 7
5 9
"another ISBN" 19

And you look at that and you think... wtf. Now your debugging task is a lot harder than having the error immediately on the line where it occurred!

Using the Uniform Access Principle, we're benefiting from the difference between an instance variable vs calculated values.

I'd forgotten what that meant and had to look it up. Useful reminder.

ATTRIBUTES ARE JUST METHODS WITHOUT ARGUMENTS

The second method is accepting cents as an argument to actually set a new value to the instance variable @price. I experimented by removing the @ symbol from the instance variable @price, and found that the 1234 value of line 24 means nothing functional to the instance without it.

I assume you mean something like...

def price_in_cents=(cents)
  @price = cents / 100
end

and you experimented changing it to

def price_in_cents=(cents)
  price = cents / 100
end

And it didn't do anything, whereas you may have expected it to call your def price= method defined by attr_accessor and set the price that way.

Is that right? If so, it's a bit of a common trap with Ruby. price = cents / 100 gets interpreted as "calculate the value in the varaiable cents divided by the literal 100 and assign it to the local variable identified by price."

To tell the interpreter to use the setter rather than assigning to a local varaiable, you have to specify the receiver: self.price = cents / 100. That says "calculate the value... and call the method price= on self with it as a parameter".

erikamaker commented 1 year ago

I might note that the "README format" might more appropriately be described as "Markdown"; hence the .md file extension.

Noted! Markdown file type it is. Named as a counterpoint to hypertext "markup language" I'm guessing. It's for making text files readable and publishable in this format, or two conver plain text formatting to HTML's. And I can also have Ruby assigned like this:

ruby
def some_method
  return "hello, example!" 
end

It would probably be most idiomatic to just say that they're different instances of the same type.

That makes sense. It's not that it's wrong to say they have differnet object_ids, but that it's either: not super useful to know that in the context I was writing about it, or: it's not conventional to say it that way, since we care more about where the two objects exist in the hierarchy?

The parameters are truly just local variables, and they do disappear. You record their values by assigning them to the instance variables (rather than "pass them into instance variables as arguments", which I think is mixing different ideas improperly).

I always want to make sure I'm getting things right, so I'm sure I'd fall into the second category of people on the receiving end of your pedantry. So, when the book says that argument variables don't "disappear", what the text is really illustrating is that the local variable itself disappears at the end of the method (like any local variable would), but its value is preserved and referenced using the corresponding instance variable. Is that right?

(Just encouraging good habits. And I was curious.)

Every time I access it, the documentation is written in a much more accessible way than I anticipated. It's in my bookmarks folder, just gotta use it more now.

There are a few really interesting lessons underneath this, which culminate in: everything is stores in binary.

I see a definition of a "type" as "the common features of a set of objects with the same characteristics" for OOP. So the type influences how the 0s and 1s data is processed? Which is why (with the exception of exception supressing (that's a fun phrase)), exceptions are thrown if you try to send the wrong message to the wrong type of receiving object?

Apologies if you already know all of this.

I almost always know only parts of what I'm learning right now, so please feel free to throw basics at me as if I'm new to it all. Worst case is that it's a refresher, and the best case is that I learn something unexpected.

For instance, it was cool to learn that Ruby is recommended by the NSA for writing secure programs. So security can have different meanings depending on the context. The text wasn't suggesting data privacy for Ruby programs across networks was poor, it meant that objects in a Ruby program aren't perfectly private in their isolated context. Right?

  def price_in_cents    # CsvReader depends on this line...
    (price * 100).round # But not on this one!
  end

Regarding the comments on this snippet. I thiiink I can see what you're saying a little more now. So, because price_in_cents is being called, CsvReader only needs to find the method name that matches it. What happens inside the method is just going to return a value, and that value could be different for another instance that redefined what price_in_cents calculates to return. This has an even more removed relationship to an unrelated class whose own price_in_cents method is not being called. What matters is how the types communicate this data, since sometimes types are compatible, but other times they are not. Is that right?

The code is the artifact that has to do the communication.

I'm rearranging my engine with this in mind the last few days. Trying to break up unrelated expressions in the same methods to instead call a method to return a value. It's making things easier, like the puzzles have less pegs the more I break it down.

It is, however, alarming how overly complicated some parts are. Renaming some variables has also helped me keep better track of some that were ambiguously named before. I see that how the code is written can suggest (or elude) how one should interpret its modeling. Pretending it's new every time I look at it is helping me break it down.

There's a corollary there, which is that restricting things to the least amount of behavior also prevents bugs!

Better encapsulation of some of these complicated areas is causing less unexpected behavior. I'm finding that I can imagine new method implementations that fit seamlessly with the rest of the puzzle before coding it. It's empowering to dream up a feature and just add it like that.

And it didn't do anything, whereas you may have expected it to call your def price= method defined by attr_accessor and set the price that way. Is that right?

Yes! So, specifying the receiver changes the game in that kind of context? Instead of price = cents / 100 only assigning the value to an isolated variable in an instance method, saying self.price = cents / 100 invokes the setter method for the instance variable. The latter is functionally similar to @price = cents / 100. Is that right?

ianterrell commented 1 year ago

It would probably be most idiomatic to just say that they're different instances of the same type.

That makes sense. It's not that it's wrong to say they have differnet object_ids, but that it's either: not super useful to know that in the context I was writing about it, or: it's not conventional to say it that way, since we care more about where the two objects exist in the hierarchy?

That's right — it's not wrong to say they have two different object_ids, but what that means is that they live in two different memory locations (and therefore may have different data), and what that means is that they're two different instances.

We don't really think about memory locations or object_ids — we tend to think slightly higher, in terms of "are they the same instance" or the related "are they equal".

So, when the book says that argument variables don't "disappear", what the text is really illustrating is that the local variable itself disappears at the end of the method (like any local variable would), but its value is preserved and referenced using the corresponding instance variable. Is that right?

That's right!

("Referenced" is a tricky word and sometimes a term of art with a specific meaning, but you understand what's happening.)

Every time I access it, the documentation is written in a much more accessible way than I anticipated. It's in my bookmarks folder, just gotta use it more now.

Yes!

Sometimes documentation is terrible. It can be outright missing, or worse than that it can be out of date. It can also be difficult to parse sometimes.

But on average people put a lot of love and care into it. And the more you read it, the more you learn how to read it.

There are a few really interesting lessons underneath this, which culminate in: everything is stores in binary.

I see a definition of a "type" as "the common features of a set of objects with the same characteristics" for OOP. So the type influences how the 0s and 1s data is processed? Which is why (with the exception of exception supressing (that's a fun phrase)), exceptions are thrown if you try to send the wrong message to the wrong type of receiving object?

From the perspective of OOP, you're spot on. A type (a class) is that common set of features you describe. I might reinforce "data and behavior" as that set of features, although you can sort of get away with classes with only data and sort of get away with classes with only behavior. But if one class has the behavior you want and another doesn't, the one without will raise the exception on that message.

For full understanding of what I meant, there's the wrinkle of the "primitive types". It's important to understand a little of, eventually, but I wouldn't worry about it much for now. But if it interests you, here's a small example. Skip between the lines if it's too much.


I'm sure it's possible to explore this in Ruby, but I don't know how! :) It's easier in a language that lets you get closer to the hardware, like C. Here's a small C program:

#include <stdio.h>

int main() {
    int i = -1;
    printf("%d %u\n", i, i);
    return 0;
}

You can run it here: https://replit.com/languages/c

It's very different from Ruby, but parts are probably readable. You have a variable i which is an integer with the value -1. Then we print it out, twice, but interpreting it differently.

The first argument to printf is a format string, which says how to print things out. The subsequent arguments are the values to substitute into the string. %d says interpret it as a signed integer; that is, an integer that can be positive or negative. %u says interpret it as an unsigned integer; that is, an integer that can only be positive. Then we pass it i twice, saying use the same value for both.

The output is -1 4294967295.

The thing to understand here is that the data is the same in both cases. They have the same underlying 0s and 1s — we just chose to interpret them differently!

There are other ways you can interpret the raw 0s and 1s: as a floating point number, as a character in a language, as a reference to a particular type, or even as multiple pieces of data at the same time.

Here's an example that shows what the binary representation of pi looks like as an integer:

#include <stdio.h>
#include <math.h>

typedef union {
  int i;
  float f;
 } u;

int main() {
    u u1;
    u1.f = M_PI;
    printf("%d %f\n", u1.i, u1.f);
    return 0;
}

It outputs: 1078530011 3.141593.

The same binary representation underneath!


The text wasn't suggesting data privacy for Ruby programs across networks was poor, it meant that objects in a Ruby program aren't perfectly private in their isolated context. Right?

Yes. It just means that private is a "recommendation" rather than an enforced language construct. Other languages are more strict.

class Foo
  private def secret
    "s3cret"
  end
end

f = Foo.new
f.secret # private method `secret' called for #<Foo:0x000000010b20b000> (NoMethodError)

f.send :secret
# => "s3cret"

That's all it really means. You can't do that as easily in something like Java.

Regarding the comments on this snippet. I thiiink I can see what you're saying a little more now. So, because price_in_cents is being called, CsvReader only needs to find the method name that matches it. What happens inside the method is just going to return a value, and that value could be different for another instance that redefined what price_in_cents calculates to return. This has an even more removed relationship to an unrelated class whose own price_in_cents method is not being called. What matters is how the types communicate this data, since sometimes types are compatible, but other times they are not. Is that right?

Two related concepts to keep in your head:

  1. Encapsulation — implementation can change as long as contract doesn't
  2. "Compatible types" — in dynamic languages often called duck typing

First, encapsulation. Here's a terrible example, but say you have an Adder that computes a sum:

class Adder
  def initialize(a, b)
    @a = a
    @b = b
  end

  def sum
    @a + @b
  end
end

Then you can have a small program that prints out sums:

def print_sum(adder)
  puts adder.sum
end

print_sum(Adder.new(1, 2)) # => 3

Now, later, you decide... well gosh, my Adder should be able to sum any number of addends, not just two! So you refactor it like so:

class Adder
  def initialize(*args)
    @args = args
  end

  def sum
    @args.sum
  end
end

You've changed the implementation of Adder, pretty dramatically! And yet, because you didn't change the API or the contract — meaning, you can call .new and .sum in the exact same way, the other program you wrote runs exactly the same!

def print_sum(adder)
  puts adder.sum
end

print_sum(Adder.new(1, 2)) # => 3

No changes were necessary to it! That's what I meant by the code depends on the method rather than the method's implementation.

But there's also the related duck typing:

class AdderTheSnakeKind
  def initialize(*args); end
  def sum; "hiss"; end
end

def print_sum(adder)
  puts adder.sum
end

print_sum(Adder.new(1, 2))
print_sum(AdderTheSnakeKind.new(1, 2))

In this case, AdderTheSnakeKind behaves with the same API as Adder, so you can use it everywhere. Well, sort of. If you were expecting a numeric value to sum you'll crash eventually on the string. :)

We say "duck typing" because "if something quacks like a duck you can use it like a duck".

class Duck
  def quack; "quack"; end
end
class Goose
  def quack; "honk"; end
end

They both "quack like a duck".

I'm rearranging my engine with this in mind the last few days. Trying to break up unrelated expressions in the same methods to instead call a method to return a value. It's making things easier, like the puzzles have less pegs the more I break it down.

It is, however, alarming how overly complicated some parts are. Renaming some variables has also helped me keep better track of some that were ambiguously named before. I see that how the code is written can suggest (or elude) how one should interpret its modeling. Pretending it's new every time I look at it is helping me break it down.

That is all super great! Good job!

So, specifying the receiver changes the game in that kind of context? Instead of price = cents / 100 only assigning the value to an isolated variable in an instance method, saying self.price = cents / 100 invokes the setter method for the instance variable. The latter is functionally similar to @price = cents / 100. Is that right?

The thing to think about here is how does Ruby know what you mean? It has to make choices along the way as it reads your code. foo = 1 looks like it could be either calling the foo= method or assigning a local variable foo. Ruby chooses the latter option, probably because it's simply more common.

The latter is functionally similar to @price = cents / 100. Is that right?

Well.... yes and no!

If you call self.price = cents / 100 then if the implementation of price= changes, you're always kept up to date with it!

Lots to think about — hope some of this is helpful. Please ignore if it's too much and just barrel onward!

erikamaker commented 1 year ago

We don't really think about memory locations or object_ids — we tend to think slightly higher, in terms of "are they the same instance" or the related "are they equal".

When do you think would be an appropriate time to focus on object_ids? Debugging?

I might reinforce "data and behavior" as that set of features, although you can sort of get away with classes with only data and sort of get away with classes with only behavior.

A little googling says that a "feature" is a prominent attribute (something that we want to be accessible). Just making sure I have this right? I'm familiar with behavior and data, but "feature" isn't something I've heard outside of casual conversation and I wanna make sure I know it well.

The output is -1 4294967295. The thing to understand here is that the data is the same in both cases. They have the same underlying 0s and 1s — we just chose to interpret them differently!

So, for this C example, the value is -1 regardless-- but viewing -1 through a positive-integer-expectant lens, it becomes 4294967295? Wouldn't it just throw an error? I understand that it's a representation of -1 but I'm a little thrown.

Encapsulation — implementation can change as long as contract doesn't

Sorry, full of quesitons today. What is a contract in terms of OOP? I tried searching it and only found a medical abstract about placentas. I don't think it's the link I needed.

If you call self.price = cents / 100 then if the implementation of price= changes, you're always kept up to date with it!

Last one, I promise (related to the most recent question)! What do you mean by "if the implementation of price=" changes? If we already defined it, that's the implementation of it, as I understand it now. So, redefining it, means that if I use price=, I'll always have the newest "version" of that method. Is that what implementation you're referring to?

Thanks for your thoughts as always! Hope you're having a good week.

ianterrell commented 1 year ago

When do you think would be an appropriate time to focus on object_ids? Debugging?

😅 Uh... sure! :) I use them almost never. The only question they answer is "are these two things the literal same instance?"

If that's a useful question, that can provide an answer. But the answer is also usually in the object's inspect method, which is often printed out.

irb(main):007:0> a = Foo.new
=> #<Foo:0x00000001530e8c20>
irb(main):008:0> b = Foo.new
=> #<Foo:0x00000001530e0908>
irb(main):009:0> c = a
=> #<Foo:0x00000001530e8c20>

The big long hex values in the output are the location of the items in memory, which is equivalent to .object_id (and can be converted back and forth in some versions of Ruby). So when I'm debugging that information is usually available and I don't have to resort to object_id.

A little googling says that a "feature" is a prominent attribute (something that we want to be accessible). Just making sure I have this right? I'm familiar with behavior and data, but "feature" isn't something I've heard outside of casual conversation and I wanna make sure I know it well.

I think you wrote originally:

I see a definition of a "type" as "the common features of a set of objects with the same characteristics" for OOP.

And I was responding to that by trying to clarify "features" down to "data and behavior", because "feature" is kind of a loose word with only casual definitions.

I think the only usage of "feature" I have at work day to day is "something the product does".

So, for this C example, the value is -1 regardless-- but viewing -1 through a positive-integer-expectant lens, it becomes 4294967295? Wouldn't it just throw an error? I understand that it's a representation of -1 but I'm a little thrown.

It was a haphazard attempt at explaining something that's probably not useful to know at this point in time! But since I started, and because I find it fun, and because I think there are nuggets of philosophy inside it.

If everything is 0s and 1s, how do we know what any sequence of 0s and 1s means?

Pretend we used the letters abcdefghij for our current digits 0-9. If I wrote cab down on a piece of paper and handed it to you, would you think I meant a taxi, or would I be telling you the number 201? The truth is, there's not enough information for you to know! The answer depends on how you read it. "How you read it" can be encoded as the data type. If I wrote instead string: cab you would know it's a taxi; if I wrote number: cab you would know it's 201. But the data cab is the same in both cases.

That's the core insight. In the c example, the same data is either the value -1 or the value 4294967295, in the same way that cab is either the value "taxi" or 201 — depending on how you interpret it.

Binary Encodings hidden in spoiler — click to reveal You know decimal, using 10 digits. It's "base-10", so each column is a factor of 10 higher. So we have the ones column, the tens column, the hundreds column... `123`, read right to left, can be described as `3*1 + 2*10 + 1*100`; or `3*10^0 + 2*10^1 + 1*10^2`, where the exponent is the column number (starting at 0, right to left). (Also fun, and I'm sure you know, but it's right to left because these are _arabic_ numerals.) Binary is the same, but "base-2", so you only have 2 digits and each column is a factor of 2 higher. So `1011` is `1*2^0 + 1*2^1 + 0*2^2 + 1*2^3`. Okay, so say we have 3 bits to work with; three digits of 0 or 1. We can store 8 distinct values. You count up just like in base-10, where you "carry the 1". ``` 000 001 010 011 100 101 110 111 ``` So you have 8 values... but what do they mean? You can count from 0 to 7 if you want, if you read it just like the `0*2^0 + 0*2^1...` interpretation. But what if you also want to represent negative numbers? There's no `-` sign, there's just 0s and 1s! So one of them has to stand for a minus sign. Which one should be the minus sign? Well, it's arbitrary. We could pick the left one, the middle one, or the right one. And then if we make a bit the minus sign, we have to decide how to interpret the other bits. And then you and I need to agree on it! Otherwise if I say `001` is positive but you think it's negative, then we will be interpreting the data differently. Anyway, the usual way computers do it is called two's complement; [wikipedia](https://en.wikipedia.org/wiki/Two%27s_complement). And here's what people have decided: Screenshot 2023-02-24 at 10 59 51 AM So we have... - We have symbols in the form of data (`cab`, `100`) - By themselves they don't have meaning (they're just `cab`, `100`) - I can assign a meaning to it ("taxi", 4) - You could assign a different meaning to it (201, -4) - The meanings were arbitrary, and we could assign entirely different meanings than the above, like `cab` meaning "compression-airway-breathing" from CPR or `100` meaning the value "one hundred" - Things are a lot more productive if we agree on the same meaning - We have to track and communicate that meaning separately (string, signed integer, acronym, binary, decimal, etc, etc) I find that fun to think about from a philosophical point of view. As human beings were constantly doing the above intuitively and naturally and usually correctly — but only to the extend that "correct" has any meaning, when the meanings assigned are arbitrary. We also do this incorrectly sometimes — or fail to agree — to the results of conflict and misunderstanding. Or we find and assign meaning to life and the universe, or not, or different meanings, etc, etc, etc. See also Wittgenstein, whose works I'm only partially through and only partially understand.

Encapsulation — implementation can change as long as contract doesn't

Sorry, full of quesitons today. What is a contract in terms of OOP? I tried searching it and only found a medical abstract about placentas. I don't think it's the link I needed.

Hmm... not about placentas. :) It's about agreement! It has a formal definition in some languages, but used a bit informally what I mean is: what does the code promise to do?

Other language make this more clear than Ruby does. In C for instance you define a function like this:

int foo(char c)

That says: "I am a method named foo that takes a single argument of type char (character), and returns an int (integer)."

That's a "contract". Other code can "rely" on that. Other code can call foo with any char and always expect to get back an int.

What's important about that is that we know that's true no matter what is inside foo. It could always return 1:

int foo(char c) { return 1; }

Or it could convert the character into an integer:

int foo(char c) { return (int)c; }

Or it could do anything else — whatever it does, we know it obeys the contract described by the function definition.

So when you are writing foo you can change the body without requiring changes to wherever you're using foo.

Granted, there are large semantic differences between different implementations, so you might want to stick to changes that also "mean the same thing".

If you call self.price = cents / 100 then if the implementation of price= changes, you're always kept up to date with it!

Last one, I promise (related to the most recent question)! What do you mean by "if the implementation of price=" changes? If we already defined it, that's the implementation of it, as I understand it now. So, redefining it, means that if I use price=, I'll always have the newest "version" of that method. Is that what implementation you're referring to?

Yes, I think you've got it.

It's hard to come up with an amazing example on the spot here, but, say you have a book with a price and you can apply a discount:

class Book
  def initialize(price)
    self.price = price
  end

  def price
    @price
  end

  def price=(price)
    @price = Float(price)
  end

  def apply_discount(percent)
    @price = @price - (@price * percent)
  end
end

book = Book.new(10.50)
puts "Before Discount: #{book.price}"
book.apply_discount(0.1)
puts " After Discount: #{book.price}"

That outputs

❯ ruby price.rb
Before Discount: 10.5
 After Discount: 9.45

Now... say you change your internal representation of price to an array of dollars and cents, but you forget to change your implementation of apply_discount, which is directly reading and writing the internal representation:

class Book
  def initialize(price)
    self.price = price
  end

  def price
    @price[0] + @price[1] / 100.0
  end

  def price=(price)
    price = Float(price)
    @price = [price.to_i, (price * 100 % 100).to_i]
  end

  def dollars
    @price[0]
  end

  def cents
    @price[1]
  end

  def apply_discount(percent)
    @price = @price - (@price * percent)
  end
end

book = Book.new(10.50)
puts "Before Discount: #{book.price}"
book.apply_discount(0.1)
puts " After Discount: #{book.price}"

Now we have...

❯ ruby price2.rb
Before Discount: 10.5
 After Discount: 10.5

that doesn't seem right... But had we implemented it with the getter and setter, relying on their implementations to do the work, we'd have:

class Book
  def initialize(price)
    self.price = price
  end

  def price
    @price[0] + @price[1] / 100.0
  end

  def price=(price)
    price = Float(price)
    @price = [price.to_i, (price * 100 % 100).to_i]
  end

  def dollars
    @price[0]
  end

  def cents
    @price[1]
  end

  def apply_discount(percent)
    self.price = self.price - (self.price * percent)
  end
end

book = Book.new(10.50)
puts "Before Discount: #{book.price}"
book.apply_discount(0.1)
puts " After Discount: #{book.price}"

Which outputs

❯ ruby price3.rb
Before Discount: 10.5
 After Discount: 9.44

(Which is, in this case, still a bit different, but due to rounding and conversions and float behavior.)

This is contrived — and within a type it's okay to write stuff directly, when it makes sense to do so, which is most of the time — but I was trying in the original comment and in this example to show how you can leverage the building blocks of other methods, even readers and writers, to remain consistent across changing implementations.

erikamaker commented 1 year ago

I think the only usage of "feature" I have at work day to day is "something the product does".

That makes sense!

If everything is 0s and 1s, how do we know what any sequence of 0s and 1s means?

I briefly took a stab at learning binary and was able to write some simple sentences with it. So, if you know the rules of a language, you can exchnage meaningful information through its syntax. The lens of how that information is referenced can change its semantics though? Is that fair? What looks ike something in one context can look wildly different in another.

It has a formal definition in some languages, but used a bit informally what I mean is: what does the code promise to do?

A little more literal than I expected, but that makes sense.

So when you are writing foo you can change the body without requiring changes to wherever you're using foo.

I can see how that would be advantageous. Like, in a project where each role's details might evolve over time, as long as the role itself remains cohesive and true to its intended purpose, the event occurs as it should.

Granted, there are large semantic differences between different implementations, so you might want to stick to changes that also "mean the same thing".

Which is why you said this, I think. Right?

ianterrell commented 1 year ago

So, if you know the rules of a language, you can exchnage meaningful information through its syntax. The lens of how that information is referenced can change its semantics though? Is that fair? What looks ike something in one context can look wildly different in another.

I wish I had a better understanding of this in the general sense — I should finally read my Chomsky, I guess — but I'd say from a programming point of view you've got it exactly. I might say that data is insufficient by itself to be semantically meaningful; it requires a context. Here we might say a variable requires both a value (data) and a type (context).

I can see how that would be advantageous. Like, in a project where each role's details might evolve over time, as long as the role itself remains cohesive and true to its intended purpose, the event occurs as it should.

Yes! That's exactly it!

Getters and setters are not the best example, although this technique is occasionally applied to them. That happens more often in other languages than Ruby; Ruby is friendly and usually reading and writing is straightforward.

Granted, there are large semantic differences between different implementations, so you might want to stick to changes that also "mean the same thing".

Which is why you said this, I think. Right?

Yes!

The most usual way this technique is applied is called "refactoring." That's simply when you take working code that does what you want and make it "better" while keeping its behavior the same.

"Better" can mean a few things, including objective measurements like performance. But it's more often subjective and the aspects to focus on are clarity, readability, and maintainability.

It's what you're doing with your game engine: slowly making it better while it does the same thing. The more you can encapsulate bits of behavior into the right size chunks of classes and methods, the more you have the freedom to improve those classes and methods with confidence that nothing else is impacted.