Locomotive Engineer exercise relies on implicit Ruby behavior

Hi folks,

I’m working my way through the Ruby track and came across https:/github.com/exercism/ruby/blob/main/exercises/concept/locomotive-engineer/locomotive_engineer_test.rb

I took one of the tests and modified it to showcase what will break without some slightly painful code.

TLDR – based on the hints provided (use hash.values), the code will only work if the hash is created with the stops in sequential order; if the stops are provided out of order, hash.values cannot hold, because Ruby hashes, since 1.9, keep the order of creation for the keys – so I think at the very least we actually have to take the hash and sort it ourselves by the keys.

See my solution, which works very naively because the tests are built in such a way that this code can work - https:/exercism.org/tracks/ruby/exercises/locomotive-engineer/solutions/Trevoke

  def test_add_missing_stops_with_many_stops_this_works
    assert_equal({ from: 'Gothenburg', to: 'Copenhagen', stops: %w[Kungsbacka Varberg Halmstad Angelholm Lund Malmo] },
      LocomotiveEngineer.add_missing_stops({ from: 'Gothenburg', to: 'Copenhagen' },
                                           stop_1: 'Kungsbacka', stop_2: 'Varberg', stop_3: 'Halmstad',
                                           stop_4: 'Angelholm', stop_5: 'Lund', stop_6: 'Malmo'))
  end

  def test_add_missing_stops_with_many_stops_this_is_broken
    assert_equal({ from: 'Gothenburg', to: 'Copenhagen', stops: %w[Kungsbacka Varberg Halmstad Angelholm Lund Malmo] },
      LocomotiveEngineer.add_missing_stops({ from: 'Gothenburg', to: 'Copenhagen' },
                                           stop_1: 'Kungsbacka', stop_3: 'Halmstad', stop_2: 'Varberg',
                                           stop_4: 'Angelholm', stop_5: 'Lund', stop_6: 'Malmo'))
  end

Anyway, uhhhhh I guess I’m opening this because I feel like we’re sort of lying to people about how Ruby works, here

I think there is a point and it could be a value to your point, although this isn’t a practice exercise, it is a concept exercise, starting to sort a hash based on the keys is a bit off-topic. The point of the assignment is that the hash which is given is already sorted for you, so you don’t have to worry about that.

Here is a solution where the you sort the hash based on key:

hash = {}
hash["stop1"] = 1
hash["stop2"] = 1
hash["stop13"] = 1
hash["stop11"] = 1

p hash.to_a.sort_by { |value, key| value.scan(/\d+/).first.to_i }
# => [["stop1", 1], ["stop2", 1], ["stop11", 1], ["stop13", 1]]

We can call it a concept exercise if we want, but we don’t control what people learn, so we should be careful what is available for them to learn. If it is a concept exercise then we should be even more careful what we show, because people who are learning from it will in fact be unable to discern what is fully correct and what isn’t.

If something must be sorted, then a hash is a terrible idea. Yes, it preserves the order, but that’s a side-effect at best. If we need to rely on things being sorted, then we should be using a data structure that reveals the intent of sorting, and a hash does not do that, unless the sort is explicit (I’ll come back to that in a sec).

As you suggesed, yes, we can do hash.sort_by { |k, v| k } and because of the way Ruby handles symbols and strings, it will be sorted.

If we make the sort explicit, such as: { 2 => "Hamstad", 1 => "Verbild" } then it’s much more trivial to consider sorting it.

We can even go one step further and suggest we have the data but it may not be in the right order by, again, using an array: [{name: "Hamstad", stop_number: 1}] – this would respect the intent expressed by both the array and the hash data structures.

Do you have any suggestions on how to improve the exercise?

It is a concept exercise, which we can see from the path you posted:

https:/github.com/exercism/ruby/blob/main/exercises/concept/locomotive-engineer/locomotive_engineer_test.rb


The point of that task in the exercise is to use hash.values. The example in the instructions lists the key, value pairs in order, and they indicate that you must return the array of stops in order. It doesn’t force you to use hash.values , nor does it force you to write the stops in the has in order.

There are people who disagree with a similar choice in JavaScript, where certain tests are placed to ensure people use a mutable solution (see one of such discussion here: Discouraged code practice found in Elyses Enchantments - #7 by DavidOfEarth). The eventual solution posed by @iHiD (here: Elyses Enchantments tests lack coherence - #7 by iHiD) was merged.

I think the Ruby track in general is open to feedback so I would highly recommend you to come up with a concrete change that preserves directing people to Hash#values without the supposed problem you mention.

If you leave that here, I am sure that people like myself will happily assess it and if this meets the requirements for the change, accept a PR.

I ended up writing a lot, so TLDR if you don’t care about any of this: “what if we used a hash that had job titles as keys and people’s names as values, and we created a hash that had a key called ‘staff’ that had all the names from that first hash”.


Okay, so. Both of you insisted very, very heavily on the fact that it is a concept exercise, which means whatever I write comes across as either disrespectful of that concept or not understanding of that concept.

I think concept exercises are fine.
I think concept exercises are crucial.
I do also think concept exercises must be very heavily bounded, and I think here there is this one detail which unfortunately steps out of bounds, which is the reason I opened the pull request on the ruby github repo, which auto-closed the repo and told me to come post it here.

And I also know that it’s incredibly difficult to create concept exercises that aren’t so trivial to read and write that they are bland, and this exercise manages to not be bland and trivial, and that’s an accomplishment in and of itself.

Re: certain tests being placed to ensure people use a mutable solution – that’s a design choice, they indicate the builder pattern, fine, in Ruby you do the same thing by returning self (IIRC this is some version of what rspec does to enable its syntactic sugar).
And re: similar choice in Javascript – that’s Javascript, each programming language likely needs to have this conversation happen separately, in order to represent its own paradigm effectively. In our case, we should aim to follow the community style guide as much as possible. GitHub - rubocop/ruby-style-guide: A community-driven Ruby coding style guide.
The style guide, it must be stated here, is quite pithy about the ordering of hashes:

Rely on the fact that as of Ruby 1.9 hashes are ordered.

And that’s fine - see my suggestions below, which leverage that. It just doesn’t make sense to have keys that indicate an order in a hash. Code wouldn’t be written this way, this hash wouldn’t be generated, and it creates an area of redundant information, which means it’s less clear what is happening and why.

I think we could make a change away from missing stops and towards, say, staff on the train for a particular trip, or for a particular leg of the trip.

So here are two ideas. I am hoping both are sufficiently close to the spirit of the exercise. one remains close to the letter of the exercise, one departs slightly.

  def test_add_waiter_information
    trip = { from: 'Bangkok', to: 'New Delhi'}
    waiters = { car_1: 'Jiminy Cricket', car_2: 'Peter Pan', car_3: 'Oliver Twist'}
    expected = { from: 'Bangkok', to: 'New Delhi', waiters: ['Jiminy Cricket', 'Peter Pan', 'Oliver Twist']}
    actual = LocomotiveEngineer.add_staff_info(trip, waiters)
    assert_equal(expected, actual)
  end
  def test_add_waiter_information
    trip = { from: 'Bangkok', to: 'New Delhi'}
    staff = { conductor: 'Jiminy Cricket', cook: 'Peter Pan', waiter: 'Oliver Twist'}
    expected = { from: 'Bangkok', to: 'New Delhi', staff: ['Jiminy Cricket', 'Peter Pan', 'Oliver Twist']}
    actual = LocomotiveEngineer.add_staff_info(trip, staff)
    assert_equal(expected, actual)
  end

In both of these tests, we are leveraging the fact that hashes preserve the creation order for the sanity of the tests, but we don’t have to, because there is no required order, and in fact the more correct test would include a custom assertion such as assert_arrays_match, which I’m sure all of us have written at least once in our lives.

We could also do this, which is slightly uglier but much sturdier from a testing perspective - though if we do this, we largely stop relying on the hash’s implicit ordering, and we don’t need that here (it’s not like we’re loading this from postgres or anything):

  def test_add_waiter_information
    trip = { from: 'Bangkok', to: 'New Delhi'}
    staff = { conductor: 'Jiminy Cricket', cook: 'Peter Pan', waiter: 'Oliver Twist'}
    actual = LocomotiveEngineer.add_staff_info(trip, staff)
    assert_equal actual.fetch(:from), trip.fetch(:from)
    assert_equal actual.fetch(:to), trip.fetch(:to)
    assert_equal actual.fetch(:staff).sort, staff.values.sort
  end

There you go, I hope this helps a little.

1 Like

The only reason I quoted you then and quote you know is because you wrote it as a conditional “if it is a concept exercise”. Take my comment at face value: it is. No value attachment. All good :blush:

That said, whilst I actually agree with your points made and I also think we shouldn’t rely on the order of the hash, because I also think that’s bad practice, rubocop is not the ruby style guide. Its choices are heavily opinionated and not universally accepted. I’m willing to go as far as to say that it’s not a defacto standard (despite I’d like it to be). I base this on the dozen or so ruby based companies I’ve worked for and with, all with vastly different (and opposite) choices of style for Ruby.

Wouldn’t the solution be to sort the values in the test? I also do like your suggestions personally. I think they work, but I don’t know if arrays should be mixed with hashes on the student side.

Well you mentioned before that we are sorta lying to the student, and sometimes in education, you aren’t giving the student the full picture, just to “simply” things. A good example is the bhor atomic models in chemistry. It is a simple and easily understandable model of how protons and electrons is setup, although when going beyond the first few elements it becomes utterly misleading from the real world. Although it is still used because the fact that it is easier to understand and you can still work with it.

Bhors atomic model for reference:

image

But if you actually want a more realistic representation you would use the quantum mechanical model, which explains everything as more of energy levels and that all electrons are in orbitals which says that it is a 95% chance or something that an electron will be there, and that is based on the fact that you can’t know the speed and the position at the same time. And there are a bunch of different orbitals and it can really quickly become complicated.

quantum mechanical model for reference:
image

Sometimes it is easier to not teach the whole picture in one go and instead take it in steps.

The point is not show the student that, ohh hashes are sorted. It is to show that you can make a hash, then take out the values, by using .values and then put that information back into a hash, aka you are decomposing and then composing again. That is what the concept is meant to teach. The reason why it is written stop_1 and stop_2 is because the keys has to be unique(as you probably already know). It could equally be written: stop_x, stop_y, stop-z. But number feelt more meaning full.

I should also inform you that the ruby version is the third iteration of this exercise. The one written for javascript is although not affected by this because it is setup different (and unreleased for that part). But the ruby version is just a slight modified version of the python one and they share the exercise in the grand scheme of thing. Thereby if this should be changed the other one also have to. I think changing it to staff works, but on that exercise the student shouldn’t be given a 2 hashes, instead 1 hash with an arbitrary number of keyword arguments.

In order of what has come up:

The Rubocop Wallop

Fine, screw Rubocop. I don’t like it anyway. I like my own style better. Like everyone else. That is a whole other conversation to have and I am not interested in starting that flame war. I have time on my hands but not THAT much.

The Sorting Questing

It would. I am not the person to decide how much a concept exercise should introduce beyond the concept itself, and test code is still code, and if these lines of test code need to be explained (if), then maybe it’s not worth it (maybe).

The Array Disarray and the Hash Balderdash

Allow me to break down the lines of code further. Both of you are making statements about data structures which don’t connect with my understanding of the code I’m sharing, or with my understanding of the desired outcomes, so by becoming more explicit I will hopefully allow you to tell me more precisely where I’m going wrong, or where the code was shared communicated the wrong intentions.

First, the concept that Meatball has brought up is that hash.values returns an array, so we must be talking about arrays somewhere. I’m not suggesting we change that.

So about this:

In one place exactly, we must create a hash if we are to stay true to the test. This is the hash.values bit. It’s unavoidable. It’s like death and taxes.

Let’s break the code I shared down into two parts, the test setup, and the test execution and assertion.

Test setup

This first line, defining the trip variable, is the same thing from the original exercise, we have to pass this original hash as the one with basic trip information
This second line, defining the staff variable, is also the same thing from the original exercise, except instead of keys named stop_1 the keys are named after job titles, and instead of being a vague kwargs, it is a specific hash. The reason it is written as a specific hash here is to be extremely explicit about the main point I am trying to make, which is that keys ought not to confuse students about the nature of sorting in the hash data structure in Ruby.

    trip = { from: 'Bangkok', to: 'New Delhi'}
    staff = { conductor: 'Jiminy Cricket', cook: 'Peter Pan', waiter: 'Oliver Twist'}

Okay, I think this is fairly clear to everyone. No arrays here. Right?

Test execution

But the second part… Whoa Nelly!
But this is just the original hash expanded with whatever came out of hash.values, so it’s probably acceptable. The primary difference between this and the exercise as it currently exists is that I’m passing in staff instead of forcing the student to use **kwargs, but as I wrote above, I did so for the sake of clear communication. I assessed that sharing assert_equal({ from: 'Gothenburg', to: 'Copenhagen', stops: %w[Person1 Person2 Person3 Person4 Person5 Person6] }, LocomotiveEngineer.add_staff_info({ from: 'Gothenburg', to: 'Copenhagen' }, conductor: 'Person1', waiter: 'Person2', bathroom_attendant: 'Person3', cook: 'Person4', coal_shoveler: 'Person5', guard: 'Person6')) would be harder to read and understand, so I didn’t do that.

    expected = { from: 'Bangkok', to: 'New Delhi', staff: ['Jiminy Cricket', 'Peter Pan', 'Oliver Twist']}
    actual = LocomotiveEngineer.add_staff_info(trip, staff)
    assert_equal(expected, actual)

Hopefully it’s clear here that the array in expected is the array we need the student to return with hash.values, and that this is the only array this piece of the code is talking about, that it is the only array I attempted to talk about, and that I did not introduce any new arrays.

Hopefully it is also clear that, while asking the student to use **kwargs forces in fact the student to gather all the disparate key-value pairs into a single hash, I am not suggesting we step away from the **kwargs lesson.

The Primitive Obsession Passion

This is the response to the “not teaching the student everything” - yes. I am in fact very carefully not talking about how this code violates the primitive obsession code smell by providing vague, unstructured hashes, instead of specific objects, because that would completely undo the purpose of the exercise, even if it would be good code.

Conclusion?

Okay, I think I’ve addressed everything that you both sent my way. Hopefully this helps.

1 Like

Hey everyone. This feels a little heated and tangental, but it’s clear reading back that we’re all trying to achieve a similar thing here. So let’s be careful now to keep this thread moving into a positive place towards a solution pls :slight_smile:


I think the challenge here is that I just solved the exercise and it never vaguely occurred to me that the stops wouldn’t be passed in the correct order. So the statement of “it feels like we’re lying to people” feels someone outlandish and over the top (and probably thus offensive to the maintainers, which isn’t what you intended). However, if one does consider the challenge from the perspective that someone from a different language (where the hash keys could be delivered in a random order each time, or were alphabetically sorted internally, etc,) then I can see why your statement holds true and that this is problematic as it suggests that to a student.

The absolute easiest thing to me would be to suggest that we say explicitly in the instructions that the steps will always be passed in in the correct order. That removes the risk that people pick up behaviour (and in fact alerts them to the fact this is important for them to know thus being explicit that they shouldn’t make assumptions about Ruby). Having thought about this for a few minutes, I’m not seeing a huge downside to this as the solution. e.g.

Note, the stops will always be passed in the correct order, so you don’t need to worry about sorting them.

From a Ruby perspective, I think it’s absolutely fine to rely on hashes maintaining the order in which they’re created. That’s documented behaviour of Ruby now (as we all know). So I don’t think it’s bad to allow the simple hash.values as long as we’ve been explicit that the keywords will be passed in in the correct order.

Another option would be to ask the person to sort the stops first by key, and to either use letters (stop_a, stop_b) and explicitely say there can’t be more than 26 stops on the line, or continue to use numbers and say there will never be more than 100 stops. Then we can do hash.sort.map(&:second) or hash.sort.to_h.values or whatever. e.g.

The stops may be passed in in and order, and so you’ll need to sort them first. Thankfully there are never more than 99 stops, so you can rely on Ruby’s normal sorting functionality for this.

I think the key is that if we can remove the ambiguity that would be good.


If you’d can’t come to a conclusion or would like some decisive opinion on this, please let me know :slight_smile:

Well, first things first:

If the statement that we’re lying to people feels outlandish, sorry.
If the statement is offensive, sorry.

I am not the guardian of the Ruby track, nor am I even a contributor, so I am in no way situated to be the best person to determine what is and isn’t a good exercise, what is and isn’t helpful, what is and isn’t understandable by the majority, and what is and isn’t a transmissible lesson.

I do have a modicum of experience with code, though of course one’s ability to do something is vastly different from one’s ability to teach something.

2 Likes

Reference issue created on exercism/ruby#1493