The article is a bit confusing, but I like the concept behind symbols: Basically a way to distinguish identifier names (chosen by the programmer) from user-facing strings.
The distinction between is one I've mentally adopted in other languages like Python as well. For personal projects I like to use single quotes for 'symbols' and double quotes for "strings", e.g.:
I almost never use single quotes unless I’m in someone else’s code that already used single quotes. And then I have to fight the urge to not change everything to double quotes just because.
Well, it's a string that will guarantee unique allocations (two identical strings are guaranteed to be allocated at the same address), which makes equality checks super fast (compare pointers directly). But pretty much just a string nonetheless...
Lisp symbols have various properties; the exact set depends on the dialect. The properties may be mutable (e.g. property list, value cell).
You can certainly associate such things with an integer: e.g. value_cell[42], plist[42].
But those associations are part of the "symbolness", not just the 42.
Integers are not suitable symbols in some ways: they have a security problem.
Real symbols can be used for sandboxing: if you don't already hold the symbol, you have no way to get it.
This won't happen for integers. Especially not reasonably small integers.
What do I mean that if you don't already have a symbol, you have no way to get it? If the symbol is interned, you can intern it, right?
Not if there is a symbol package system, and you don't have access to the package in which the symbol is interned.
Now you might say, since objects are typically represented as pointers, aren't those a kind of integer anyway? Yes they are, but they are different type which doesn't support arithmetic; we can restrict programs from calculating arbitrary pointers, but we can't restrict programs from calculating arbitrary integers.
Even if we have escape hatches for converting an integer to a pointer, or other security/safety bypassng mechanisms, those escape hatches have an API which is identified by symbols, which we can exclude from the sandbox.
I (and GP given their use of capitalised Symbol) was talking about Ruby symbols, which for the most part are more equivalent to their object_id than to their textual representation.
What I mean by "integer" is not "pointer", it's "natural number", in the sense that there exists only one `1` vs you can have multiple textual "foo" strings.
So it's more useful to think of symbols as natural numbers which you don't know nor care the actual value of, because as a human you only care about the label you have attached to it, but you do care about its numberness property of it conceptually existing only once.
String can be interned in Ruby, but it’s always possible for an equal string that is not interned to also exist. Hence interned strings can’t benefit from the same optimization than symbols.
You can first compare them by pointer, but on a mismatch you have to fallback to comparing content. Same for the hashcode, you have to hash the content.
I'm talking about as a user of the language, not as a language designer. I have an unpopular opinion about this, but symbols are error prone and these optimizations are ultimately not worth it.
Symbols are special cased pseudo-strings for langages which have either extremely inefficient strings (erlang) or mutable ones (ruby). That’s about the extent of it.
Python or Java don’t really have a use for them because their strings are immutable, and likely interned when metaprogramming is relevant (e.g. method or attribute names).
Whenever Perl encounters a string literal in the code (especially one used as a hash key or a bareword), it often "interns" it. This means it stores a single, canonical, read-only copy of that string in a memory pool.
That's the core idea, and then Ruby has surface stuff like the symbol syntax and class. I'm pretty sure it's fine to use strings as hash keys though if you like.
Ruby has always had frozen strings (what it didn't have was interning of string literals, which is what the somewhat-poorly-named "# frozen_string_literal: true" option available from Ruby 2.3 and made default in Ruby 3.4 actually does, which makes string literals basically equivalent to symbol—but, not actually symbols, unlike, in another example of suboptimal naming, what would happen with String#intern, which has existed longer to intern strings, but is actually just an alias of String#to_sym.
An irrelevant implementation detail. Interned strings are also stored globally, and depending on implementations interned strings may or may not be subject to memory reclaiming.
> Ruby does not care for efficiency, so there is no point to argue for symbols vs string performance
Which is why Ruby's having symbols is associated with mutable strings, not inefficient strings.
And there's a gulf between not caring too much about efficiency and strings being a linked list of integers, which is what they are in Erlang.
Interning is important for symbols that are involved in I/O: being printed and read, so that two or more occurrences of the same symbol in print will all read to the same object, and so there is print-read consistency: we can print an interned symbol, and then read the printed symbol to obtain the same object.
Symbols are useful without this also. Symbolic processing that doesn't round trip symbols to a printed notation and back doesn't require interned symbols.
Symbols have a name, but are not that name.
They also have various properties, which depends on the Lisp dialect.
Classical Lisp dialects, like MacCarthy's original, endow each symbol with a property list. Another classical property is the "value cell".
In Common Lisp lists have a home package retrieved by the function symbol-package.
A variable being globally proclaimed special can be implemented as a property of the symbol.
Symbols are interned in packages, not globally, so two symbols can be interned, yet have the same name: mypackage:let and cl:let both have the name "LET", but different home packages.
Uninterned symbols with the same name can be readily made: just call (make-symbol "FOO") twice and you get two symbols named "FOO", which print as #:FOO.
The #: notation means symbol with no home package, used as a proxy for "uninterned", though a perverse situation can be contrived whereby a symbol has no home package (and so prints with the #: notation), yet is interned into a package.
Introduce a FOO symbol in the keyword package:
[1]> :foo
:FOO
Now import it into the CL-USER package:
[2]> (import :foo :cl-user)
T
Verify that cl-user::foo is actually the keyword symbol :foo:
[3]> 'cl-user::foo
:FOO
Now, unintern :foo from the keyword package, leaving it homeless:
[4]> (unintern :foo :keyword)
T
Let's print it, accessing it via the cl-user package where it is still interned by import:
[5]> 'cl-user::foo
#:FOO
There is quite a bit to this symbol stuff than just "interned strings".
Symbols are simply not strings objects; they have strings as a name.
They're not. A symbol is an arbitrary identifier, which can be used to point to system elements (e.g. classes, methods, etc...). These are all things you can do just fine with immutable interned strings. Which is exactly what languages which have immutable interned strings do.
You'd just have a broken VM if you used mutable strings for metaprogramming in Ruby, so it needs symbols. Both things it inherited from all of Perl, Smalltalk, and Lisp.
They aren't interned frozen strings (unless they were symbols; String#intern was, and still is, an alias for String#to_sym, and String#freeze did not and does not imply String#intern or String#to_sym), and it also (even for literals) took an extra step to either freeze or intern them prior to Ruby 2.3 introducing the "# frozen_string_literal: true" file-level option (and Ruby 3.4 making it unnecessary because it is on by default.)
Amusingly, string literals interned by default in 3.4 or because of the setting in earlier >2.3 Rubies are still (despite being interned) Strings, while Strings interned with String#intern are Symbols.
How so? Quite literally symbols are used as an immutable string with a shorter syntax. So much so that I've been finding their literal constraints limiting lately.
Almost the entire value of symbols separate from strings is at the level of programmer communication rather than PL semantics.
It tells a reader of the code that this term is arbitrary but significant, probably represents an enmeshment with another part of the code, and will not be displayed to any user. When seeing a new term in code that is a lot of the things you're going to need to figure out about it anyway. It's a very valuable & practical signal.
If you need to mutate or concat or interpolate or capitalize or any other string operation it, it probably shouldn't be a symbol anymore, or shouldn't have been to start with.
> Almost the entire value of symbols separate from strings is at the level of programmer communication rather than PL semantics.
That's the opposite of reality. Symbols are necessitated by PL semantics, which is why languages which don't have those problematic string semantics tend to not bother with symbols.
> It tells a reader of the code that this term is arbitrary but significant
That you can do that with symbols is not why they exist (you can need to associate additional semantics with pretty much any first or third-party type after all, that's why the newtype pattern is so popular in modern statically typed languages).
And it's not like you need formal symbols to do it in the first place. For instance like an other nearby commenter in Python I do that by using single and double-quoted strings, having taken up that habit from Erlang (where symbols are single quoted and strings are double quoted).
> And it's not like you need formal symbols to do it in the first place.
I mean we don't need any of this shit. Go take a long bath and then write some assembly I don't care. Symbols are a useful tool in some languages, for the reasons I described. That you're creating ad hoc quoting conventions to recover part of the functionality of this feature in languages that don't have it is a pretty strong signal I'm correct! Opposite of reality my ass lol.
It's completely true of lisp. Lisp strings are generally mutable (although literal strings may be interned and non-mutable, or even worse mutation of literal strings may be UB). Same for Smalltalk.
I'm a ruby developer, would never dream of switching to another language as my bread and butter - but my language design hot take for ruby is that symbols were a mistake and unnecessary and the language would have been better off just using frozen (immutable) string literals for everything except for the syntax of keyword arguments.
Unfortunately we can't change it now, but generally I just don't use symbols anymore unless I absolutely have to.
I've always been conflicted on them. I like that they allow me to be more explicit about things which are read from input or parsed out as strings vs internal parts of my program (I rarely call .to_sym), but I've also lost time to being careless and indexing hash maps with the wrong type.
I've had to fix so many errors over the years due to string/symbol mismatch in function calls and/or hash access, and never once have I benefited from the extra optimizations that the guaranteed singular allocation of symbols have given me where I wasn't able to get the same benefit from a frozen string literal.
If it was up to me I'd get rid of them, but I know I have an unpopular opinion and it's really only a thought experiment.
user = { name: "Alice", age: 30 }
puts user[:name] # Alice
puts user["name"] # nil
I'm 100% convinced that every Ruby developer has at least once made a bug where they tried to access a hash entry using a symbol, where the key was actually a string or vice-versa.
It would be great if Ruby would finally have immutable strings by default and, at that point, it would be possible to make symbols be strings.
This would prevent any such user[:name] vs user["name"] bugs while not breaking any other functionality. And also keeping the memory "optimized" by reusing a single immutable string.
> I'm 100% convinced that every Ruby developer has at least once made a bug where they tried to access a hash entry using a symbol, where the key was actually a string or vice-versa.
Yeah, that is true. It adds a cognitive load onto the ruby developer writing the code as well. Personally I prefer symbols as keys in a Hash, mostly because I like symbols, I assume it may be faster usually (this depends on factors, such as how many symbols one uses, the garbage collection kicking off and so forth, but by and large I think for most use cases, Symbols are simply more efficient). We also have abominations such as HashWithIndifferentAccess; Jeremy wrote an article why that is not good (indirectly, e. g. the article he wrote was about Symbols more, their use cases and differences to Strings, but from this it follows that HashWithIndifferentAccess is not a good idea. While I agree, I think some people simply don't want to have to care either way).
If I need to query a hash often, I tend to write a method, and the method then makes sure any input is either a string or a symbol for that given Hash.
> It would be great if Ruby would finally have immutable strings by default
But it has. I still use "# frozen_string_literal: true", but if you omit it, the Strings are frozen by default. People could set "# frozen_string_literal: true" in a .rb file if they want to retain the old behaviour.
> it would be possible to make symbols be strings.
But Symbols are not Strings. And bugs based on x[:foo] versus x['foo'] are always going to happen. They are very easy to avoid though. I don't really run into these in my own code, largely because I settled on symbols as keys for a Hash.
> And also keeping the memory "optimized" by reusing a single immutable string.
But a Symbol is not a String. Not even an immutable String. I understand what you mean (and internally it may be that way already, actually), but it is not a String.
I also prefer symbols as keys in hash. It just looks more aesthetically pleasing. :)
I think the optimization string vs symbol is negligent in most of the apps. If you need that level of optimization, you should probably switch to Rust.
> If I need to query a hash often, I tend to write a method, and the method then makes sure any input is either a string or a symbol for that given Hash.
This is terrible. This is the exact opposite of what Ruby is trying to achieve: developer happiness. You basically implement "symbol is a string" for hashes (aka HashWithIndifferentAccess).
> But it has. I still use "# frozen_string_literal: true", but if you omit it, the Strings are frozen by default.
This is not the case. If you omit "# frozen_string_literal: true", the strings are mutable, in all versions of Ruby, even in Ruby 4.0, which will be released on 25 Dec.
> But a Symbol is not a String. Not even an immutable String. I understand what you mean (and internally it may be that way already, actually), but it is not a String.
If it walks like a duck and quacks like a duck...
Who cares? What's the difference it makes for you whether symbols and string are interchangeable? Show me one valid use-case where having symbols and strings being different (user[:name] vs user["name"], or attr_reader "name") is useful.
Rails makes this more confusing with HashWithIndifferentAccess[1]. People coming from Rails are often confused by this when working with straight ruby and this kind of hash access doesn’t work.
Agreed. However had it should also be mentioned that this originated from rails.
Many bad things originate from the rails ecosystem. (Arguably some good too, but I am very pessimistic ever since shopify's recent powermove and DHH side-line commenting off-the-fence while being on shopify's board.)
The article is a bit confusing, but I like the concept behind symbols: Basically a way to distinguish identifier names (chosen by the programmer) from user-facing strings.
The distinction between is one I've mentally adopted in other languages like Python as well. For personal projects I like to use single quotes for 'symbols' and double quotes for "strings", e.g.:
Does anyone else do something similar?Single vs double quotes is also semantically significant in Ruby. Double quoted strings support interpolation whereas single quoted does not.
I have been with you for a very long time,until I recently realized how tricky the edge cases are, I cannot figure out a good rule.
A json api, should it use symbols or strings?
Json has string keys, but the api keys are chosen by the programmer.
I can't remember which edgecase,but there are another bunch I found that are tricky.
I almost never use single quotes unless I’m in someone else’s code that already used single quotes. And then I have to fight the urge to not change everything to double quotes just because.
> Does anyone else do something similar?
I do exactly the same thing. Did you also derive it from Erlang?
I think the whole article is super confusing.
A Symbol is really just a string!
Well, it's a string that will guarantee unique allocations (two identical strings are guaranteed to be allocated at the same address), which makes equality checks super fast (compare pointers directly). But pretty much just a string nonetheless...
A symbol really is just an integer which happens to have a fancy name when looked at from a user's point of view.
Lisp symbols have various properties; the exact set depends on the dialect. The properties may be mutable (e.g. property list, value cell).
You can certainly associate such things with an integer: e.g. value_cell[42], plist[42].
But those associations are part of the "symbolness", not just the 42.
Integers are not suitable symbols in some ways: they have a security problem.
Real symbols can be used for sandboxing: if you don't already hold the symbol, you have no way to get it.
This won't happen for integers. Especially not reasonably small integers.
What do I mean that if you don't already have a symbol, you have no way to get it? If the symbol is interned, you can intern it, right?
Not if there is a symbol package system, and you don't have access to the package in which the symbol is interned.
Now you might say, since objects are typically represented as pointers, aren't those a kind of integer anyway? Yes they are, but they are different type which doesn't support arithmetic; we can restrict programs from calculating arbitrary pointers, but we can't restrict programs from calculating arbitrary integers.
Even if we have escape hatches for converting an integer to a pointer, or other security/safety bypassng mechanisms, those escape hatches have an API which is identified by symbols, which we can exclude from the sandbox.
I (and GP given their use of capitalised Symbol) was talking about Ruby symbols, which for the most part are more equivalent to their object_id than to their textual representation.
What I mean by "integer" is not "pointer", it's "natural number", in the sense that there exists only one `1` vs you can have multiple textual "foo" strings.
So it's more useful to think of symbols as natural numbers which you don't know nor care the actual value of, because as a human you only care about the label you have attached to it, but you do care about its numberness property of it conceptually existing only once.
frozen string literals also have unique allocations now, so symbols are kindof redundant now
No they’re not.
String can be interned in Ruby, but it’s always possible for an equal string that is not interned to also exist. Hence interned strings can’t benefit from the same optimization than symbols.
You can first compare them by pointer, but on a mismatch you have to fallback to comparing content. Same for the hashcode, you have to hash the content.
I'm talking about as a user of the language, not as a language designer. I have an unpopular opinion about this, but symbols are error prone and these optimizations are ultimately not worth it.
OP is really tying themselves into knots.
Symbols are special cased pseudo-strings for langages which have either extremely inefficient strings (erlang) or mutable ones (ruby). That’s about the extent of it.
Python or Java don’t really have a use for them because their strings are immutable, and likely interned when metaprogramming is relevant (e.g. method or attribute names).
Whenever Perl encounters a string literal in the code (especially one used as a hash key or a bareword), it often "interns" it. This means it stores a single, canonical, read-only copy of that string in a memory pool.
That's the core idea, and then Ruby has surface stuff like the symbol syntax and class. I'm pretty sure it's fine to use strings as hash keys though if you like.
> I'm pretty sure it's fine to use strings as hash keys though if you like.
Sure. They are just less efficient as hash keys.
Although now the distinction blurs with frozen strings (and the string literals being frozen by default switch).
Ruby has always had frozen strings (what it didn't have was interning of string literals, which is what the somewhat-poorly-named "# frozen_string_literal: true" option available from Ruby 2.3 and made default in Ruby 3.4 actually does, which makes string literals basically equivalent to symbol—but, not actually symbols, unlike, in another example of suboptimal naming, what would happen with String#intern, which has existed longer to intern strings, but is actually just an alias of String#to_sym.
Symbol is the only feature I miss after switching to Python. It makes code so much more readable to distinguish keys and normal strings.
Much like yccs27 above, I do that using single and double quoted strings (a habit I got from Erlang).
Could not read OP, clownflare is down.
> Symbols are pseudo-strings
Can guess by LISP: Symbols reside in the symbol table, which is global scope. Strings reside in the variable tables, subject to local scope.
It is two different storage mechanisms
> inefficient strings
Ruby does not care for efficiency, so there is no point to argue for symbols vs string performance
> Ruby does not care for efficiency, so there is no point to argue for symbols vs string performance
Symbols existed entirely for performance reasons and were once never GC'd: this is absolutely Ruby "car[ing] for efficiency."
Today, Ruby's Symbol is GC'd (and is much closer to 'String' in practicality) but still has enormous impacts on performance.
> It is two different storage mechanisms
An irrelevant implementation detail. Interned strings are also stored globally, and depending on implementations interned strings may or may not be subject to memory reclaiming.
> Ruby does not care for efficiency, so there is no point to argue for symbols vs string performance
Which is why Ruby's having symbols is associated with mutable strings, not inefficient strings.
And there's a gulf between not caring too much about efficiency and strings being a linked list of integers, which is what they are in Erlang.
Lisps have unnterned symbols also.
Interning is important for symbols that are involved in I/O: being printed and read, so that two or more occurrences of the same symbol in print will all read to the same object, and so there is print-read consistency: we can print an interned symbol, and then read the printed symbol to obtain the same object.
Symbols are useful without this also. Symbolic processing that doesn't round trip symbols to a printed notation and back doesn't require interned symbols.
Symbols have a name, but are not that name.
They also have various properties, which depends on the Lisp dialect.
Classical Lisp dialects, like MacCarthy's original, endow each symbol with a property list. Another classical property is the "value cell".
In Common Lisp lists have a home package retrieved by the function symbol-package.
A variable being globally proclaimed special can be implemented as a property of the symbol.
Symbols are interned in packages, not globally, so two symbols can be interned, yet have the same name: mypackage:let and cl:let both have the name "LET", but different home packages.
Uninterned symbols with the same name can be readily made: just call (make-symbol "FOO") twice and you get two symbols named "FOO", which print as #:FOO.
The #: notation means symbol with no home package, used as a proxy for "uninterned", though a perverse situation can be contrived whereby a symbol has no home package (and so prints with the #: notation), yet is interned into a package.
Introduce a FOO symbol in the keyword package:
Now import it into the CL-USER package: Verify that cl-user::foo is actually the keyword symbol :foo: Now, unintern :foo from the keyword package, leaving it homeless: Let's print it, accessing it via the cl-user package where it is still interned by import: There is quite a bit to this symbol stuff than just "interned strings".Symbols are simply not strings objects; they have strings as a name.
> extremely inefficient strings (erlang)
Doesn't most modern Erlang code use binaries instead of charlists? Elixir and Gleam certainly do.
What about LISPs? They have atoms too.
Generally mutable strings there too, like Ruby
This is silly. The semantics are entirely different!
> This is silly.
Oh my bad, great counterpoint.
> The semantics are entirely different!
They're not. A symbol is an arbitrary identifier, which can be used to point to system elements (e.g. classes, methods, etc...). These are all things you can do just fine with immutable interned strings. Which is exactly what languages which have immutable interned strings do.
You'd just have a broken VM if you used mutable strings for metaprogramming in Ruby, so it needs symbols. Both things it inherited from all of Perl, Smalltalk, and Lisp.
Ruby always had immutable (frozen) strings, so no, this never was a reason for Symbols existence.
They aren't interned frozen strings (unless they were symbols; String#intern was, and still is, an alias for String#to_sym, and String#freeze did not and does not imply String#intern or String#to_sym), and it also (even for literals) took an extra step to either freeze or intern them prior to Ruby 2.3 introducing the "# frozen_string_literal: true" file-level option (and Ruby 3.4 making it unnecessary because it is on by default.)
Amusingly, string literals interned by default in 3.4 or because of the setting in earlier >2.3 Rubies are still (despite being interned) Strings, while Strings interned with String#intern are Symbols.
> They aren't interned frozen strings
Doesn't matter. The parent claim was:
> You'd just have a broken VM if you used mutable strings for metaprogramming in Ruby
From day one it was possible to freeze strings used in metaprograming. I mean Ruby Hashes do that to strings keys.
> Ruby 3.4 making it unnecessary because it is on by default.
That's incorrect: https://byroot.github.io/ruby/performance/2025/10/28/string-...
How so? Quite literally symbols are used as an immutable string with a shorter syntax. So much so that I've been finding their literal constraints limiting lately.
Almost the entire value of symbols separate from strings is at the level of programmer communication rather than PL semantics.
It tells a reader of the code that this term is arbitrary but significant, probably represents an enmeshment with another part of the code, and will not be displayed to any user. When seeing a new term in code that is a lot of the things you're going to need to figure out about it anyway. It's a very valuable & practical signal.
If you need to mutate or concat or interpolate or capitalize or any other string operation it, it probably shouldn't be a symbol anymore, or shouldn't have been to start with.
> Almost the entire value of symbols separate from strings is at the level of programmer communication rather than PL semantics.
That's the opposite of reality. Symbols are necessitated by PL semantics, which is why languages which don't have those problematic string semantics tend to not bother with symbols.
> It tells a reader of the code that this term is arbitrary but significant
That you can do that with symbols is not why they exist (you can need to associate additional semantics with pretty much any first or third-party type after all, that's why the newtype pattern is so popular in modern statically typed languages).
And it's not like you need formal symbols to do it in the first place. For instance like an other nearby commenter in Python I do that by using single and double-quoted strings, having taken up that habit from Erlang (where symbols are single quoted and strings are double quoted).
> And it's not like you need formal symbols to do it in the first place.
I mean we don't need any of this shit. Go take a long bath and then write some assembly I don't care. Symbols are a useful tool in some languages, for the reasons I described. That you're creating ad hoc quoting conventions to recover part of the functionality of this feature in languages that don't have it is a pretty strong signal I'm correct! Opposite of reality my ass lol.
That’s really not true for Lisp.
Ruby, like its predecessor Perl, is one of the finer examples of Greenspunning and shows a lot of Lisp influence.
Unfortunately I can’t read the actual submission right now due to the cloudflare outage.
> That’s really not true for Lisp.
It's completely true of lisp. Lisp strings are generally mutable (although literal strings may be interned and non-mutable, or even worse mutation of literal strings may be UB). Same for Smalltalk.
In a Lisp with mutable strings, like Common Lisp, those strings which are symbol names are still mutable.
OP here, you can read it here https://github.com/stonecharioteer/tech-blog/blob/main/conte...
I'm a ruby developer, would never dream of switching to another language as my bread and butter - but my language design hot take for ruby is that symbols were a mistake and unnecessary and the language would have been better off just using frozen (immutable) string literals for everything except for the syntax of keyword arguments.
Unfortunately we can't change it now, but generally I just don't use symbols anymore unless I absolutely have to.
I've always been conflicted on them. I like that they allow me to be more explicit about things which are read from input or parsed out as strings vs internal parts of my program (I rarely call .to_sym), but I've also lost time to being careless and indexing hash maps with the wrong type.
Overall, I think I'm glad they exist.
I've had to fix so many errors over the years due to string/symbol mismatch in function calls and/or hash access, and never once have I benefited from the extra optimizations that the guaranteed singular allocation of symbols have given me where I wasn't able to get the same benefit from a frozen string literal.
If it was up to me I'd get rid of them, but I know I have an unpopular opinion and it's really only a thought experiment.
> Dang it, Ruby. You never cease to amaze me!
This has been true forever.
I still wish the Rails enums were written with slightly more object portability.
[dead]
Symbols are a foot gun.
> Symbols aren’t interchangeable though.
I'm 100% convinced that every Ruby developer has at least once made a bug where they tried to access a hash entry using a symbol, where the key was actually a string or vice-versa.It would be great if Ruby would finally have immutable strings by default and, at that point, it would be possible to make symbols be strings. This would prevent any such user[:name] vs user["name"] bugs while not breaking any other functionality. And also keeping the memory "optimized" by reusing a single immutable string.
That’s not much more of a foot gun than:
Symbols are always taught as a first class object in Ruby, not just syntactic sugar for accessing hashes. “foo” does not equal :foo> I'm 100% convinced that every Ruby developer has at least once made a bug where they tried to access a hash entry using a symbol, where the key was actually a string or vice-versa.
Yeah, that is true. It adds a cognitive load onto the ruby developer writing the code as well. Personally I prefer symbols as keys in a Hash, mostly because I like symbols, I assume it may be faster usually (this depends on factors, such as how many symbols one uses, the garbage collection kicking off and so forth, but by and large I think for most use cases, Symbols are simply more efficient). We also have abominations such as HashWithIndifferentAccess; Jeremy wrote an article why that is not good (indirectly, e. g. the article he wrote was about Symbols more, their use cases and differences to Strings, but from this it follows that HashWithIndifferentAccess is not a good idea. While I agree, I think some people simply don't want to have to care either way).
If I need to query a hash often, I tend to write a method, and the method then makes sure any input is either a string or a symbol for that given Hash.
> It would be great if Ruby would finally have immutable strings by default
But it has. I still use "# frozen_string_literal: true", but if you omit it, the Strings are frozen by default. People could set "# frozen_string_literal: true" in a .rb file if they want to retain the old behaviour.
> it would be possible to make symbols be strings.
But Symbols are not Strings. And bugs based on x[:foo] versus x['foo'] are always going to happen. They are very easy to avoid though. I don't really run into these in my own code, largely because I settled on symbols as keys for a Hash.
> And also keeping the memory "optimized" by reusing a single immutable string.
But a Symbol is not a String. Not even an immutable String. I understand what you mean (and internally it may be that way already, actually), but it is not a String.
I also prefer symbols as keys in hash. It just looks more aesthetically pleasing. :) I think the optimization string vs symbol is negligent in most of the apps. If you need that level of optimization, you should probably switch to Rust.
> If I need to query a hash often, I tend to write a method, and the method then makes sure any input is either a string or a symbol for that given Hash.
This is terrible. This is the exact opposite of what Ruby is trying to achieve: developer happiness. You basically implement "symbol is a string" for hashes (aka HashWithIndifferentAccess).
> But it has. I still use "# frozen_string_literal: true", but if you omit it, the Strings are frozen by default.
This is not the case. If you omit "# frozen_string_literal: true", the strings are mutable, in all versions of Ruby, even in Ruby 4.0, which will be released on 25 Dec.
> But a Symbol is not a String. Not even an immutable String. I understand what you mean (and internally it may be that way already, actually), but it is not a String.
If it walks like a duck and quacks like a duck... Who cares? What's the difference it makes for you whether symbols and string are interchangeable? Show me one valid use-case where having symbols and strings being different (user[:name] vs user["name"], or attr_reader "name") is useful.
When one consistently uses symbols for keys and strings for data then when you notice a `user[<String>]` it is a very visible, obvious mistake.
Rails makes this more confusing with HashWithIndifferentAccess[1]. People coming from Rails are often confused by this when working with straight ruby and this kind of hash access doesn’t work.
1. https://api.rubyonrails.org/classes/ActiveSupport/HashWithIn...
Here's a fantastic writeup about frozen strings in Ruby and the upcoming changes: https://byroot.github.io/ruby/performance/2025/10/28/string-...
I'd guess that the majority of people who've made a bug like this got started on Ruby via Rails, where many hashes are HashWithIndifferentAccesses.
HWIAs are convenient, but they do confuse the issue.
Sure, we wouldn't have ActiveSupport::HashWithIndifferentAccess if it wasn't an occasional issue.
HashWithIndifferentAccess was added because back in the day symbols were immortal, hence could not be used for request parameters.
It no longer make sense today, and any new use of it is a smell.
Agreed. However had it should also be mentioned that this originated from rails.
Many bad things originate from the rails ecosystem. (Arguably some good too, but I am very pessimistic ever since shopify's recent powermove and DHH side-line commenting off-the-fence while being on shopify's board.)
Rails has paid my salary for the best part of 20 years on and off. I'm OK with it. ;)
The foot-gun there is dynamic typing, not symbols.