From a8366f37b6e195e9677f9c2bb1d00424ea1eb1ad Mon Sep 17 00:00:00 2001 From: Sam Morgan Date: Fri, 24 Nov 2017 15:45:05 +0000 Subject: [PATCH] Refactor a lot of the chapters * Finish up chapter 10 * Consistent model --- chapter10/README.md | 131 +++++++++++++++++++++++++++++----- chapter6/README.md | 26 +++++-- chapter7/README.md | 98 +++++++++++++------------ chapter8/README.md | 26 ++++--- chapter9/README.md | 169 +++++++++++++++++++++++++++++++++++++------- 5 files changed, 348 insertions(+), 102 deletions(-) diff --git a/chapter10/README.md b/chapter10/README.md index b992df9..24a9a70 100644 --- a/chapter10/README.md +++ b/chapter10/README.md @@ -1,6 +1,6 @@ # Writing your own Classes -We've been spending a lot of time writing complex procedures. We've spent a lot of time _telling the computer how to get to the answer_: +We've been spending a lot of time writing complex procedures. We've spent a lot of time _telling program objects how to get to the answer_: ```eval-ruby # set up some scores @@ -19,26 +19,26 @@ end accumulator.to_f / scores.length ``` -- _Telling the computer how to get to the answer_ might look like: "create an accumulator. Add the scores to it. Then divide them." This is called **imperative** programming – literally, 'ordering the computer to do things'. +- _Telling an object how to get to the answer_ might look like: "create an accumulator. Add the scores to it. Then divide them." This is called **imperative** programming – literally, 'ordering the computer to do things'. -In [Chapter 9](../chapter9/README.md), we started seeing how we could take these imperative procedures and hide them behind _method names_. This means that our code looks more like the high-level idea of _telling the computer what we want_, by interacting with abstractions: +In [Chapter 9](../chapter9/README.md), we started seeing how we could take these imperative procedures and hide them behind _method names_. This means that our code looks more like _telling an object what we want_, by interacting with abstractions: ```ruby # average some scores, please average(scores) ``` -- _Telling the computer what we want_ might look like "average these scores." This is called **declarative** programming – literally, 'saying what we would like to happen'. +- _Telling an object what we want_ might look like "average these scores." This is called **declarative** programming – literally, 'saying what we would like to happen'. -When we're writing complex procedures, we're often working in a very _imperative_ world. We're handling things like _control flow_ with _conditionals_ and _loops_, working with simple objects like _arrays_, _strings_, _hashes_, and _integers_. It's a tough world, and it's quite unfriendly to non-programmers. +When we're writing complex procedures, we're often working in a very _imperative_ world. We're handling things like _control flow_ with _conditionals_ and _loops_, working with simple objects like _arrays_, _strings_, _hashes_, and _integers_. It's a tough world, and it's quite unfriendly to non-programmers (and unfriendly to programmers who didn't write the procedure themselves). -Therefore, as programmers, we try to abstract our work to a declarative style wherever possible. This is why I introduced you first to the program world: because it's a declarative world, where you can ask `1` if it's an integer: +Therefore, as programmers, we try to abstract our work to a declarative style wherever possible. This is why, back in [Chapter 1](../chapter1/README.md), we were first introduced to the 'program world': because it's a declarative world, where you can ask `1` if it's an integer: ```eval-ruby 1.integer? ``` -And it'll just _tell you the answer_. +And it'll just _tell you the answer_. Sure, there's probably some imperative procedure going on under the hood. In fact, there _definitely is_. But we don't have to know about it. That frees us up to think about more high-level program concerns, like how to meet a particular requirement. These are the kinds of worlds we strive to create as programmers. One very popular route to doing so is to use **Object-Oriented Programming**. We've met a bunch of this already, but here's a quick refresher: @@ -71,7 +71,7 @@ In particular, remember where all the complex imperative procedures we've been w -That's right – procedures are running inside methods. We learned how to write methods in [Chapter 9](../chapter9/README.md) – but the methods we defined weren't obviously part of any given object's interface. So where have they been living? +Procedures are running _inside methods_. We learned how to write methods in [Chapter 9](../chapter9/README.md) – but the methods we defined weren't obviously part of any given object's interface. So where have they been living? ## Global scope @@ -106,9 +106,9 @@ end players_by_sport ``` -...we're defining variables on the main program object, creating objects inside the main program object, and returning values to the main program object (which, if we're using a REPL, the main program object returns to us). +...we're asking the main program object to do all this work. We're instructing the universe directly. -When we wrote the `average` method: +In [Chapter 9](../chapter9/README.md), when we moved the averaging procedure into the method named `average`: ```eval-ruby def average(scores) @@ -122,7 +122,7 @@ def average(scores) end ``` -We defined this method _on the main object_. +We defined this `average` method _on the main object_. > In Ruby, there are no 'objectless methods'. @@ -620,7 +620,7 @@ class Robot end def walk - return "I'm walking on my " + @number_of_legs + " legs!" + return "I'm walking on my " + @number_of_legs.to_s + " legs!" end end @@ -883,7 +883,7 @@ class Dog end ``` -Variables that we define inside these things cannot be seen outside of them: +Variables that we define inside a `def...end` or `class...end` cannot be seen outside of them: ```eval-ruby def average @@ -898,7 +898,7 @@ puts accumulator puts some_variable ``` -The area of a program in which a variable can be read is called the variable **scope**. `def` and `class` are known as **scope gates**: when the program runs this instruction, it enters a new scope. Variables defined inside this scope cannot be read outside of the scope gate. Variables defined outside of the scope gate cannot be read inside it. +The area of a program in which a variable can be read is called the variable **scope**. `def` and `class` are known as **scope gates**: when the program runs a line containing `def` or `class`, it enters a new scope. Variables defined inside this scope cannot be read outside of the scope gate. Variables defined outside of the scope gate cannot be read inside it. ```eval-ruby my_variable = 1 @@ -917,7 +917,7 @@ Here's a visual representation of scope: > Confused by the word 'scope'? Think of the scope on top of a sniper-rifle: when you look down it, you can only see a part of the world: the part of the world you can shoot. -## Scope and parameters +## Using scope to understand method parameters Scope is especially helpful when it comes to understanding method parameters. For each parameter, methods will define a local variable within their scope. The name of that variable will be set to the name of the parameter: @@ -932,7 +932,22 @@ end age_reporter(age) ``` -Since `def` is a scope gate, we can't read these local variables outside of them: +The same goes for parameters provided to procedures inside `each` loops: + +```eval-ruby +people = ["Steve", "Yasmin", "Alex"] + +people.each do |name| + # Each element of people is assigned to a local variable called 'name' + # We can then read that local variable inside this loop + puts name +end + +# But we can't read the local variable outside the loop +puts name +``` + +Since `def` is a scope gate, we can't read these parameters outside of their message body: ```eval-ruby name = "Sam" @@ -943,4 +958,86 @@ end # We can't read this person -``` \ No newline at end of file +``` + +[This article](https://www.sitepoint.com/understanding-scope-in-ruby/) dives a little deeper into the idea of 'scope' in Ruby. + +- **Scope** is what parts of the program an object can 'see' at any time. + +## Using Objects to be more declarative + +By defining our own classes, we can make our programs super-readable: almost like English. Here's an example: + +```ruby +heathrow = Airport.new +plane = Plane.new + +heathrow.land(plane) + +heathrow.hangar_report +# => "There is 1 plane in the hangar" +``` + +Also, we can stop programmers from doing things that don't fit with our model of how the program should work: + +```ruby +heathrow = Airport.new + +heathrow.take_off(plane) +# => "Error: There are no planes to take off" +``` + +In the examples above, the object referenced by `heathrow` is an instance of the `Airport` class. Somehow, it can store planes. At the heart of the `heathrow` object, in its state, lives an array – an object we introduced in [Chapter 7](../chapter7/README.md) as a way to store other objects. Around this array, `heathrow` defines methods which are appropriately-named for the program being run – some sort of airport simulation. Here, the 'airport simulation' is known as the program **domain**. + +Why don't we just use an array, instead of instaces of this `Airport` class? That way, the program could work like this: + +```ruby +heathrow = [] +plane = "Boeing 737" + +heathrow.push(plane) + +"There is #{ heathrow.length } plane in the hangar" +``` + +The answer is: the second describes the program domain less effectively. As programmers, we strive to write code that accurately reflects the program domain. + +Here's how `Airport` 'wraps' an array: + +```eval-ruby +class Airport + def initialize + @hangar = [] + end + + def land(plane) + @hangar.push(plane) + end + + def take_off(plane) + if @hanger.length > 0 + if @hangar.includes? plane + plane_index = @hangar.index(plane) + @hangar.delete_at(plane_index) + return plane + else + return "Error: plane not in hangar" + end + else + return "Error: there are no planes to take off" + end + end + + def hangar_report + if @hangar.length == 1 + "There is 1 plane in the hangar" + else + "There are #{ @hanger.length } planes in the hangar" + end + end +end +``` + +Because the array is referenced only by an _instance variable_, nothing outside of `Airport` instances can read it: so we're protected from programmers trying to `pop` a plane from `heathrow`, and so on. This essentially _limits the interface of the array_ to only methods we want to allow. + +> Spend some time playing with the code example above. Write a `Plane` class if you can – get the initial code example running. The code here is similar to code introduced in the first week of Makers Academy, so don't worry if it boggles you at the moment. diff --git a/chapter6/README.md b/chapter6/README.md index c04e853..a230278 100644 --- a/chapter6/README.md +++ b/chapter6/README.md @@ -1,6 +1,14 @@ # Advanced Control Flow -We now have access to the following tools: +We've met the following kinds of object: + +* Integers +* True and False +* Nil +* Floats +* Strings + +And we the following tools: * Naming * Statements @@ -10,11 +18,21 @@ We now have access to the following tools: * Strings * `gets` and `puts` -These are all the things required to write a more complex interactive procedural program! +We can now control these objects using these tools, to write a more complex procedural program. Let's do it! ## Using string input conditionally -Let's say we want to have the following program: +A user is asking us for the following program: + +> I hate my mate Steve: so much so that my hatred spills over to anyone with a first name beginning with 'S'. I want a program that, when anyone types their name in, shouts at them if their name begins with an 'S'. Anyone else should just get a friendly greeting. + +The statement above is an example of a **specification**: a vague answer to the question "what do you want this program to do?". Our first job as programmers is to break specifications into manageable, step-by-step instructions that we can then feed into the machine. + +We do this to avoid biting off more than we can chew. The most common programming mistake is to try and build too much, too fast. We want to decompose this specification into really clear, step-by-step **requirements**. + +> The process of breaking a specification into requirements is called **algorithmic thinking**. + +Here's the list of requirements for the specification above: - The user sees a greeting, which asks them to enter their name. - The user enters their name. @@ -28,7 +46,7 @@ Let's say we want to have the following program: When building a program, we move step-by-step through the requirements. -> Biting off too many requirements at once is the #1 mistake in programming. The best programmers do everything they can to avoid it. That's why the idea of the 'best programmer is a lazy programmer' took hold. Force yourself to move in small steps now! It's a habit that will pay off. +> The best programmers do everything they can to avoid 'biting off more than they can chew', or 'moving in too large a step'. That's why the idea of the 'best programmer is a lazy programmer' took hold. Force yourself to move in small steps now! It's a habit that will pay off. During the Makers Academy course, we'll meet tools designed to force us to move in small steps. #### 1. Make the file diff --git a/chapter7/README.md b/chapter7/README.md index 49e6d91..bb4016d 100644 --- a/chapter7/README.md +++ b/chapter7/README.md @@ -7,17 +7,17 @@ So far we've met objects that can really only 'contain' one thing: We call whatever an object 'contains' that object's **state**. -What if we want to store more than one thing in an object's state? Is there an object for that? +What if we want an object whose state can store more than one thing? Is there an object for that? -> We've actually met an object that can contain many things: The Main Object contains all the objects in the program. Therefore, when we describe the program world, and the objects in it, we're really describing the state of the main object: the **program state**. +## `Array` -## Collections +The `Array` class creates instances that can store many other objects inside themselves. Like `String` instances are called 'strings', `Array` instances are called 'arrays'. -The `Array` class is used to make a 'collection object': an object that can store many other objects inside itself. Crucially, a Ruby array is **mutable**: it can be made larger, by adding more objects. You can also remove objects from it, and it'll shrink back down to size: like an elastic band. +Arrays can have objects added to them, and removed from them: they'll grow and shrink depending on how many objects they contain, like an elastic band. -> An instance of the `Array` class is called an array. +> Any object which can change its state during the course of the program world's existence is referred to as being **mutable**. Strings can be chopped up, capitalized, and so on: they're mutable, too. Integers can't be changed: `1` will always represent `1`. They're **immutable**. Because other objects can change the state of an array – by adding or removing objects to or from it – arrays in Ruby are _mutable_. -Here's how you use the **Array** class: +Let's instruct the `Array` class to create a new instance. Then, let's use the array instance method `push` to add some strings into the array: ```eval-ruby an_array = Array.new @@ -26,7 +26,7 @@ an_array.push("Hello World") an_array.push("It's me!") an_array.push("Mario!") -return an_array +an_array ``` @@ -37,7 +37,7 @@ return an_array - _**Add a couple of integers and floats to the array.**_ - _**Add the main object to the array.**_ -Hold on. If the main object contains all other objects – including this array we just made – how can we add it to the array? How can something inside the universe contain the universe itself? + ## Modifying arrays -We've seen that we can add things to an existing array object by using the array's `push` method, with the object-to-be-added as an argument to that method: +We've seen that we can add things to an existing array object's state by using the array's `push` method, with the object-to-be-added as an argument to that method: ```eval-ruby another_array = [] another_array.push("A short string") + +another_array ``` > Things inside an array are called **elements**. -Arrays have syntactic sugar, too: `[]` does the same thing as `Array.new`. Using this shorthand, you can set up arrays that already contain elements: +Just like Ruby offers syntactic sugar for creating strings with `""`, arrays have syntactic sugar, too: `[]` does the same thing as `Array.new`. Using this shorthand, we can set up arrays pre-filled with elements: ```eval-ruby an_array_containing_elements = ["This", "is", "an", "array"] ``` -You can remove elements from an array using `delete_at(index)`: +We can remove elements from an array by sending it the message `delete_at`, with an argument denoting the 'index' of the element we want to remove. For arrays, this 'index' is a number, which denotes an element in the array. The first element is index `0`, the second is index `1`, the third is index `2`, and so on: ```eval-ruby array = ["a", "b", "c"] @@ -74,9 +76,9 @@ array.delete_at(1) array ``` -> Watch out! Arrays are _zero-indexed_. That means they count from zero: the first element is in position `0`. +> Watch out! Arrays are _zero-indexed_. That means they count from zero: the first element is in position `0`. Play with the code example above until that's clear. -Or just the last element using `pop`: +We can also remove just the _last element of an array_ by sending an array the `pop` message: ```eval-ruby array = [1, 2, 3] @@ -90,7 +92,7 @@ All of the above will modify the array: that is, the array will change when you ## Reading arrays -You can read a single element from an array by using the `[]` method: +You can read a single element from an array by sending the array the `[]` method with the index of your desired element: ```eval-ruby array = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] @@ -103,7 +105,7 @@ array[0] - _**Access different elements of the array above using `[]`.**_ -You can read a bunch of elements at once, using `slice`: +You can read a bunch of elements at once, by calling the array's `slice` method: ```eval-ruby array = [1, 2, 3, 4, 5, 6] @@ -115,7 +117,7 @@ array.slice(0, 1) ## Outputting from arrays -Arrays can be useful for storing elements you might want to keep separate sometimes, and have together in other times. If storing strings in an array, it's pretty common to use `join` to output them stitched-together: +Arrays can be useful for storing elements you might want to keep separate sometimes, and join together in other times. A common use case for an array is to store strings, then to send the array of strings a `join` message, which will concatenate them: ```eval-ruby sentence = ["Hello,", "you", "are", "NOT", "welcome", "here"] @@ -125,7 +127,7 @@ sentence.join(" ") > The argument you provide to `join` is the joining character. Here, we used a space (`" "`). What happens if we use a different character? -- _**Add a line of code that removes the `"NOT"` string from the array, making the sentence friendlier. Don't modify the original array!**_ +- _**Add a line of code that removes the `"NOT"` string from the array, making the sentence friendlier.**_ %accordion%See how I'd do it%accordion% @@ -156,17 +158,19 @@ string = String.new("Some text") > In [Chapter 6](../chapter6/README.md), we saw that we can use Ruby's syntactic sugar to create new strings using `string = "Some text"`. Under-the-hood, this calls `String.new("Some text")`. -What might surprise you is that strings are actually lists of characters. We can use some, but not all, array-like methods on them: +What might surprise you is that strings actually store lists of characters – similarly to how arrays store lists of elements. We can call some – but not all – array methods on strings: ```eval-ruby -string = "Hello World!" +greeting = "Hello World!" -string[0] +greeting[0] ``` -We just used the array reader method `[]` to pull the first character from a string (remember, lists are zero-indexed in Ruby). +We just used the sent the array reader method `[]` to `greeting` to pull the first character from a string (remember, lists are zero-indexed in Ruby). -For more complicated strings, it can be helpful to turn them into arrays. Ruby gives us a method, `split`, to do this with: +> This is possible because `String` instances define a method `[]` on their interface. + +We might want to tell a string to create a new array using its text. Strings define a method called `split` to do this: ```eval-ruby string = "Hello World!" @@ -174,11 +178,9 @@ string = "Hello World!" string.split ``` -By default, `split` will split the string into an array of strings, using a space – `" "` – as the split point. - -> We call this split point a _delimiter_. +Sending `split` to a string will cause the string to invoke some procedure that returns a new array of strings. Strings will use their text to build the new array. By default, they'll use a space – `" "` – as the split point. We call this split point a _delimiter_. -We can provide a different delimeter to `split` to make a different array: +We can provide a different delimeter to `split` to ask the string to build a different array: ```eval-ruby string = "Hello World!" @@ -188,7 +190,7 @@ string.split("l") > Where did the 'l's go in the example above? Think about this: in the first `split` example, where did the `" "` spaces go? -If we provide an empty string (with no characters or spaces in it), the string will be split into an array with each element representing a character of that string: +If we provide an empty string (with no characters or spaces in it), the string will be build an array with each element representing one character of that string's text: ```eval-ruby string = "Hello World!" @@ -196,7 +198,7 @@ string = "Hello World!" string.split("") ``` -We've already seen that we can `join` arrays that are split. This gives us some power to manipulate strings: +We've already seen that we can send the `join` method to arrays. The array will return a concatenation of its elements. Therefore, combining `split` and `join` gives us some power to manipulate strings: ```eval-ruby bad_string = "Why|am|I|so|hard|to|read" @@ -223,7 +225,7 @@ Although it might seem a bit confusing, arrays can therefore contain other array array_of_arrays = [["An", "array", "of", "strings"], ["another", "array", "of", "strings"]] ``` -Because this can get confusing to read, these 'arrays of arrays' are often split onto multiple lines. The following is the same as the above, just easier to read: +Because this can get confusing to read, these 'arrays of arrays' are often written across multiple lines. The following is the same as the above, just easier to read: ```eval-ruby array_of_arrays = [ @@ -234,7 +236,7 @@ array_of_arrays = [ > Play around with `join` and the array reader function `[]` to figure out how this array of arrays works. -Arrays of arrays could represent different groups (say, teams of people): +Arrays of arrays could be used to represent different groups (say, teams of people): ```eval-ruby groups = [ @@ -246,7 +248,7 @@ team_1 = groups[0] team_2 = groups[1] ``` -Or, we can make an array of arrays from different groups: +Or, we can make an array of arrays _from_ different groups: ```eval-ruby team_1 = ["Mary", "Sam"] @@ -255,11 +257,11 @@ team_2 = ["Peter", "Kay"] groups = [team_1, team_2] ``` -> It's perfectly fine to reference variables within arrays. Why? _Referential transparency!_ Ruby just turns the names `team_1` and `team_2` into the arrays they reference. +> It's perfectly fine to reference variables within arrays. Why? _Referential transparency!_ The main object, in which this procedure is running, just turns the names `team_1` and `team_2` into the arrays they reference. ## Combining arrays -You can combine arrays by adding them, just like strings: +You can tell arrays to build new arrays that combine their elements, using `+`: ```eval-ruby array_1 = ["What's", "the", "last", "word", "in", "this"] @@ -268,11 +270,11 @@ array_2 = ["sentence?"] array_1 + array_2 ``` -> Since strings are actually storing lists of characters, this explains why the string method `+` does what it does. +> Notice that `+` doesn't alter `array_1` or `array_2`: it builds a new array that combines their elements. ## Finding out how many elements there are -We can get the length of an array (i.e. how many elements it contains) like this: +We can tell an array to give us its length (i.e. how many elements it contains) by calling the `length` method on it: ```eval-ruby array = [1, 2, 3, 4] @@ -280,13 +282,15 @@ array = [1, 2, 3, 4] array.length ``` -## Using arrays to control the flow +## Using arrays to manage control flow -One main use of arrays is to control the flow of information, by running a procedure once for each item of an array. This process is called **iterating** over an array. +In [Chapter 4](../chapter4/README.md), we met `while`, which can be used to manage control flow by forcing an object to repeat procedures. -There are a few ways to do this. We can use a `while` loop with an accumulator to loop through each item of an array: +Arrays can be used to manage control flow too: by forcing an object to execute a procedure once for each element of an array. This process is called **iterating** over an array. -```ruby +There are a few ways to do this. We can use a `while` loop with an accumulator to run a procedure once for each item of an array: + +```eval-ruby my_array = ["Hello", "there", "friend!"] current_index = 0 @@ -296,7 +300,9 @@ while current_index < my_array.length do end ``` -We could use this structure, combined with the array reader method `[]`, to run a procedure using each element of an array: +> Still confused by `while` loops? Alter the code example above to use `break` instead. + +We can combine this structure with the array reader method `[]` to tell an object to do something with elements of the array one-after-the-other: ```eval-ruby my_array = ["Hello", "there", "friend!"] @@ -308,7 +314,7 @@ while current_index < my_array.length do end ``` -Ruby provides us with a neat way of doing this 'run a procedure using each element of the array' approach: the array `each` method: +`Array` provides us with a neat method to tell an object to 'run a procedure once for each element of the array': the `each` method: ```eval-ruby my_array = ["Hello", "there", "friend!"] @@ -318,7 +324,7 @@ my_array.each do end ``` -What about if we want to reference each item within an array during the procedure? We can do that in the following way: +What about if we want an object to do something with elements of the array one-after-the-other? That is: to reference each item within an array during the procedure? We can do that in the following way: ```eval-ruby my_array = ["Hello", "there", "friend!"] @@ -384,7 +390,7 @@ A common programming problem goes something like this: - Filter this list of numbers to return only numbers less than 10. -To solve this, we can use an **array as an accumulator**. On each pass of a loop, we'll add items to an array if they meet a condition: +To solve this, we can use an **array as an accumulator**. On each pass of a loop, we'll tell the main object to add items to an array _if they meet a condition_ (being less than 10): ```eval-ruby list_of_numbers = [17, 2, -1, 88, 7] @@ -399,11 +405,11 @@ end accumulator ``` -> The `accumulator` was mutated during the `each` loop (it had elements added to it). What happened to the `list_of_numbers`? Play with the REPL above to find out. +> The `accumulator` was mutated during the `each` loop (it had elements added to it). What happened to the array referenced by `list_of_numbers`? Play with the code example above to find out. ## Checking if elements are in arrays -We can use the `includes?` method to find out if an element is in an array: +We can ask an array whether it includes an object by sending it the `includes?` method: ```eval-ruby words = ["Hello", "World!"] diff --git a/chapter8/README.md b/chapter8/README.md index 0b9e5d5..004b0d0 100644 --- a/chapter8/README.md +++ b/chapter8/README.md @@ -1,12 +1,14 @@ # Hashes -We've just met a very powerful way of storing information in the program world: instances of the `Array` class. Arrays are good, but they struggle with a lot of information. For example, what does each element in the following array mean? +We've just met a very powerful object for storing information in the program world: instances of the `Array` class. + +Arrays are great for storing objects, but they get harder to understand the more objects they contain. For example: what does each element in the following array _mean_? ```ruby important_program_information = [0, "Hello", ["Sam", 1.17]] ``` -Moreover, accessing elements in arrays is a bit difficult to understand without intimately knowing how the array was designed: +Moreover, reading elements from arrays using `[]` gets harder to understand as the array grows in complexity: ```ruby teams_with_substitutes = [[["Jim", "Yasmin", "Audrey"], ["Alex", "Mustafa"]], [["Pyotr", "Canace"], ["Xi"]]] @@ -15,7 +17,9 @@ team_1_substitutes = teams_with_substitutes[0][0][1] team_2_players = teams_with_substitutes[1][0][0] ``` -...and it's a pain to read. Is there a solution? Sure there is: the `Hash` class, and its instances: hashes. +...and it's a pain to read. Remember how important _naming_ is for helping other programmers understand your program: what does `[0][0][1]` mean? How is it different from `[1][0][0]`? + +Is there a solution? Sure there is: the `Hash` class, and its instances: hashes. ## From arrays to hashes @@ -28,17 +32,17 @@ Arrays and hashes are similar in that they both contain lists of _elements_. The Remember how variables are _names_ for _objects_? In programming, we sometimes refer to these names as **keys** and the objects they reference as **values**. Together, they make a 'key-value pair'. -Arrays use _indices_ as their keys. That is, the first element of an array has an index of `0`. The second has an index of `1`. Given an array, you can get the value with key `0` in the following way: +Arrays use _indices_ as their keys. That is, the first element of an array has an index of `0`. The second has an index of `1`. Given an array, you can ask it for the value with key `0` in the following way: ```eval-ruby # Given an array array = [1, 2, 3] -# Get the value with key 0 +# Read value with key 0 array[0] ``` -And you can set the value at key `0` in the following way: +And you can tell the array to set the value at key `0` in the following way: ```eval-ruby # Given an array @@ -59,13 +63,13 @@ Hashes can use _any object as a key_: hash = { String.new("first item") => 1, 44.2 => 2, Object.new => 3 } ``` -Most commonly, we'll use strings: +Commonly, we'll use strings: ```eval-ruby favourite_things = { "sport" => "tennis", "food" => "chunky bacon" } ``` -This is because, like arrays, we often want to read the data. String keys provide us with an easy way to read this data: +This is because, like arrays, we often want to read the data. String keys provide us with an easy, clearly-named way to read this data: ```eval-ruby favourite_things = { "sport" => "tennis", "food" => "chunky bacon" } @@ -92,7 +96,7 @@ hash_pretending_to_be_an_array[0] = "football" hash_pretending_to_be_an_array ``` -Even more commonly than strings, we'll use **symbols**. These are a special object in Ruby. They work like strings, except they're _immutable_ – they can't be changed once they've been set. +Even more commonly than strings, we'll use **symbols**. Symbols are a special, and very widespread object in Ruby. They work like strings, except they're _immutable_ – they can't be changed once they've been set. Since we rarely want to change the keys in a hash, symbols are a perfect choice: @@ -102,7 +106,7 @@ favourite_things = { :"sport" => "tennis", :"food" => "chunky bacon" } favourite_things[:"sport"] ``` -> To write a symbol, add a semicolon `:` before a name. There's syntactic sugar, too: you don't need the quotes (`""` around the symbol). Also, you can as a string to fetch its equivalent symbol very easily: simply send the string the message `to_sym` (like how `to_f` worked for integers and floats). +> To write a symbol, add a semicolon `:` before a name. There's syntactic sugar, too: you don't need the quotes (`""` around the symbol). Also, you can ask a string to fetch its equivalent symbol very easily: simply send the string the message `to_sym` (like how `to_f` worked for integers and floats). Using symbols makes your code look super-programmery, and tells other programmers which objects they should expect to change, and which should stay the same. So, this is the first function of hashes: as a **key-value store** for named information. @@ -110,7 +114,7 @@ So, this is the first function of hashes: as a **key-value store** for named inf ## Using hashes to control the flow of information -One major value of a hash is that it can be used to refactor a conditional: especially if that conditional is getting too long. Here's an example program, which berates you if you curse at it: +One major value of a hash is that it can be used to refactor a conditional: especially if that conditional is getting too long. Here's an example procedure. The object running this procedure berates you if you curse at it: ```eval-ruby curse = "dang" diff --git a/chapter9/README.md b/chapter9/README.md index 1a157a7..4c91446 100644 --- a/chapter9/README.md +++ b/chapter9/README.md @@ -1,11 +1,36 @@ # Writing your own methods -We've written a lot of procedures, now. They've used conditional logic, loops, iterators, and a number of kinds of specialised object. +Most Ruby procedures are wrapped up in methods, which are defined 'inside' objects. When you call a method on an object: -But sometimes, we might want to reuse these procedures. Let's say that we have the following program specification: +```eval-ruby +1.positive? +``` + +That object executes a procedure inside itself: + +```eval-ruby +# Inside 1 +if self > 0 + return true +else + return false +end +``` + +And then returns the outcome of the procedure (here, `true`). + +We've written a lot of procedures, now. They've used conditional logic, loops, iterators, and a number of kinds of specialised object. Every time we've written a procedure, we've defined it on the main object, which has carried out the procedure for us. + +In this chapter, we'll learn how to give these procedures a name, by wrapping them inside methods. In [Chapter 10](../chapter10/README.md), we'll learn how to define these methods on objects other than the main object. + +## Methods are reusable procedures + +Let's say that we have the following program specification: > We're a school. Our students have just finished taking their final test. We have test scores for each class of students, and we want to know the average for each class. We also want to know the average for the whole school. +Here's some (horrible!) code to solve this problem. As you can see – it's really repetitive. + ```eval-ruby # Here are the scores test_scores_for_class_1 = [55, 78, 67, 92] @@ -73,12 +98,13 @@ Of course is there is. In the example above, we can use algorithmic thinking to 1. Accumulate the scores. 2. Divide the accumulation by the number of scores. +3. Return the result. We can write a procedure that will carry out these rules. And, using a method, we could give that procedure a name. That's what a method is: a **named procedure**. > Variables are named objects. Methods are named procedures. -Here's how we define a method: +Here's how we define a method, using `def`: ```eval-ruby def method_name @@ -86,7 +112,7 @@ def method_name end ``` -Just like variables, we need to define a name for the method – so we can call it later: +Just like variables, we need to define a name for the method. We can use this name to call the method later: ```eval-ruby def hello_world @@ -96,18 +122,20 @@ end hello_world ``` -So, we're going to define a method called `average`: +To reduce the repetition of our code above, we're going to define a method called `average`: ```eval-ruby def average end ``` -> Right now, we're defining the method on the program object. That's why we can call our method without referencing an object. More on this in [Chapter 10](../chapter10/README.md) – if you're confused for now, go remind yourself about [Chapter 3](../chapter3/README.md)! +...and we're going to put the procedure for finding an average inside it. + +> Right now, we're defining the method on the program object. That's why we can call our method without referencing an object using dot syntax. In [Chapter 10](../chapter10/README.md), we'll define methods on other objects. If you're confused, go remind yourself about messages and objects using [Chapter 3](../chapter3/README.md). ## Return values from methods -Remember that when we send an object a message, the object invokes a method using that message. Then, it carries out a procedure. Finally, it returns something back to whoever sent the original message. +Remember: when we send an object a message, the object invokes a method using that message. Then, the object carries out a procedure. Finally, the object returns something back to whoever sent the original message. To designate what the return value should be from a method, use the `return` keyword: @@ -120,9 +148,9 @@ end gimme_five ``` -Remember that procedures are read by the computer _instruction-by-instruction_. When the computer hits a `return` statement, it'll stop executing the procedure inside the method, and just return whatever it sees. +Remember from [Chapter 4](../chapter4/README.md) that procedures are executed by the object _instruction-by-instruction_. When an object hits a `return` statement, it'll stop executing the procedure inside the method, and just return whatever it's told to. In the example above, the object on which `gimme_five` is defined – the main object – proceeds through the procedure, stopping when it sees `return 5`, and returning `5` as the return value. -That means that any instructions after a `return` instruction won't be executed: +This 'stop when you see return' gimmick means that any instructions after a `return` instruction won't be executed: ```eval-ruby def stop_halfway @@ -134,9 +162,68 @@ end > Play around with the example above. How can we return `sum` instead? +#### Implicit returns + +In Ruby, any methods that don't contain a `return` statement will secretly add one on the last line of the procedure. So the following two examples are identical: + +```eval-ruby +def hello + return "Hello World!" +end + +hello +``` + +```eval-ruby +def hello + "Hello World!" +end +``` + +This kind of secret returning is called using an **implicit return** (as opposed to actually writing one out, which is an **explicit return**). + +#### Empty method procedures + +Remember that `nil` is a Ruby object used to represent the 'absence of anything'. When we define an empty method – one with no procedure inside – Ruby will secretly add a line containing `nil` to it. So, again, the following two examples are identical: + +```eval-ruby +def do_nothing +end + +do_nothing +``` + +```eval-ruby +def do_nothing + nil +end + +do_nothing +``` + +> Because of implicit returns (see the section just before this one), `do_nothing` is actually executing the following procedure: `return nil`. + ## Method parameters -Methods can take parameters, which are objects used in the procedure: just like the `each` loop we saw in Chapters [7](../chapter7/README.md) and [8](../chapter8/README.md): +When we call a method on an object, we often provide it with arguments to use during the procedure: + +```eval-ruby +cars_i_like = ["Citroen 2CV", "Tesla Roadster"] + +# push takes one argument +cars_i_like.push("Audi TT") +``` + +Defined on the array `cars_i_like` is a procedure that does something like this: + +```ruby +# inside cars_i_like +self.elements = self.elements + "Audi TT" +``` + +In the case above, the object `"Audi TT"` is provided as an _argument_ to the method `push`. Then, in the procedure, the `"Audi TT"` object is used somehow. + +Method parameters are temporary names for objects provided as arguments. They're set up at the beginning of the procedure, then they go away at the end: just like the `each` loop we saw in Chapters [7](../chapter7/README.md) and [8](../chapter8/README.md): ```eval-ruby def hello(person) @@ -146,7 +233,7 @@ end hello("Sam") ``` -Method parameters define a variable inside the method procedure. The name of the variable is set to the parameter name, and the value is set to whatever argument you pass to the method when you execute it: +Method parameters _define a variable inside the method procedure_. The name of the variable is set to the parameter name, and the value is set to whatever argument you pass to the method when you execute it: ```eval-ruby def hello(person) @@ -168,6 +255,20 @@ end hello("Sam") ``` +In this way, the object currently executing a procedure can use other objects to vary the return value: + +```eval-ruby +def make_cake(flour_exists?) + if flour_exists? + return :cake + else + return 0 + end +end + +make_cake(true) +``` + ## Writing a method procedure Inside the `def`...`end` statement, we write the procedure we want to run: @@ -181,7 +282,7 @@ def average end ``` -We want our `average` method to work with an array of numbers, so we'll give it one parameter: +We want the procedure inside `average` to work with an array of numbers, so we'll give it one parameter representing that array of numbers, `scores`. That way, when we call `average` we can provide an argument. That argument will be accessible (under the temporary name `scores`) to the procedure inside `average`: ```eval-ruby def average(scores) @@ -190,7 +291,7 @@ end > Remember – it doesn't matter what we call the parameter. The method will assign a variable inside itself, called `scores`, equal to whatever we pass into it. I've called the parameter `scores` because that's what we're going to pass to average. But `numbers` would work, as would `test_scores`, or `digits`, or `chickens`. So long as we refer to the parameter name we defined in the method procedure, we're good-to-go. -From the averaging code we wrote earlier, we know the first thing we do is accumulate the scores together: +From the averaging procedure we wrote earlier, we know the first thing we do is accumulate the scores together: ```eval-ruby def average(scores) @@ -216,6 +317,23 @@ def average(scores) end ``` +Then, we return the result: + +```eval-ruby +def average(scores) + scores_accumulator = 0 + + scores.each do |score| + scores_accumulator += score + end + + result = scores_accumulator.to_f / scores.length + return result +end +``` + +Actually, we can use Ruby's implicit return policy to leave that last step out of the procedure: as the result will be implicitly returned. + Tada! We now have an `average` method we can call whenever we want: ```eval-ruby @@ -278,7 +396,7 @@ puts "School average: " + school_average.to_s school_average ``` -OK, this is much shorter. But we can do better, by eliminating some of these variables using referential transparency: +OK, this is much easier to read. But we can do better, by eliminating some of these variables using referential transparency: ```eval-ruby # Define the average method @@ -311,7 +429,7 @@ average([average(test_scores_for_class_1), average(test_scores_for_class_2), ave ## Over-simplifying -We can go deeper. If we move the printing of averages into our `average` method, we can get rid of a few more cluttered lines: +We could go deeper. If we move the printing of averages into our `average` method, we can get rid of a few more cluttered lines: ```eval-ruby # Define the average method @@ -340,25 +458,28 @@ average(test_scores_for_class_3) average([average(test_scores_for_class_1), average(test_scores_for_class_2), average(test_scores_for_class_3)]) ``` -OK, now we're printing way too much. Every time we call `average`, it prints something (even if we just want the average value). **This is a major lesson of refactoring.** It's pretty tempting to try and tidy up lines of code into methods, but we run into problems when we try to make methods that do too much. Right now our `average` method should really be called `average_and_print`. +But is this really better? + +- For one, we're printing way too much. Every time we call `average`, it prints something (even if we just want the average value). +- For two, that last line is incredibly hard to read. + +**This is a major lesson of refactoring.** It's generally good to tidy up lines of code into methods, but we run into problems when we try to make methods that do too much. Right now our `average` method should really be called `average_and_print`. > Methods should **do one thing, and do it well**. The word `and` in a method name is a major clue that something's been refactored poorly. This principle is called the _Single Responsibility Principle_, and you'll meet it a lot in the next few months. -This entire process – of taking lots of lines of code, each of which does some small task, and grouping it into a method, is called **abstraction**. What you are doing is taking a bunch of actions and giving that whole bunch a name. As a result, it becomes easier to work with – working with the `average` method is definitely easier than writing all those lines of code by hand – but you also lose understanding of what's happening inside it. +Taking a procedure (lots of lines of code) and grouping it into a named method is an example of **abstraction**. Abstraction results in code that is easier to work with – using the `average` method is definitely easier than writing all those lines of code each time – but we lose understanding of what's happening inside the code. > I said in [Chapter 2](../chapter2/README.md) that naming is one of the hardest problems in programming. The other one is picking the 'right abstraction' – the right way to group and simplify your code. That's what we'll be spending a lot of time on during Makers Academy. ## Picking the right abstraction -Choosing the right abstraction is incredibly hard to do. And, it's often a while before you know if you made a good choice or not. +Picking the right abstraction is really hard. And, you often need to use your abstraction for a while before you know if you made a good choice or not. This means that, as programmers, we get used to writing and deleting code. + +> Code is cheap! It costs literally nothing. Get used to deleting it, and rewriting it. Practicing rewriting code from scratch will be painful for a while yet, but you'll get much faster this way. It's easy to get precious over code you've written – but the confidence of knowing you're able to reproduce it from scratch is worth having. -That said, here are some helpful rules of thumb to picking the right abstraction when writing methods: +Here are some helpful rules of thumb to picking the right abstraction when writing methods: 1. Can you name your method in a simple way, without using the word 'and'? Does it do one thing, and nothing more? 2. Can you name your method after what it returns, instead of what it does? For instance, `average(test_scores)` is a better name than `averages_scores(test_scores)`. For another example, `score(hand)` is a better method name than `scores_cards(hand)`. -If you can answer 'yes' to both 1 and 2, your method is more likely to be a good one. - - -- `nil` in method bodies -- `return \ No newline at end of file +If you can answer 'yes' to both 1 and 2, your method is more likely to be a good one. \ No newline at end of file