Assignment 1: One-Liners
Errata
Problem 2: We say don’t use Array#reverse; but avoid all #reverse methods on core classes (Array#reverse_each, Enumerable#reverse_each, etc.). For example, you will be downgraded for using String#reverse. There are a number of excellent solutions.
Problem 4: you may assume that the sorted order of the values is distinct.
Problem 6: You may assume that the parts of the String before and after the dash are greater than or equal to 0. Numbers only (so no “a..z”).
Problem 9: When we say that the items 1, 5, 10, etc. are the same, we mean that the first three elements of each array are the same.
No solutions require the use of “eval”! Avoid!
No solutions require the use of “tap”! Avoid!
The point of this assignment is to develop your skills at writing concise and “Rubyish” Ruby code. Ruby has large and clever core and standard libraries, as well as syntax that allows for writing short but readable code. Some claim that 10 lines of Java can be expressed in 1 line of Ruby.
We give you a Ruby class with a number of methods — but in the code we hand out, there is no implementation for each method. You have to write the implementation. Each method is preceded with a comment that defines the parameters and return value for the method. There are 10 problems; You must do as many as you can. Each is worth 10 points, totaling 100 points. Some are easy; some are hard.
We’d also like you to invent a Ruby one-liner challenge of your own. In the class we hand out, you’ll see that after the 10 problems, there is an additional method called student_one_liner. At present it has two parameters a and b, but, like many of the one-liners we assign to you, you may have a good one with just 1 parameter. Then over in the test code in test/other/solution_set_test.rb, you should write one or more tests that exercises your one-liner — or simply replace the example method called test_student_one_liner. Also don’t forget to put a comment before the method that “solves” your one-liner that explains what the one-liner is supposed to do. If you write a great one-liner challenge, we will use it in the course in a subsequent year and will credit you. We will take the quality of your one-liner into account should your final score fall on a grade boundary.
It is fair game to consult the Pickaxe (also known as Programming Ruby), Ruby documentation, and other published works and articles on Ruby in print or on the web. It is not fair, however, to use a search engine to find answers. That is, if there’s some struggle to find an example answer, that’s ok because I’d hope you’d learn something along the way: But Googling for the specific answer would not create the neural pathways in your brain.
The easiest way to work through these is to write your code in irb. Once you have a good solution, paste it into the solution set. Indeed, facility with irb is one of the most important things you should be practicing at this stage of the course.
There are some rules regarding your implementation for each method.
- Your implementation must be one line of Ruby code. What is meant by “a single line”? You must supply one statement. You may not use the statement separator (the semi-colon) to separate statements inside the method block. There is an exception to the “single line” rule: You may include one extra statement inside of an iterator block by using the semi-colon. (This proves to be especially important for inject, but is also sometimes useful for collect as well.)
- Unless explicitly permitted by the question, you may not define additional variables. This doesn’t include block parameters — the params inside vertical bars in an expression like
a.map { |e| e*2 } - You may not change the values of the parameters, or elements of the parameters (if any). (For example, if an array [1, 2, 3] is passed in as the parameter a, you may not set a[0] = 2 in the method.) To prevent this, we “freeze” the parameters before we test your methods.
- Avoid regular expressions. Assume that there is a “better” Ruby way to do something than to use a regular expression. Exception: It is very handy to split a String into an array with
s.split(//)(though even this can be done without explicitly giving a regular expression withs.split(''). - Always hand in whatever you have. In other words, if you have had to use extra statements, variables, or regular expressions, still hand it in! Working code, even if not Rubyish and elegant, is still working code and will be recognized as such. The best submissions, though, will observe all of the rules above.
– use those all you want.
The following is NOT an acceptable solution for a couple of reasons: It’s more than one line, and it depends on introducing an extra variable h outside of your single line:
# Not acceptable!
h = Hash.new
h.default=0
a.collect! { |x| h[x] += 1 }
(If you find yourself needing to introduce a variable that “keeps track” of something, review the documentation for Enumerable#inject – Also note that it is fair game to use the new method as needed . . .)
Example:
# Given an Array 'a' of Strings, return a new Array with the same values sorted in reverse # order. def empl_a(a) # your solution end
So, for example, a solution would be like so:
# Given an Array 'a' of Strings, return a new Array with the same values sorted in reverse # order. def empl_a(a) a.reverse end
You must also define a few methods that provide your name, e-mail address, and student id. In their current form they look like this:
def info_first_name "Your first name" end
And must become:
def info_first_name "John" end
To make it easier to check your work, we have provided you with a test framework. To run it, change your directory to test/other/ and run solution_set_test.rb. For example:
jgnmbair:assn1 jgn$ cd test/other jgnmbair:other jgn$ ruby solution_set_test.rb Loaded suite solution_set_test Started ..FFFFFFFFFFFFFF Finished in 0.033543 seconds. 1) Failure: test_prob01_0(StudentSolutionSetTest) [solution_set_test.rb:35:in `check' solution_set_test.rb:19:in `test_prob01_0']: <"ThisIsATestOfTheEmergencyBroadcastSystem"> expected but was <nil>. # MUCH DELETED 16 tests, 16 assertions, 14 failures, 0 errors jgnmbair:other jgn$
Notice the line with “..FFFFFFFFFFFFFF” — the dots signify passed tests. We provide the solution for the first four examples. The rest of the tests are failing because there is no code defined to satisfy the requirements.
Each method is tested with one or more data sets. You can look at the datasets in test/fixtures/test_case_datasets.yml. While you are working, you may want to look at this, and even edit it to add or remove sample data. But keep in mind that we will test with at least this data and possibly more.
We have also added a means to test just ONE problem. If you specify the problem number (01, 02, 03, etc.) after “ruby solution_set_test.rb”, then the tests for that problem only will be run, and we also dump out the input parameters and expected results. Example:
jgnmbair:other jgn$ ruby solution_set_test.rb 05 Loaded suite solution_set_test Started . Problem: 05 Dataset: 0 Parameter(s): a = [25, 10, 5, 1] b = 124 Exepected result: [4, 2, 0, 4] F Problem: 05 Dataset: 1 Parameter(s): a = [25, 10, 5, 1] b = 194 Exepected result: [7, 1, 1, 4] F Finished in 0.009801 seconds. 1) Failure: test_prob05_0(SolutionSetTest) [solution_set_test.rb:59:in `check' solution_set_test.rb:24:in `test_prob05_0']: <[4, 2, 0, 4]> expected but was <nil>. 2) Failure: test_prob05_1(SolutionSetTest) [solution_set_test.rb:59:in `check' solution_set_test.rb:24:in `test_prob05_1']: <[7, 1, 1, 4]> expected but was <nil>. 3 tests, 2 assertions, 2 failures, 0 errors jgnmbair:other jgn$
Checklist for Submission
- Download the code bundle from the downloads page.
- Define the “info” methods providing your first name, last name, e-mail address, and student id number.
- Define your implementations for the 10 methods.
- Include your own one-liner (comment, implementation, and test).
- Zip everything up into an archive with your name in the file name. NOTE: You can use the “rake package” command to do this very neatly.
- One last thing: Please do not post this assignment anywhere or make copies of it.
When asking for student ID, do you want DCE ID or the Harvard ID?
@Ken Vedaa
The Harvard ID if you have one. We don't check the semantics of the id, so you could put into that field "DCE: xxxxx" or "HUID: yyyyy" -- the basic idea is that if there is confusion about the name, we have some way to differentiate you.
With the dropbox system at iSites, the necessity to collect id's is probably moot now (we used to accept submissions by e-mail).
I just started to poke around at this assignment and I think including the test framework is extremely helpful. Test-driven-development (you all may see TDD on job postings) is definitely something I want to learn more about and testing with ruby early makes me happy.
I noticed that there is a YAML file (I think of it as cleaner XML) that seems to contain most of the logic for the tests. I'm intrigued and curious if we will be learning how to build tests in this manner. I don't think it's quite covered in the book -- from the few chapters I've read and the chapter on Unit Testing. Right now the code looks confusing but way cool. It differs from what's covered in the book (where the logic and assert methods are part of the test-rb files).
I would guess this method can save a developer a lot of time developing the test cases. I'm also guessing that one could reuse the solution_set_test.rb file for a variety of projects with a few modifications to the file and a valid fixture. I think I like this method better because it decouples hard-coded/expected values from the test Class and places it in a more acceptable "properties-like" file.
Please correct me if I'm assuming too much!
@Donnie Demuth
The test case data is stored in the YAML file. In the assignment summary, I point out that you can do:
ruby solution_set_test.rb 05
To test specifically problem 5 only. When you do this, it will dump out to the console each dataset for that problem (there can be many -- we actually don't provide you enough different datasets) and the expected return value for each set. By that means, you can see the data in the YAML file in a more obvious way.
These tests are fancy, because we generate on the fly a set of tests for each problem -- it uses Ruby meta-programming. If anyone is interested, I can provide a walk-through some time -- maybe I'll do it when I show some of the meta-programming techniques.
In any case, what you are seeing is just the tip of the iceberg. We have a little database of one-liners, and then certain ones are picked. Then there is a Ruby program that generates the test skeleton and the YAML file. When we generate the test skeleton, we also generate another file (not in your download) for staff that implements each problem with a working solution. This makes it easier to compare your code to a solution that is known to work. [In fact, we have multiple solutions for most of the one-liners.]
When we discover/invent new one-liners, we just add them to the generator.
The generator code is a hack (pretty gross, really), but it's also a good example of how much you can accomplish with Ruby just to "get it done."
If I say
ruby solution_set_test.rb 12
it says that 2 tests passed, even though there is no problem 12. What is it testing?
Also, when we write our own one-liner, are we supposed to add it to test/fixtures/test_case_datasets.yml ? That seems to presuppose a lot of knowledge of the framework you described above.
@Ron Newman
When the Test::Unit::TestCase starts up, it checks to see if there is any test defined. If there is not, it stops right there.
Since our tests are added dynamically, a test has to exist already. So SOME test has to exist. In our case, besides the dynamically-generated tests (based on the test data), there are two regular tests, test_for_name and test_student_one_liner. They both pass, and they are checked no matter which specific test you ask for. Arguably there should be an error message for trying to test 12. I don't know why you would do that -- we make some assumptions about the person testing.
@Ron Newman
To test your one liner: Simply edit solution_set_test#test_student_one_liner - hardcode your tests right in there (like a normal test).
Notice how the test that is given tests the example student one liner I wrote myself.
I'll add a sentence or two to make this a bit more clear.
I tried the "test 12" thing mainly because when I did "test 08" before I had actually implemented anything, it told me that 2 tests had passed and one had failed.
As recommended by the syllabus, I attempted the assignment to make sure that I'll be able to "hang" with the class. As mentioned above, some of the probably are really hard and this took me much more than 5 hours... Walking away and coming back to the difficult ones helped out a lot.
These were surprisingly hard -- I'm not used to thinking this way. Problem 8 was the hardest for me, and I ended up writing a recursive solution after failing to come up with anything else. (Would have been easier if Ruby Enumerable had #collect_with_index or #inject_with_index methods.)
I'd like to add more test cases to test/fixtures/test_case_datasets.yml , to increase my confidence that I've created correct solutions to the problems. But I'm not sure I really understand the syntax there, or where to go for documentation of it. What does "- !ruby/object:InputOutput" mean, and why do some lines bgin with two hyphens instead of one?
@Ron Newman
Ron - Re: working through Assignment 1: Note that I haven't lectured on blocks and iteration, so hold off on submitting: You might find that after I get into it you'll rethink your implementations.
For adding tests: I would recommend that you not touch the YAML. You can add any tests you want by writing new test methods -- take a look at the way the example student_one_liner is tested.
The YAML is just a way to represent the contents of objects. Because some of the data regarding specific classes, the YAML file includes the name of the class (InputOutput). Take a look at lib/one_liners.rb for the class definitions.
If I write my own test methods, what should I call them, and how do I arrange to get them invoked? Will it be invoked in addition to your YAML test cases, or instead of them?
(Above, you mentioned "edit it [the .yml file] to add or remove sample data" which is why I was headed in that direction)
(I think what's confusing me here is that I can't figure out what code actually starts executing in solution_set_test.rb -- it appears at first glance to be just a class with method definitions and nothing that invokes them)
Any method beginning test_ will be run.
It is really up to you, whether you add to the YAML file, or add additional test_ methods. The advantage of adding test_ methods is that you'll actually be learning how to add tests in the typical case. The way the YAML file is rigged up is somewhat idiosyncratic; we did it that way so that our whole "one-liner" system is automated.
But do note that the tests that count are the ones we provide.
If you add tests that don't conflict with our idea of what the one-liners are supposed to do, we'll likely add the test cases to our database for future versions of the class.
Any method beginning test_ will be run.
Ahhh! That's the part I didn't understand. Thanks.
I think this is true, as the logic would have been easier with this, however, once I found a solution to the problems (with the exception of adding commas to really big numbers), adding with_index or _index may have made the code more complex than it needed to be.
I notice that the one-liners are going to be used throughout and maybe at the end of the session for grading purposes (for deciding which grade students get if they are "on-a-edge"). This really excludes sharing our solutions and discussing some of this stuff which I feel is really fun to look at. Can we get 5 or so problems that we could all play with that give us a chance to discuss different strategies like map, collect, inject? I found myself getting so comfortable with inject, that I rarely picked to use something else throughout, though there were some exceptions where I used map or collect.
@Jeff Ancel
I think this will be covered when "Blocks and Iterators" are discussed in class. We may be jumping too far ahead and should stick to the track/rail mentioned in the syllabus as there are many other students on board.
That said, I did stumble across the RubyQuiz website today after reading a ruby developer's wiki. This might provide a good understanding on how different Rubyists attempt a problem -- as well as keeping our instructor's workload more manageable. What happens is: a problem is discussed, users send in their answer, and a moderator discusses what he liked and didn't like. I hope that helps.
Jeff and Donnie,
I will post a "Note" with a few one-liners we didn't assign this year for various reasons: You can discuss potential solutions there.
John
does the "no new objects"-rule also apply to objects provided to the inject block? Or in code:
a.inject(Hash.new(0)) { ...more code...}
Is this allowed?
@Gabriel Hase
a.inject(Hash.new(0)) is fine!
The rule is: You may not define additional variables.
Create new objects as needed.
A little question for the expected output of prob09:
Do the sub-arrays have to be in the same order as in the original array?
So for the input:
a = [[1, 2, 3, 4, 5], [1, 2, 3, 9, 4], [1, 2, 3, 4, 4], [2, 3, 4, 5, 6], [4, 5, 6, 7, 8],[2, 3, 4, 7, 9]]
are both of the below valid ouptut or only the first (only order of subarrays different, not the subarrays themselves)?
[[1, 2, 3, 4, 5], [2, 3, 4, 5, 6], [4, 5, 6, 7, 8]]
[[1, 2, 3, 4, 5], [4, 5, 6, 7, 8], [2, 3, 4, 5, 6]]
@Gabriel Hase
I was curious about this also. Eventually, I stumbled upon a different method (vs using inject) that produced the same results given by the example. I do recall running the test-script on both methods successfully.
The answer to this seems to depend a lot on whether Ruby 1.9 preserves the insertion order of Hash elements (for instance, when you later call h.each, h.collect, h.inject, h.keys, h.values, etc.). If it does, and if you can depend on this fact, it's much easier to write deterministic test cases.
The Pickaxe (page 71 of the PDF) says: " And, as of Ruby 1.9, you’ll find something that might be surprising: Ruby remembers the order in which you add items to a hash. "
Page 533 of the same Piackaxe PDF seems to hedge a bit: "The order in which keys
and/or values are returned by the various iterators over hash contents will generally be the
order that those entries were initially inserted into the hash. " What does 'generally' mean in this sentence?
http://www.ruby-doc.org/core/classes/Hash.html says something different: "The order in which you traverse a hash by either key or value may seem arbitrary, and will generally not be in the insertion order".
"ri Hash" says the same thing as ruby-doc.org .
Who's right?
@Ron Newman
In 1.9, the order of the keys is preserved (from insertion). I.e., the Hash is an ordered hash. Personally, I think this is horrible, and that they should have added a separate ordered hash class. It is ok to depend on this feature to pass tests, but you would not want to write real-world code that assumes that the order of the keys is determinant (to preserve compatibility with earlier versions of Ruby). Other changes between 1.8.7 and 1.9.1 bother me less because they fail faster (so you notice) but I think this change can be insidious.
http://eigenclass.org/hiki.rb?Changes+in+Ruby+1.9#l86
http://www.igvita.com/2009/02/04/ruby-19-internals-ordered-hash/
So, for problem 4, if I have a hash that has 2 different keys with the same value, what is the expected result?
Say, a = {"foo"=>500, "baz"=>1, "hither"=>2, "nah"=>500}
I guess we still have to preserve all the keys?
naomi.
In your example, two VALUES are the same. The keys are all different.
The result should be baz=1&foo=500&hither=2&nah=500
That's the answer for problem 5. The answer for problem 4 is under-specified because two hash values are the same; it could be either
["baz", "hither", "foo", "nah"]
or
["baz", "hither", "nah", "foo"]
(which makes such a case hard to test)
Now if you really do have duplicate keys, Ruby will silently discard one of them without any kind of error or warning:
irb(main):006:0> a = {"color" => "red", "pattern"=>"plain", "color"=>"blue"}
=> {"color"=>"blue", "pattern"=>"plain"}
@Ron Newman
@Naomi
Ah. Sorry. For #4, you may assume that the values sort to a distinct order.
@Ron Newman
True, but there is no warning/error by design.
When we get to talking about testing, it might be useful to discuss how to write a test for a method whose expected result isn't fully specified. Problem 5 is another such method, since the order of CGI GET parameters is deliberately non-significant.
@Ron Newman
A "normal" test is just code, so you would write Ruby to check that the result conforms with your requirements. There is no magic. You would probe for each hash key, and verify that the key is there; and then check that the value is what you want.
The tests for assignment 1 are unusual because for each method, we provide a list of sample input/output pairs; and the output is essentially constant and doesn't admit of variability.
For #5, I should have added the expeectation that the output param order is the same as the input Hash key order (which works for Ruby 1.9.1). Also, next year, we will remove the language "list of URL parameters," and just describe the output, to prevent assumptions being made about how URL parameters work.
Thanks. Right now it actually says the opposite: "The order of URL parameters does not need to be the same as the order of Hash elements".
I'll send a note around. The original wording is a leftover from Ruby 1.8.6/7, and we told students that if the test failed we would examine it manually.
regarding the ";" and statements is this allowed:
a.inject { |m,e| statement1;statement2;statement3;statementN;m}
@Lateral Punk
No! Only one statement separator may be present (meaning that you can have two statements). The canonical case is the "; m" at the end, though we will allow two more sophisticated statements. In any case, ONE statement separator. Since you're using the curly braces for your one-liners, that statement separator would be a ; not a newline. Just looking over our sample solutions, you should only need the semi-colon for injects that need to return the memo (that is, the "; m" formulation). Of course, you are free to use more statements; but your score will be reduced.
Incidentally, I would very strongly discourage using .tap, which I haven't lectured on. It is unnecessary for these one-liners.
Here's how I put these rules above: Let me know how it's not clear so that I can make it more clear next time:
"What is meant by 'a single line'? You must supply one statement. You may not use the statement separator (the semi-colon) to separate statements inside the method block. There is an exception to the “single line” rule: You may include one extra statement inside of an iterator block by using the semi-colon."
i got my answer when I re-read the assignment description. At most one ";"
I figured out how to achieve what I wanted via the ternary operator
@john
Just saw your post after i submitted mine. thanks for the detailed explanation as always. I was hesitant when I posted my question since it seemed like a hack anyways. When I re-did that question, I was really impressed with the power of Ruby. I mean this thing is just nuts. I've been talking about it with some of my non-programmer friends and even they are finding it interesting. So far, believe it or not, quesiton 2 was the hardest for me (I'm on #8 and I think i've figured it out). I must have spent a totla of 2 hours on question 2 (obviously i moved onto the others and learned from them).
Truly enjoying this assignment...
@Lateral Punk
just making sure this is ok right:
a.inject('') { |m,e| if (cond) ? then_clause : else_clause; m }
exactly one ";"
please please please
@Lateral Punk
That's fine, though you may not need the "if" since you're using the ternary operator.
Example:
# return a new array. If an element == 2, then use "two" for that element
# in the new array. Otherwise, "something else"
a = [ 1, 2, 3 ]
a.inject([]) { |m, e| m << (e == 2 ? 'two' : 'something else'); m }
The reason we allow it is this:
If your main statement doesn't evaluate to something suitable for the memo, then you need some means to make that happen. So we allow the extra statement.
In the case above, you don't actually need the additional statement.
a.inject([]) { |m, e| m << (e == 2 ? 'two' : 'something else') }
But consider this:
# create a new array. If an element == 2, then use "two" for the new element;
# otherwise do not add an element.
a.inject([]) { |m, e| m << "two" if e == 2 }
In this case, the statement can sometimes evaluate to nil (when the condition e == 2 is not met). In that case, you need that extra statement:
a.inject([]) { |m, e| m << "two" if e == 2; m }
for prob09:
it says "You may assume that the items (1, 5, 10, etc.) are sorted from lowest to highest."
but the example input has:
a = [[1, 2, 3, 4, 5], [1, 2, 3, 9, 4], [1, 2, 3, 4, 4]]
the 2nd "element" has the 9 before the 4 so it's not sorted....what gives?
@Lateral Punk
The data is right, the wording needs a bit of clarification:
You may assume that the items relevant relevant to how "sameness" is defined -- the first 3 -- are sorted from lowest to highest.
In the example input:
[
[1, 2, 3, 4, 5],
[1, 2, 3, 9, 4],
[1, 2, 3, 4, 4]
]
As you can see, in each case the first three items (1, 2, and 3) are sorted from lowest to highest.
So, I'm keeping some notes as I get a one-liner done, as well as unsuccessful tries and ideas on how to implement it differently. Should I keep this when submitting my assignment or it would be confusing?
thanks,
naomi.
@Naomi
Good question. We're sometimes interested in comments regarding HOW it works, and why your solution might be better than others:
def prob09a( . . .)
# .collect is not suitable here, because we need to come up with a sort
# of "sum" of the different elements, which suggests .inject
end
The history of your path to your submission is less interesting than a note or two as to why your solution is particularly good giving the alternatives or things that wouldn't work.
But, truly, the TA's are going to be focused on your code, almost to the exclusion of your comments. The reason is that we know most of the good solutions, so if you've done something really unusual (either in a good way or a bad way) it will jump out.
What do you think?
Style question.
I notice I wrote my answers with one letter variables. Normally I'd never do this, but that seems to be the style of this assignment, no?
@Ken Busch
Good question. For future assignments, I hope everyone will use description variable, method, and class names.
For the one-liners, the expectation that the solutions will be concise makes short variable names appropriate.
Also:
For the parameter names in blocks: I always use very short (usually one-letter) parameter names, mostly to emphasize that the parameter doesn't have a huge amount of "meaning" in that context.
@Naomi and @john: I put in comments for one of my solutions, because my solution was recursive, and John told me that recursion was not necessary to solve the problem.
In the end I decided I liked recursion better than the alternatives, but I put several other (less concise but non-recursive) solutions in comments.