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).
> 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
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.
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 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.
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).
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.
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.
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.
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-...
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.
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, 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.
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.
Doesn't most modern Erlang code use binaries instead of charlists? Elixir and Gleam certainly do.
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.
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...
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.
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.
This has been true forever.
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.:
add_label(text="Hello World!", align='center')
Does anyone else do something similar?I do exactly the same thing. Did you also derive it from Erlang?
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.
Unfortunately we can't change it now, but generally I just don't use symbols anymore unless I absolutely have to.
Overall, I think I'm glad they exist.
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.
Mystery-Machine•2mo ago
> 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.
Lio•2mo ago
shevy-java•2mo ago
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.)
Lio•2mo ago
byroot•2mo ago
It no longer make sense today, and any new use of it is a smell.
shevy-java•2mo ago
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.
Mystery-Machine•2mo ago
> 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.
lloeki•2mo ago
an0malous•2mo ago
IshKebab•2mo ago
thomascountz•2mo ago
tsm•2mo ago
HWIAs are convenient, but they do confuse the issue.
rubyfan•2mo ago
1. https://api.rubyonrails.org/classes/ActiveSupport/HashWithIn...