RB101: Notes
Lesson 1: Preparations #
Launch school forums use Markdown, specifically Github flavored markdown -check the ‘Formatting Help’ link, when making a post to check -surround code with backticks (preview it of course)
Be sure to always ask good questions
https://launchschool.com/lessons/c82cd406/assignments/abae2b83
Ideas for study sessions #
- Topic Presentation. Pick a particular topic or programming concept, and briefly present on that topic to the rest of the group using a combination of explanations and/or code examples. Have the rest of the group ask questions and give feedback.
- Code Example Questions. Have each student in the study group prepare a few code examples which illustrate or explore a particular concept. Take turns to ask the rest of the group about your examples, e.g. ‘what will happen when this code is run?’, ‘what will line 5 output?’, etc.
- Pair Problem Solving. With a study partner, pick a coding problem that neither of you have solved before and solve it together. Discuss the problem and approach to solving it, and iterate on an approach.
- Live Coding Practice. With a study partner, both of you choose a problem that the other has not seen before and solve it ahead of time. Then, on a call, your partner can give you a problem (that is new to you) to solve, and you can then give them a problem (that is new to them) to solve. Practice explaining your thought process, why you’re choosing certain methods, etc.
- Flash-card Quiz. Prepare a flash card deck on a particular topic or concept. Use these cards in the session to quiz your study partner or study group.
- use vscode live coding, https://app.coderpad.io/sandbox, or repl.it
Exercises: Small Problems #
We recommend that you do around 20 exercises (or 2 sets) after each lesson (no need to complete the Advanced exercises set).
The two layer problem #
-
learning to solve problems while simultaneously memorizing the syntax of a particular language - is hard!
-
we need to use PEDAC
- *P Understand the Problem
- identify expected input and output
- make the requirements explicit
- identify rules
- mental model of the program (optional) *mental model is summary of entire problem, What it requires not How to solve it
- *E Write Examples/Test Cases
- validate understanding of the problem
- *D Data Structure
- how we represent data that we will work with when converting the input to output
- *A Algorithm
- steps for converting input to output
- *C Code (with intent)
- implementation of algorithm
- *P Understand the Problem
-
the goal is to try and separate the logic concern from the syntax concern
Common pitfalls of hack and slash or coding without intent: #
- Missed requirements
- Unforeseen Edge Cases
- Hard to understand code
- Code that’s difficult to maintain
Lesson 2: Small Programs #
For this lesson, always use parenthesis for method calls, although they are normally optional
Like everything else in Ruby, boolean objects also have real classes behind them, and you can call methods on true and false.
true.class # => TrueClass
true.nil? # => false
true.to_s # => "true"
true.methods # => list of methods you can call on the true object
Ruby is a very liberal language and considers everything to be truthy other than false and nil.
This means that even the integer 0 is considered truthy, which is not the case in some other languages.
Remember that && short circuits if it encounters a false, and nil is considered “falsy”.
Remember this rule: everything in Ruby is considered “truthy” except for false and nil.
Pseudo-code is meant for humans to read.
We don’t use programming code first, because we’re trying to load the problem into our brain first.
*keyword *meaning
START start of the program
SET sets a variable we can use for later
GET retrieve input from user
PRINT displays output to user
READ retrieve value from variable
IF / ELSE IF / ELSE show conditional branches in logic
WHILE show looping logic
END end of the program
SUBPROCESS label to show that there is some other thing that will take care of this process
Good to code in casual format, then in formal pseudo-code using keywords
Using a flowchart helps us map out the logical sequence of a possible solution in a visual way. #
- oval - start/stop
- rectangle - processing step
- rhombus - input/output
- diamond - decision
- circle - connector
Arrows show the “flow” of logic, from shape to shape
Mapping out the step by step logic of our program is called the imperative or procedural way of solving a problem
using a method in a higher level programming language like ruby, which encapsulates a basic concept into that method, like each, is a declarative way of solving a problem
be imperative in flowcharts (no declarative constructs)
helps you to “think like a computer”
try and keep your train of thought at the logical level and not think about code.
As you use pseudo-code and flowcharts to help you dissect the logic of a problem, you’ll be constantly trying to figure out how detailed the chart and words should be, and what can be extracted to sub-processes. This is exactly what a programmer should be thinking about when designing the solution to a problem. You won’t get it right the first time.
Start at a high level, using declarative syntax. For example, if you’re working on a calculator, you can start with something like this:
- Get the first number
- Make sure it’s valid, otherwise, ask for another
- Get the second number
- Make sure it’s valid, otherwise, ask for another
- Get the operator
- Make sure it’s valid, otherwise, ask again
- Perform operation on the two numbers
- Display result
- Ask if user wants to do another calculation
What makes a good programmer? #
if the key to programming is debugging, then the key to debugging is having a patient and logical temperament
steps to debugging(identify, understand, fix) #
- reproduce the error
- determine the boundaries of the error
- trace the code -need to ‘trap the error’ (isolating it’s location)
- understand the problem well
- implement a fix -fix one problem at a time -care not to just use a trailing rescue to cover up an error if it can be avoided, this is usually a code smell that you hadn’t though carefully about the possible problems that could go wrong, and therefore you haven’t through about how to handle the potential error conditions
- test the fix
techniques for debugging #
- line by line -be patient
- rubber duck
- walking away -make sure you first spend time loading the problem in your brain
- using pry
- using a debugger
In summary, debugging is arguably the most important skill you need to learn as a programmer. Focus on developing a patient, systematic temperament; carefully read error messages; use all the wonderful resources at your disposal; approach debugging in sequential steps; and use the techniques we covered above – especially Pry. If you haven’t yet, go install Pry now and play around with it a little bit.
The meaning of an expression in Ruby is determined by what is called operator precedence. #
-Don’t rely too much on precedence. It’s easy to forget the precedence order and get confused by an unexpected result. If you’re using 2 or more different operators in an expression, use parentheses to explicitly define the meaning.
An operator that has higher precedence than another is said to bind more tightly to its operands.
a {} has slightly higher priority than a do…end block, which could effect how a block gets called
ruby’s tap method -object instance method tap, useful debugging tool
array = [1, 2, 3] mapped_array = array.map { |num| num + 1 }
mapped_array.tap { |value| p value } # => [2, 3, 4]
use parentheses, don’t rely on the precedence rules
Loan Calculator #
- make nice header with dashes
- uses prompt function is good
- can test only if numbers are empty or positive float
- why isn’t more validation necessary?
- use main loop to ask if another calculation is desired
- show desired format in prompts (5 for 5%, (in years))
- save more specific variables so no transformations necessary in larger equation
- use Kernel#format to format currency output to user
Coding Tips #
- debug for hours, commit to the time and repitition, get burned and learn :)
Naming Things #
- choose descriptive variable and method names
- variables are names not for how they are set, but for what they actually store
Naming Conventions #
snake_case
for everything except classes which areCamelCase
, and constants which areUPPERCASE
Mutating Constants #
- don’t change the value of constants
Methods #
- make sure that methods do one thing, with very limited responsibility
- keep them short, maybe 10 lines, 15 max
- if too long, split it into 2 or 3 methods
How to write good methods #
- don’t display something to the output
- return a meaningful value
- since ruby always returns a value, the key here is that the return value shouldn’t be the intent of the method
- decide whether the method should return a value with no side effects, or perform side effects with no return value
- the method name should reflect whether it has side effects or not (for example, some ruby methods end with
!
if they have side effects)
- the method name should reflect whether it has side effects or not (for example, some ruby methods end with
- in ruby, we would not call a method
return_total
, it would just betotal
, returning a value is implied
Methods should be at the same level of abstraction #
- you should be able to mentally extract the method from the larger program, and just work with the method in isolation
- when you have a method like this, you can use it without thinking about its implementation
Method names should reflect mutation #
update_total
implies that the parameter passed in to it will be mutated, we would not expect to see something liketotal = update_total(total, cards)
- goal should be to build small methods that are like LEGO blocks, stand-alone pieces of functionality that you can use to piece together larger structures.
- some methods will be convoluted, but as our understanding grows, we should be able to refactor it later
- it’s alright to start with a less organized, exploratory first draft
Displaying output #
- good practice to preface method names that will return a string or output strings with something like
print_
,say_
, ordisplay_
Miscellaneous Tips #
- Don’t prematurely exit the program, programs should only have one exit point
- 2 spaces, not tabs
- name your methods from the perspective of using them later! think about how you would like to invoke them
- know when to use a “do/while” vs a “while” loop
loop do
puts "Continue? (y/n)"
answer = gets.chomp
break if answer.downcase == 'n'
end
- this is prefered to a while loop, as a while looop would require
answer
to be initalized outside the loop, and so it’s slightly easier to reason with - always clarity over terseness
Truthiness #
- in Ruby, more than just
true
evaluates to true in a conditional
In Ruby, everything is truthy except
nil
andfalse
- avoid “assignment within a conditional code”
Approach to Learning #
- learning takes focus and attention, and repition over a long period
- don’t be demoralized if you do something once and you can’t remember most of it, that’s normal
Variable Scope #
Variables and Blocks #
- a block is
do...end
or{..}
following a method invocationdo..end
for multi-line blocks and{..}
for single line blocks
- blocks create a new scope for local variables (inner scope)
A method definition has no notion of “outer” or “inner” scope – you must explicitly pass in any parameters to a method definition.
- outer scope variables can be accessed by inner scope
- also, you can change variables from an inner scope and have that change affect the outer scope
- have to be careful when instantiating variables in an inner scope, to not accidentally re-assign an existing variable in an outer scope, big reason to avoid single-letter variable names
- inner scope variables cannot be accessed in outer scope
- remember that where a variable is initialized determines its scope
- peer scopes do not conflict
- we could use the same variable name
a
in two different blocks of code, however it will be two different variables
- nested blocks
- nested blocks follow the same rules of inner and outer scoped variables
- only difference is vocabulary, switching from “outer” or “inner” to “first level”, “second level”, etc..
- variable shadowing
- variable shadowing prevents access to an outer scope local variable, when an block is block parameter shares a name with a variable in outer scope
- choose long and descriptive names instead
Variables and Method Definitions #
A method definition has no notion of “outer” or “inner” scope – you must explicitly pass in any parameters to a method definition.
- a method definition can’t access local variables in another scope
- a method definition can access objects passed in
If a local variable and a method were to share the same name, Ruby will first search for the local variable if there is one, if one is not found then Ruby will try to find a method with that name. If neither is found then a
NameError
is thrown.
- to remove some ambiguity, we can indicate a method specifically by including a set of empty argument parentheses with the method invocation, i.e. replacing
puts hello
withputs hello()
, although there is no analogue for explicitly calling a local variable
Blocks within Method Definitions #
- the rules of scope for a method invocation with a block remain in full effect even if we’re working inside a method definition
Constants #
- the scoping rules for constants is not the same as local variables. In procedural style programming, constants behave like globals
- they can be accessed by any inner scope, unlike local variabels, and are said to have lexical scope
More Variable Scope #
-
two key terms are method definition and method invocation
-
method definition is when, within our code, we define a ruby method using the
def
keyword -
method invocation is when we call a method, whether that happens to be an existing method from the Ruby Core API or core Library, or a custom method that we’ve defined ourselves using the def keyword.
A block is defined as method invocation followed by curly braces or
do..end
- An important take away for now is that blocks and methods can interact with each other, the level of that interaction is set by the method definition and then used at method invocation
- Given this additional context:
- we can think of method definition as setting a certain scope for any local variables in terms of the parameters that the method definition has, what it does with those parameters, and also how it interacts (if at all) with a block
- We can then think of method invocation as using the scope set by the method definition
Pass by Reference vs Pass by Value #
Ruby is in a way both pass by value and pass by reference
- “pass by value” traditionally means that when you pass a method an object, you are only giving it a copy of the original object. This is how methods always work in C for example, and is shown in ruby with re-assignment within a method. variable re-assignment within a method doesn’t affect the object outside of the method, so it is an example of “pass by value”
- “pass by reference” is used to describe when operations within a method are aboe to affect the original object. it is not only the appropriate value passed into the method, but a reference to the true location of it’s value. this is shown in Ruby by the use of a destructive method within a method, and how it is able to mutate the caller from within a method, even though it is outside the scope of a method
- some refer to Ruby’s combination of these two behaviors as “pass by value of the reference” or “call by sharing”
Most importantly, remember, when an operation within the method mutates the caller, it will affect the original object
!
is a naming convention to end methods in Ruby which are destructive, but this is just a convention, not a rule (for example,Array#<<
is destructive, but doesn’t end with!
)- In Ruby, numbers and boolean objects are immutable, as well as a few other types
- Most objects are mutable!
- the ability to mutate depends in part on the mutability or immutability of the object represented by the argument, but also one how the argument is passed to the method
- immutable objects act like Ruby passes them around by value
- mutable objects act like Ruby passes them around by reference
- all methods are non-mutatiing with respect to immutable ojects, and assignment is non-mutating
- the ability to mutate depends in part on the mutability or immutability of the object represented by the argument, but also one how the argument is passed to the method
several common methods that sometimes cause confusion, as they mutate the caller but don’t end with
!
:#[]=
,#<<
, setter methods,String#concat
<<
is a method defined for some classes like collections and strings, careful though, it might also represent non-mutating methods for other classes- Setters are mutating, careful, they superficially look like assignments
- with indexed assignmment, the elements of a collection or string are replaced. with setters, the state of the object is altered, usually by mutating or reassigning an instance variable
person.name = 'Bill' person.age = 23
this looks like assignment, but are mutating setter calls, they mutate the object bound to
person
- assignment in Ruby acts like a non-mutating method, it doesn’t mutate any objects, but does alter the binding for the target variable
- howver, the syntantically similar indexed assignment and object setter operations are mutating
- almost everything in Ruby is an object: literals, named objects (variables and constants), complex expressions
- methods can include methods, blocks, procs, lambdas, and even operators
- arguments can include actual arguments, the caller of the method, operator operands, or a return value
- every computer language uses some sort of evaluation strategy when passing objects, and the most common strategies are known as strict evaluation strategies. Ruby uses strict evalaluation exclusively, and this means that every expression is evaluated and converted to an object before it is passed along to a method.
- two most common strict evaluation strategies are pass by value and pass by reference, referred to as object passing strategies
- pass by reference isn’t limited to mutating methods. A non-mutating method can use pass by reference as well, so pass by reference can be used with immutable objects. There may be a reference passed, but the reference isn’t a guarantee that the object can be mutated.
- Given all of this, it’s not uncommon to just say that ruby is pass by reference value or pass by value of the reference
- ruby passes around copies of the references! its a blend of the two evaluation strategies
Ulimately, the answer is that Ruby uses pass by reference value
- pass by reference would be accurate if you don’t account for assignment and immutability
- Ruby acts like pass by value for immutable objects, and appears to act like pass by reference for mutable objects, but it’s really pass by reference value
Coding Tips 2 #
Using New lines to organize code #
- organize code into chunks to make it easier to read, using new lines
Making your code readable is of paramount importance, not only for others, but for future self
Should a method return or display? #
Understand if a method returns a value, or has side effects, or both
- side effects could be either displaying something to the output, or mutating an object
- avoid writing methods that do both!
Name methods appropriately #
- preface names of methods that output value with
display_
orprint_
If you find yourself constantly looking at a method’s implementation every time you use it, it’s a sign that the method needs to be improved
- a method should do one thing, and be named appropriately
Don’t mutate the caller during iteration #
Don’t mutate a collection while iterating through it, or else you’ll get unexpected behavior (although of course you can mutating elements of the collection while iterating through it)
Variable Shadowing #
Don’t do it, be careful about choosing appropriate block variables, make it unique, rubocop will catch this :)
Don’t use assignment in a conditional #
- Never use assignment in a conditional, as it isn’t clear whether you meant to use
==
or if you indeed meantt o do assignment. - if you must, like maybe with a loop such as
while num = numbers.shift
, wrap the assignment in parentheses, to signify to future programmers (and yourself) that you know what you’re doing and it’s done on purpose:while (num = numbers.shift)
- just don’t do it though
Use underscore for unused parameters #
- Suppose you have an array of names, and you want to print out a string for every name in the array, but you don’t care about the actual names. In those situations, use an underscore to signify that we don’t care about this particular parameter.
names = ['kim', 'joe', 'sam']
names.each { |_| puts "got a name!" }
- Or, if you have an unused parameter when there are multiple parameters:
names.each_with_index do|_, idx|
puts "#{idx+1}. got a name!"
end
Gain experience through struggling #
- Don’t memorize “best practices”, but spend enough time programming to the point where you understand the context for those practices.
- Don’t be fearful of violating rules or afraid to make mistakes, but keep an eye out for improvements
- spend the time!!, struggle, search, play around
Quiz notes #
- Q - 10: from the discussion forums,
for
andwhile
loops are expressions, not method calls, so don’t form blocks. it looks likeloop..do
is a method call though and forms a block? - Q - 16: I’m stupid, Using a method that mutates the caller does not change the memory address that the variable is pointing to, this remains the same
SPOT session RB109 assessment notes #
- make a template of explanations of ‘all’ concepts i would need to talk about in written assessment, can copy/paste specific values into on the fly
example of template entry below!
#### `Array#map`
On line 9 the local variable `array` is initialized to the *Array* object ` [1, 2, 3, 4, 5]`. On line 11 the `map` method is invoked on the object referenced by the local variable `array`.The block is defined by the `{}` curly braces along side the `map` method invocation. `Array#map` method iterates over the array object it is called on and executes the `block` for each element in the array. The `map` method returns a new array with elements transformed based on the return value of the block.
For every iteration the `map` method passes the current element as an argument to the block which is assigned to the block parameter `num`.
The `map` method iterates through the collections and returns a new collection transformed based on the return value of the block.
-
will defintely be using coderpad for live assessment, get used to it
-
loop is a method invocation, and it takes a block
- define a block
-
reference statements you’ve already made “as explained before”
-
start with the call stack
-
github markdown, use single backticks for variables and things
-
practice talking through code, line by line
- focus on each concept, line by line
-
3 concepts to be super strong on
- iteration
- each (returns the original collection)
- selection
- select (looks for ‘truthyness’ of the return value of the block, things that evaluate to true)(returns a new array)
- transformation
- map (returns a new array)
- ‘map method is called on the object referenced by the local variable array’
- also
- dupe vs clone
- truthyness
- hash vs array
- mutating vs non mutating methods
- iteration
-
make sure algorithm matches the code! update it as you go, as necessary
-
scan is the opposite of split!
-
run the code locally before talking about it, just to be sure
-
enumerator is like a box with things inside, you have to open it
-
study with as many people as you can, get their pedac!!
Lesson 4 #
Collections Basics #
Element Reference #
- strings use an integer-based index that represents each character in the
string, starting at 0:
- you can reference a specific character using this index
str = 'abcdefghi'
str[2] # => "c"
- you can also reference multiple characters within a string, by using an index starting point, and the number of characters to return
str[2, 3] # => "cde"
- this is a syntactical sugar call to slice i.e.
str.slice(2, 3)
- because methods always have a return value, we can use method chaining to call another method on the return value
str[2, 3][0] # => "c"
# this is effectively the same as 'cde'[0]
str = 'The grass is green'
str[4, 5] # => "grass"
str.slice(4, 5) # => "grass"
Array Element Reference #
- Like strings, arrays are also ordered, zero-indexed collections
- arrays are lists of elements that are ordered by index, where each
element can be any object
- specific elements can be referenced using their indices, just like strings
- just like strings, can use
arr[x, y]
as alternative syntax for theArray#slice
method
Important to remember, that
Array#slice
andString#slice
are not the same method!
String#slice
returns a new string,Array#slice
returns a new array!!!- Be careful with the different returns from the
Array#slice
method:
arr = [1, 'two', :three, '4']
arr.slice(3, 1) # => ["4"]
arr.slice(3..3) # => ["4"]
arr.slice(3) # => "4"
Hash Element Reference #
- hashes use key-value pairs, instead of an integer-based index
hsh = { 'fruit' => 'apple', 'vegetable' => 'carrot' }
hsh['fruit'] # => "apple"
hsh['fruit'][0] # => "a"
- when initializing a hash, the keys must be unique
- values however can be duplicated
- hash keys and values can be any object in Ruby, but it is common practice to use symbols as the keys. symbols can be thought of as immutable strings
Element Reference Gotchas #
Out of Bounds Indices #
- referencing an out-of-bounds index of a string returns
nil
:- no big deal,
nil
is obviously an invalid return value for a string
- no big deal,
- referencing an out-of-bounds index of an array also returns
nil
:- have to be careful, as arrays can contain any type of object, including
nil
Array#fetch
throws anIndexError
exception if the index is out of bounds. This is very helpful, as a regular index could be misleading if out of bounds. If you want to be safe, use#fetch
- have to be careful, as arrays can contain any type of object, including
Negative Indices #
- Negative indices references elements in
String
andArray
objects starting from the last index in the collection-1
and working backwards
Invalid Hash Keys #
Hash
also has a#fetch
method, which can also be useful when trying to diambiguate valid hash keys with anil
value from invalid hash keys
Conversion #
- Because strings and arrays share similarities, there are ways to convert them
from one to the other, such as
String#chars
andArray#join
:String#chars
returns an array of individual charactersArray#join
returns a string wtiht the elements of the array joined together- hash has a
#to_a
method which returns an array (returns nested array of key-value pairs) - array similarly has a
#to_h
method (returns a hash, from a nested array)
Element Assignment #
String Element Assignment #
- we can use the element assignment notation of
String
to change the value of a specific character within a string by referring to its index
str = "joe's favorite color is blue"
str[0] = 'J'
str # => "Joe's favorite color is blue"
Array Element Assignment #
- similar of course to string element assignment
arr = [1, 2, 3, 4, 5]
arr[0] += 1
arr[3] = 9
arr # => [2, 2, 3, 9, 5]
Hash Element Assignment #
- similar again, but you use the hash key instead of an index when assigning a value
hsh = { apple: 'Produce', carrot: 'Produce', pear: 'Produce', broccoli: 'Produce' }
hsh[:apple] = 'Fruit'
hsh # => { :apple => "Fruit", :carrot => "Produce", :pear => "Produce", :broccoli => "Produce" }
Looping #
Break placement #
- if
break
is placed on the last line within aloop
call, this mimics the behavior of a “do/while” loop. the code within the block is guarenteed to execute at least once - if
break
is placed on the first line within a loop call, this mimics the behavior of awhile
loop. the code below break may or may not execute at all, depending on the condition
Looping over a hash #
- two step process:
- create an array of keys using
Hash#keys
, then iterate over that array(using a simple counter variable), saving each key into a new variable - use that new variable to retrieve the appropriate value out of the hash
- create an array of keys using
Summary #
- Looping comprises four basic elements:
- a loop
- a counter
- a way to retrieve the current value
- a way to exit the loop
Introduction to PEDAC process #
- following the PEDAC process saves time and lets you solve complex problems efficiently
P - [Understand the] Problem #
-
three steps:
- read the problem description
- check the test cases, if any
- if any part of the problem is unclear, ask the interviewer or problem requester to clarify the matter
-
clarifying questions:
- problem domain, do i understand?
- test case issues, like zero or empty inputs
- any assumptions I can safely make?
- are there any assumptions I’m making?:
- do i need to return the same object or a new one???
- always verify assumptions either by looking at the test cases or by asking the interviewer
Data Structure / Algorithm #
- formal pseudocode is not always necessary, but can sometimes be helpful
- you don’t need to write all your pseudocode before you start coding:
- fine to include methods in pseudocode that you will write pseudocode to help implement after the fact
- you should be able to write a plain English solution to the problem
- if you can’t do that, you won’t be able to code it either, you don’t need “fancy” methods to solve these problems
- avoid implementation detail! you risk getting locked into a particular
approach or way of thinking about the problem
- also don’t worry about the efficiency of the algorithm
Testing Frequently #
- test your code early and often while writing it
- don’t wait!!
Big Picture Thoughts #
- Not a completely linear process
- Move back and forward between the steps
- Switch from implementation mode to abstract problem solving mode when necessary
- Don’t try to problem solve at the code level
Selection and Transformation #
- Selection is picking certain elemenents out of the collection depending on some criterion
- Transformation is manipulating every element in the collection
Looping to Select and Transform #
When performing transformation, it’s always important to pay attention to whether the original collection was mutated or if a new collection was returned.
More Flexible Methods #
By defining our methods in such a way that we can pass in additional arguments to alter the logic of the iteration, we can create more flexible and generic methods.
Summary #
- Using the three actions iteration, selection, or tranformation, we can manipulate a collection nearly any way we need to
- pay attention to when the original collection is mutated vs when the method returns a new collection
- understand how these methods can be made more generic by allowing for additional parameters to specify some criteria for selection or transformation
Methods (each
, select
, and map
) #
each
is functionally equivalent to using loop, and represents a simpler way of accompishing the same taskeach
returns the original collection! (loop returns nil, or anything you send tobreak
instead)
select
is a built-in way for arrays and hashes to iterate over a collection and perform selectionselect
evaluates the return value of the block, and only cares about its truthinessselect
returns a new collection containing all of the selected elements- when using
select
, always be aware of the return value of the block!!- if the block evaluates to a “falsey” value, such as a
puts
call returning nil, theselect
call would return an empty array
- if the block evaluates to a “falsey” value, such as a
map
also considers the return value of the block, like select, but instead *uses the return value of the block to perfrom transformation instead of selectionmap
returns a new collection! (a map!)- it always performs transformation based on the return value of the block
rememeber, certain collection types have access to specific methods for a reason (
select
andmap
are defined in the Enumerable module and are available to theArray
andHash
classes, cannot callselect
ormap
on a string, but could always callString#split
first to make that possible
Method | Action | Considers the return value of the block? | Returns a new collection from the method? | Length of the returned collection |
---|---|---|---|---|
each |
Iteration | No | No, it returns the original | Length of original |
select |
Selection | Yes, its truthiness | Yes | Length of original or less |
map |
Transformation | Yes | Yes | Length of original |
More Methods #
- With many methods, such as
Enerable#any?
, you need to be aware of 2 return values, the return value of the method, and the return value of the blockany?
looks at the truthiness of the block’s return value in order to determine what the method’s return value will be- can be used with a hash, just need to pass two parameters in order to access both the key and the value
Enerable#all?
is similar toany?
, also looks at truthiness of the block’s return value, but method only returnstrue
if the block returns true in every iterationEnumerable#each_with_index
, nearly identical toeach
, block’s return value is ignored, but takes a second argument representing the index of each element- when calling on a hash, the first argument now represents an array containing both the key and the value
{ a: "ant", b: "bear", c: "cat" }.each_with_index do |pair, index|
puts "The index of #{pair} is #{index}."
end
# The index of [:a, "ant"] is 0.
# The index of [:b, "bear"] is 1.
# The index of [:c, "cat"] is 2.
# => { :a => "ant", :b => "bear", :c => "cat" }
Enumerable#each_with_object
takes a block like above, but also takes a method argument, which is a collection object that will be returned by the method. Additionally, the block takes two arguments of its own:- the first block argument represents the current element, and the second represents the collection object that was passed in as an argument to the method.
[1, 2, 3].each_with_object([]) do |num, array|
array << num if num.odd?
end
# => [1, 3]
Enumerable#first
doesn’t take a block, but does take an optional argument which represents the number of elements to return. If no argument given, it returns only the first element in the collectionEnumerable#include?
doesn’t take a block, but does require one argument. It returnstrue
if the argument exists in the collection andfalse
if it doesn’t.- when called on a hash,
include?
only checks the the keys, not the values - essentially an alias for
Hash#key?
, should probably useHash#key?
instead, as the intention is more explicit.
- when called on a hash,
Enumerable#partition
divides up elements in the current collection into two collections, depending on the block’s return value, and the most idiomatic way to usepartition
is to parallel assign variables to capture the divided inner arrays
odd, even = [1, 2, 3].partition do |num|
num.odd?
end
odd # => [1, 3]
even # => [2]
- always returns an array
- method documentation will normally include:
- one or more method signatures, indicating arguments, block or no block, and what it returns
- a brief description of how the method is used
- some code examples
Quiz Mistakes #
Enumerable#map
- “If
map
was called with a block that returnednil
on every iteration, it would return an empty array.” - NOT TRUE, the return value in this situation
would be an array containing
nil
s; one for each item in the original array. The array would be the same size as the original, but would be filled withnil
s, not empty like select would return with falsey values.
- “If
Enumerable#select
- “If
select
was called on an array with a block that returned a truthy value on each iteration, the original array would be returned” - NOT TRUE, select always returns a NEW ARRAY! Might be the same original values contained therein, but a new array nonetheless.
- “If
Lesson 5: Advanced Ruby Collections #
Sorting #
- Sorting is mostly performed on arrays, since items in arrays are accessed via their index
- Strings don’t have access to sorting methods, but it’s easy to convert them to an array first
- Since Ruby 1.9 it is possible to sort a hash, though there generally isn’t a need to do this
What is sorting? #
- sorting is setting the order of the items in a collection according to certain criterion
Comparison #
- sorting is carried out by comparing the items in a collection with each other, it is at the heart of how sorting works
The <=> method (the “spaceship” operator) #
- any object in a collection that we want to sort must implement a
<=>
method, this performs comparison and returns a-1
,0
, or1
- if
<=>
returnsnil
tosort
then it throws an arguemnt error - all the
sort
method cares about it the return value of the<=>
method - if you want to sort a collection that contains particular types of objects,
you need to know two things:
- does that object type implement a
<=>
comparison method? - if yes, what is the specific implementation of that method for that object
type (i.e.
String#<=>
is implemented differently thanInteger#<=>
String
order is determined by a character’s position in the ASCII table- careful, uppercase
A
precedesa
in ASCIIbetical order - similarly, careful with symbols
- can call
ord
on a string to determine it’s ASCII position
- some useful rules are:
- uppercase letters come before lowercase letters
- digits and (most) punctuation come before letters
- there is an extended ASCII table containing accented and other characters, this comes after the main ASCII table
- does that object type implement a
The sort
method #
- we can also call
sort
with a block, giving us more control over how items are sorted. the block needs two arguments (the two items to be compared), and the return value of the block has to be-1
,0
, ornil
- you can add addional code in the block, as long as the block returns
-1
,0
, ornil
- you can add addional code in the block, as long as the block returns
[2, 5, 3, 4, 1].sort do |a, b|
puts "a is #{a} and b is #{b}"
a <=> b
end
String#<=>
compares multi-character strings character by character (like casecmp), so strings beginning witha
will come before those beginning withb
, which matters more than string lengthArray#<=>
works similarly, going index by index - “in an element-wise manner”- if an int and a string are compared, an error would be thrown by sort, but it’s possible the comparison will short-circuit first and the offending comparison would never be made
The sort_by
method #
sort_by
is similar tosort
but is usually called with a block, the code in the block determines how the items are compared
['cot', 'bed', 'mat'].sort_by do |word|
word[1]
end
# => ["mat", "bed", "cot"]
sort_by
could be used to sort a hash, two arguments would need to be passed to the block, the key and the valuesort_by
always returns an array, even when called on a hash
Array#sort
andArray#sort_by
have equivalent destructive methods sort! and sort_by!. these are specific to arrays and not available to hashes- other methods which use comparison:
min
max
minmax
min_by
max_by
minmax_by
Summary #
- sorting is complex algorithmicly to implement yourself, but we can use the
built-in
sort
andsort_by
methods to do it for us - comparison is at the heart of sorting. when sorting collections, you need to
know if the objects you want to sort on implement a
<=>
method and how that method is defined - methods other than
sort
andsort_by
also use comparison as the basis for how they work
Nested Data Structures #
- collections can contain other collections
Referencing collection ellements #
arr[0][1] # -> 3
Updating collection elements #
arr = [[1, 3], [2]]
arr[0][1] = 5
careful, this is element reference, followed by element update
Other nested structures #
- hashes can be nested within an array as well
- careful, arrays can contain any type of ruby object, including multiple different objects at the same time, including nested data structures
arr = [['a', ['b']], { b: 'bear', c: 'cat' }, 'cab']
arr[0] # => ["a", ["b"]]
arr[0][1][0] # => "b"
arr[1] # => { :b => "bear", :c => "cat" }
arr[1][:b] # => "bear"
arr[1][:b][0] # => "b"
arr[2][2] # => "b"
Variable reference for nested collections #
- careful, variables are pointers!!
a = [1, 3]
b = [2]
arr = [a, b]
arr # => [[1, 3], [2]]
a[1] = 5
arr # => [[1, 5], [2]]
Shallow copy #
- ruby provides us with two methods that allow us to copy an object, including
collections:
dup
andclone
- these both create a shallow copy of an object
- this means that only the object that the method is called on is copied, if the object contains other objects - like a nested array - then those objects will be shared, not copied.
dup
allows objects within the copied object to be modified
arr1 = ["a", "b", "c"]
arr2 = arr1.dup
arr2[1].upcase!
arr2 # => ["a", "B", "c"]
arr1 # => ["a", "B", "c"]
clone
works the same way
arr1 = ["abc", "def"]
arr2 = arr1.clone
arr2[0].reverse!
arr2 # => ["cba", "def"]
arr1 # => ["cba", "def"]
- careful, in these examples both
arr1
andarr2
are changed - this is because the destructive methods of the examples are called on the object within the array rather than the array itself! the objects are shared and the variables still point to the same collection
- careful careful, are you modifying an array or hash, or at the level of the
object within those collections?
- for example, calling
map!
on a copied array would change the array itself, and not the original array… but, calledupcase!
on each object within an array by means of aneach
call would mutate each object, as referenced by both arrays
- for example, calling
Freezing Objects #
- the main difference between
dup
andclone
is thatclone
preserves the frozen state of the object, anddup
d
oesn’t - in Ruby, objects can be frozen in order to prevent them from being modified
freeze
only freezes the object it’s called on. if the object it’s called on contains other objects, those objects will not be frozen.- for example, if you freeze a nested array, the nested objects could still be modified after calling freeze
- this also applies to strings within an array
arr = [[1], [2], [3]].freeze
arr[2] << 4
arr # => [[1], [2], [3, 4]]
arr = ["a", "b", "c"].freeze
arr[2] << "d"
arr # => ["a", "b", "cd"]
Deep Copy #
- In Ruby, there’s not built-in or easy way to create a deep copy or deep freeze
objects within objects.
- when working with collections, especially nested collections, be aware of the level within the collection at which you are working
Working with Blocks #
-
when evaluating seemlingly complex code, ask the following questions:
- what is the type of action being performed? (method call, block, conditional, etc?)
- what is the object that action is being performed on?
- what is the side-effect of that action (e.g. output or destructive action)?
- what is the return value of that action?
- is the return value used by whatever instigated the action?
-
Do Not Mutate The Collection That You Are Iterating Through
- one way to avoid this, is creating a shallow copy of an array, and iterating through the copy while mutating the original
Summary #
- Some important things to remember:
- if at first code appears opaque or complex, take the time to break it down step by step
- if necessary use some sort of systematic approach (such as a table)
- figure out what is happening at each step, paying particular attention to:
- return value
- side effects
- pay attention to the return values of all statements in your code, especially where implicit return values are being relied on
- make sure you have a clear understanding of the underlying concepts such as data structure, loops, iterative methods and the blocks passed to them
- be clear about the method implementation of the iterative method(s) being
used, especially:
- what values are passed to the block
- what the method does with the return value of the block
- if you are unclear about a method implementation, a good initial step is to refer to the ruby docs
Lesson Summary #
- be aware, when you make a copy of a collection object it is a shallow copy, the objects within the collections are shared between the copy an dthe original
- when working with blocks, especially when using nested collections:
- take the time to break down and understand the structure of a collection
- choose an appropriate method and be clear on its implementation and return value
- understand what is being returned by the various methods and blocks at each level. when iterating through nested collections, be particularly aware of the return value of the block and any side effects of the code within the block.
Lesson 6 #
Introduction #
- recommended approach to solving larger problems:
- break down the problem into smaller peices
- map out the flow of the problem in a flow-chart, using sub-processes
- when ready to tackle a component or sub-process, write out the pseudo-code for that sub-process only, with clear inputs and clear outputs
- play around with the code, write every line, don’t copy/paste
- do the assignments in sequence
- don’t be afraid to watch the walk-through videos