Skip to content

Conversation

@meatball133
Copy link
Member

@meatball133 meatball133 commented Dec 31, 2025

This is apart of making ostruct an optional concept: https://forum.exercism.org/t/openstruct-is-now-officially-discouraged/17490

@meatball133 meatball133 force-pushed the add-hashes branch 3 times, most recently from c4876ae to d89a432 Compare December 31, 2025 17:02
@meatball133
Copy link
Member Author

I would say we are a bit in a limbo, though, the array and enumeration concepts are connected to one exercise, and personally, I think it makes sense to introduce arrays before hashes. But the enumeration concept mentions hashes. So either I think enumeration should be stripped out of hashes, which might not make sense. Instead it might make sense to make a separate exercise for arrays so we can have. arrays => hashes => enumeration. I think this shouldn't necessarily hold this pr, since it won't make things worse in my opinion, since not having a hash concept to begin with isnt ideal either.

@meatball133
Copy link
Member Author

An option could be to temporairly place hashes over arrays

@kotp
Copy link
Member

kotp commented Jan 7, 2026

An option could be to temporairly (sic) place hashes over arrays

I do tend to teach "collections" rather than independent data structures, since other than the data structure itself, the idea of Enumerable being the thing that gives a lot of power. What do you think about taking that approach for a concept, rather than the concept of "map/dictionary" specifically? I have not looked at the changes in this PR yet, though. Just got back to my computer after a two week break (travelling).


Even though hashes are unordered collections, Ruby maintains the insertion order of key-value pairs.
This means that when you iterate over a hash, the pairs will be returned in the order they were added.
However, deleting elements may affect the order of remaining elements.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Expand on this, is the order at that point of operation undeterminate? Citation would be helpful for further exploration, and historically in Ruby, this is an interesting point as well.

my_hash = { 1 => "one", :two => 2, "three" => [3, "three"] }
```

Alternatively if the keys are symbols, you can use a more JSON-style syntax:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps avoid JSON reference. But maybe mention the "newer syntax as of Ruby 1.9" form as shown.

my_hash = { name: "Alice", age: 30, city: "New York" }
```

You can create an empty hash using the `Hash.new` method:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Capitalize "Hash" when speaking of the type specifically or the object itself, since hash is also a method and an idea. (Even if it weren't this situation, I would likely suggest capitalization as well as code syntax for the specific thing.)


## Accessing values

You can access values in a hash using their corresponding keys, the syntax reminds of array indexing, but using the key instead of an index:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
You can access values in a hash using their corresponding keys, the syntax reminds of array indexing, but using the key instead of an index:
You can access values in a `Hash` instance using its corresponding keys, the syntax reminds of array indexing, but using the key instead of an index:

You can access values in a hash using their corresponding keys, the syntax reminds of array indexing, but using the key instead of an index:

```ruby
my_hash = { "name" => "Alice", "age" => 30, "city" => "New York" }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No space inside Hash delimiters, similar to Array and Range, save the space inside {} delimiters for blocks.

Suggested change
my_hash = { "name" => "Alice", "age" => 30, "city" => "New York" }
my_hash = {"name" => "Alice", "age" => 30, "city" => "New York"}

Alternatively, present it as:

Suggested change
my_hash = { "name" => "Alice", "age" => 30, "city" => "New York" }
my_hash = {
"name" => "Alice",
"age" => 30,
"city" => "New York"
}

Indentation adjusted for readability (this is how I style my key/value pairs reducing eye jitter), and not necessary, but might be nice to have.

@@ -0,0 +1,38 @@
class GrossStore
UNITS = {'quarter_of_a_dozen' => 3, 'half_of_a_dozen' => 6, 'dozen' => 12, 'small_gross' => 120, 'gross' => 144, 'great_gross' => 1728}.freeze
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice that there is no space inside the delimiter, but why the freeze? We advertise this as a mutable object, but then make it immutable here?

Also blank line after class definition line is a signal of "topic change" and creates a paragraph of sorts.

This is an exemplar, not just an example, so we should apply opinions here to be that exemplar.

But I will not critique explicitly here. Welcome to discuss it on Discord, though, if you want to (even pair program through it if you want).

Or even as a product of a mentor request! ;)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea here was mostly that a user shouldn't modify that hash since it is supposed to be a constant throughout the exercise. So if a user tried to modify it while working with it, it would raise an error.

@@ -0,0 +1,24 @@
class GrossStore
UNITS = { 'quarter_of_a_dozen' => 3, 'half_of_a_dozen' => 6, 'dozen' => 12, 'small_gross' => 120, 'gross' => 144,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
UNITS = { 'quarter_of_a_dozen' => 3, 'half_of_a_dozen' => 6, 'dozen' => 12, 'small_gross' => 120, 'gross' => 144,
UNITS = {'quarter_of_a_dozen' => 3, 'half_of_a_dozen' => 6, 'dozen' => 12, 'small_gross' => 120, 'gross' => 144,

As a minimum, no space inside delimiters.

Perhaps a columnar presentation of the Hash as well, easier to see patterns, etc., than having a horizontal showing of this.

UNITS = { 'quarter_of_a_dozen' => 3, 'half_of_a_dozen' => 6, 'dozen' => 12, 'small_gross' => 120, 'gross' => 144,
'great_gross' => 1728 }.freeze

attr_reader :bill
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

public method definition above the private method initialize is something that I personally detest, it suggests something that is untrue (that initialize is perhaps not private, since in this organization it is in the midst of public method definitions.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In most online resources I found is attr_reader placed above initialize.

],
"prerequisites": [
"advanced-enumeration"
"hashes"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be capitalized, as hashes are created by using hash method calls, and is something different.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the slug

],
"prerequisites": [
"ostruct"
"hashes"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

... same here.

@meatball133
Copy link
Member Author

I do tend to teach "collections" rather than independent data structures, since other than the data structure itself, the idea of Enumerable being the thing that gives a lot of power. What do you think about taking that approach for a concept, rather than the concept of "map/dictionary" specifically? I have not looked at the changes in this PR yet, though. Just got back to my computer after a two week break (travelling).

I think in a way that is a good idea, I think however that the concept might end up being too big. Since this concept is around 200 lines say array is the same and then what about other collections (such as sets). It might make sense to have these after each other but don't know about having a single big one.

@kotp
Copy link
Member

kotp commented Jan 8, 2026

I do tend to teach "collections" rather than independent data structures, since other than the data structure itself, the idea of Enumerable being the thing that gives a lot of power. What do you think about taking that approach for a concept, rather than the concept of "map/dictionary" specifically? I have not looked at the changes in this PR yet, though. Just got back to my computer after a two week break (travelling).

I think in a way that is a good idea, I think however that the concept might end up being too big. Since this concept is around 200 lines say array is the same and then what about other collections (such as sets). It might make sense to have these after each other but don't know about having a single big one.

I would not likely cover sets, but I teach about strings, as a collection as well. Later (intermediate) I teach "what would happen if..." in order to teach mixins with Enumerable.

But yes, no need to teach all the collections, Hash and Range and Array being more than sufficient to gain insights into collections in general, and perhaps not going so far as to "how to create your own 'collection' class".

@kotp
Copy link
Member

kotp commented Jan 9, 2026 via email

@meatball133
Copy link
Member Author

In other words, assert or
refute, but not assert equals true or assert equals false.

I just checked the code and there is nowhere we actually do assert_equal true or false for that matter. We use refute and assert. But I think it might confuse the student if we say return a truthy or falsey value, I think it might be better with true and false.

On another note, rubocop complains when there aren't spaces around the start and end of a hash, I think that was the reason I added them. So I think we would have to disable that if we don't want it

@kotp
Copy link
Member

kotp commented Jan 9, 2026 via email

@kotp
Copy link
Member

kotp commented Jan 9, 2026 via email

@kotp
Copy link
Member

kotp commented Jan 10, 2026 via email

@meatball133
Copy link
Member Author

Yeah, I know... and they are "wrong". ;) And as we know "most often done" does
not necessarily mean "the right thing to do", nor even the "best way to do it".
If we consider the access and scope of what this does, having the "jitter" is
not really desired.
Even the style guide infers that initialize is public because it states "above
other public methods" even though initialize is not public. Having a private
method nested in the midst of public methods at a minimum infers that
initialize is something other than private. And that is objectively not true.
Also, a reason why initialize is often not placed after a private method
call is that Yardoc breaks behavior that it's core tool (rdoc used internally)
does not break, and that is the documentation of the public constructor new.
I think that is a bug in Yardoc, but have not been motivated enough to submit a
fix for it, I likely have also not reported it, or check that it has been
reported.

So I think the ruby doesn't see attr_reader :name as a method but instead a macro and that is why it should be placed above: https://rubystyle.guide/#consistent-classes

I have a hard time going against style guides and how most code is formatted online.

@kotp
Copy link
Member

kotp commented Jan 16, 2026

Yeah, I know... and they are "wrong". ;) And as we know "most often done" does
not necessarily mean "the right thing to do", nor even the "best way to do it".
If we consider the access and scope of what this does, having the "jitter" is
not really desired.
Even the style guide infers that initialize is public because it states "above
other public methods" even though initialize is not public. Having a private
method nested in the midst of public methods at a minimum infers that
initialize is something other than private. And that is objectively not true.
Also, a reason why initialize is often not placed after a private method
call is that Yardoc breaks behavior that it's core tool (rdoc used internally)
does not break, and that is the documentation of the public constructor new.
I think that is a bug in Yardoc, but have not been motivated enough to submit a
fix for it, I likely have also not reported it, or check that it has been
reported.

So I think the ruby doesn't see attr_reader :name as a method but instead a
macro and that is why it should be placed above: https://rubystyle.guide/#consistent-classes

As quoted from the guide link you provided:

  # initialization goes between class methods and other instance methods
  def initialize
  end

  # followed by other public instance methods
  def some_method
  end

"Followed by other public methods" when initialize is in fact not a public method, this is the inference that describes something that simply is not the case. This is why I think that it belongs at the top of the private methods, but not alone, as it "looks" like it might be something that it is not. It is not the constructor (I get professionals telling me it is) and it is not public (and I get professionals telling me it is). I kind of attribute it to this "follow the guide without thinking" idea. Follow what you see everyone else doing, don't ruffle feathers, let people think what they think as they read the guide, further propagate the myth suggested here.

We can do better.

If you check the source code, you will see that attr_reader and the others are written internally as any other method is written, and so it is very likely a method. I am not aware of any formal definition of "macro" in Ruby, but then I may not have been awake that day in class.

I have a hard time going against style guides and how most code is formatted online.

Yet "Online" is only where a relatively small portion of code resides. This is highly biased to "Open Source" code, and perhaps even biased to GitHub specifically.

But, yeah, I get it, it is kind of the tendency to "go along with the flow". But when the style guide is objectively wrong, or written in a way that can be confusing, inferring things that are objectively not the case, against the inference, that shows that the guide is not infallible.

It is hard, though, when the style guide is not official and introduces ideas that perhaps the attr_* methods are not methods but macros, even though they behave as macros in other languages, by writing code (In C for us, when using MRI Ruby), they are still simply methods, and behave no differently than other methods, they are even written in a way under the hood no differently than other methods are created, other than that it also creates and exposes methods at the Ruby level of abstraction. It is a community, and of course is not infallible.

Things here should be taken with a purpose and a sense of "probably a good idea, but definitely not written in stone".

And useful in its own right. But as educators we should be aware of where it kind of (even perhaps helpfully) goes astray.

@meatball133
Copy link
Member Author

If I understand you correctly and just so I do, your idea would be that it should be formated like this:

class GrossStore
  UNITS = { 'quarter_of_a_dozen' => 3, 'half_of_a_dozen' => 6, 'dozen' => 12, 'small_gross' => 120, 'gross' => 144,
            'great_gross' => 1728 }

  attr_reader :bill

  # DO NOT MODIFY ANY OF THE CODE ABOVE THIS LINE

  def add_item?(item, quantity)
    # Add the specified quantity of the item to the bill
    return false unless UNITS.key?(quantity)

    @bill[item] ||= 0
    @bill[item] += UNITS[quantity]
    true
  end

  def remove_item?(item, quantity)
    # Remove the specified quantity of the item from the bill
    return false unless UNITS.key?(quantity)
    return false unless @bill.key?(item)
    return false if @bill[item] < UNITS[quantity]

    @bill[item] -= UNITS[quantity]
    @bill.delete(item) if @bill[item].zero?
    true
  end

  def quantity(item)
    # Return the quantity of the specified item in the bill
    @bill.fetch(item, 0)
  end

  def initialize
    # Initialize an empty bill as a hash
    @bill = {}
  end
end

Because you are correct from a language perspective is initialize a private method, but new is a public method and since these are undountably conected I think (I don't really have any source for this) think that initialize is a "special" case.

Or is it that private methods should be over public? Since if we say that initialize should be treated as a private method and that these methods needs to be grouped and that attr_reader :bill is a public method. I am a bit confussed exactly how you think it should be formatted. And it isn't just the style guide which has this formatting, the offical ruby docs uses this format: https://docs.ruby-lang.org/en/4.0/Module.html#method-i-protected-label-Example

@kotp
Copy link
Member

kotp commented Jan 16, 2026

If I understand you correctly and just so I do, your idea would be that it should be formated like this:

class GrossStore
  UNITS = { 'quarter_of_a_dozen' => 3, 'half_of_a_dozen' => 6, 'dozen' => 12, 'small_gross' => 120, 'gross' => 144,
            'great_gross' => 1728 }

  attr_reader :bill

  # DO NOT MODIFY ANY OF THE CODE ABOVE THIS LINE

  def add_item?(item, quantity)
    # Add the specified quantity of the item to the bill
    return false unless UNITS.key?(quantity)

    @bill[item] ||= 0
    @bill[item] += UNITS[quantity]
    true
  end

  def remove_item?(item, quantity)
    # Remove the specified quantity of the item from the bill
    return false unless UNITS.key?(quantity)
    return false unless @bill.key?(item)
    return false if @bill[item] < UNITS[quantity]

    @bill[item] -= UNITS[quantity]
    @bill.delete(item) if @bill[item].zero?
    true
  end

  def quantity(item)
    # Return the quantity of the specified item in the bill
    @bill.fetch(item, 0)
  end

  def initialize
    # Initialize an empty bill as a hash
    @bill = {}
  end
end

Because you are correct from a language perspective is initialize a private method, but new is a public
method and since these are undountably conected I think (I don't really have any source for this) think that initialize is a "special" case.

The initialize is not a special case, not any more special than allocate, but they are both used by the constructor, other than the documentation that we put for initialize is used to document the constructor (broken in Yard, not broken for Rdoc).

Indeed, we can even reuse and alias initialize if we want to expose it, or even outright make it public.

Or is it that private methods should be over public? Since if we say that initialize should be treated as a private method

We are not saying that it should be treated "as a" private method, we state that it "is a" private method, only because it is, nothing more and nothing less.

However, I would say that we do "treat it specially" as it documents the public constructor, which is the class level new method. We do that by placing it above the rest of the private methods.

But more generally, like a good book, our characters are introduced before they are active players in our story.

Indeed, a "good editor" will allow you to fold the code so that you end up with a "Table Of Contents" view of those private methods, and when you see them used in the protected and public methods, one does not have to scroll beyond to find out where those characters (names) are coming from, we have already traveled down to where they are used, past where they are defined, in order to have already discovered them.

and that these methods needs to be grouped and that attr_reader :bill is a public method. I am a bit
confussed exactly how you think it should be formatted. And it isn't just the style guide which has this formatting, the offical ruby docs uses this format: https://docs.ruby-lang.org/en/4.0/Module.html#method-i-protected-label-Example

You are correct that there are problems in the official Ruby documentation as well, this has been an ongoing problem since at least 1997, when I first started getting involved in the Ruby landscape. But things have been getting better.

I mentor that perhaps we want to do something like this:

  • class/module definition
    • class/module modifications (extend, prepend, etc)
      • class level
        • constants
        • class/modules defined first
        • other class definitions
      • instance level
        • private
          • attribute reader/writer/accessors
          • handwritten methods
            • initialize first
            • other private methods
      • protected
        • attribute reader/writer/accessors
        • hand written methods
      • public
        • attributes reader/writer/accessors
        • hand written methods
  • end of class/module definition

This arrangement also allows us to quickly determine the attributes at each level. If there is nothing in that space, then it is not there, no need to look around for it.

But also, if initialize is not there, then we have not overwritten the inherited private initialize either. No where else to look.

So it is "kind things together" and no miscommunication to be had, no inferring that initialize is somehow different from any other private method, it's only "special" thing is that it is inherited, exists before we overwrite it, and it is already private due to inheritance.

It is all about communication, not not even subtly communicating something that is not the case.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants