How are keywords different from symbols?

From a high-level understanding, I tend to think of keywords as being "a closed set of strings determined by your program's source code", while symbols are similar but the domain is at runtime.

Neither usage is strict: You can turn arbitrary strings into keywords and symbols. But generally, keywords are used in function definitions and macros and similar source code-level concepts, while symbols are often used in e.g. keys of hash tables populated at runtime.

At a lower level, Rumble[1] seems to define keywords as symbols: I see a (define-record-type keyword (fields symbol)) that seems to indicate keywords are a thin wrapper over symbols. And symbols seem to be a primitive concept with #%string->symbol[2].

Is the "source code vs. runtime" notion a good way to distinguish them? Or are there deeper differences? (maybe the optimizer treats them differently somehow, or I also saw some primitives that deal with keywords so maybe the files I found aren't relevant) Or should we think of them as a syntax nicety, similar to how brackets and parentheses are different but interchangeable?

Is the definition I found the "true definition" of keywords in Racket? If not, where is that?

[1] racket/keyword.ss at 448b77a6629c68659e1360fbe9f9e1ecea078f9c · racket/racket · GitHub

[2] racket/symbol.ss at master · racket/racket · GitHub

1 Like

A correct/complete explanation is above my pay grade, but my attempt:

Keywords are used by macro expansion. The lambda macro is an example of using keywords for a "keyword arguments" feature. (lambda expands to a plain-lambda that knows nothing about keyword arguments.)

Unlike symbols, keywords are not expressions.

> #:keyword
; #%datum: keyword misused as an expression
;   at: #:keyword

For macros it's useful to be able to specify a bit of syntax that can't possibly be an indentifier. For example cond uses else as a magic symbol, but you could imagine instead using a keyword like #:else.

  • As a macro user, this makes the special syntax parts "pop" more clearly (even if the ergonomics of typing "#:` aren't amazing).

  • As a macro implementer, you don't need to worry about whether else has some binding, or remember to tell syntax-{case parse} that else should be treated as a literal (matched only for its symbolic value).

The syntax-parse system has a syntax class expr which matches everything except keywords. For example in your template you could use foo:expr to match any value (keywords aren't values). And you could use #:else to match exactly that keyword.


I feel like this explanation is at best impoverished, and possibly incorrect, but hopefully it will nudge people to correct it. :smile:

6 Likes

I think Racket uses the term keyword to mean a specific thing, as defined in the Guide: 3.7 Keywords

In lots of programming languages keyword is a synonym for ‘reserved words’ that are a fixed list of symbols that you cannot/must not/or should not redefine.

My understanding is that Racket, along with other schemes, doesn’t have that restriction, and you can redefine what symbols bind to. I believe lexical scope stops this from being a disaster in Racket, with parameters available for when you want to redefine what a symbol resolves to at runtime.

Edit. Just saw Greg’s response but I’ll leave this here anyway.

Adding to the other replies: the purpose of a keyword is to not be an expression.

For example, in (do-something #:verbose? #t), it's clear that #:verbose? makes the #t a keyword-based argument and not a positional argument, because a #:verbose? by itself is not an expression and therefore cannot be a first by-position argument to do-something. You can always quote a keyword to make it an expression, but a quoted keyword is syntactically different than the keyword by itself.

The non-expressionness of a keyword makes it useful in syntactic forms other than function calls, too. In a syntactic form like struct, keywords delimit optional subforms with little risk that a keyword intended to start a new subform will be accidentally treated as an argument of a preceding subform. Compare to define-record-type in R6RS, which relies more on parentheses to group identifier-tagged subforms with their arguments.

6 Likes

Also, keywords, symbols, and string constants are interned but 'manufactured strings' are not. Therefore:

> (eq? "foo" "foo")
#t
> (~a 'foo)
"foo"
> (eq? "foo" (~a 'foo))
#f
> (eq? 'foo 'foo)
#t
> (string->symbol "foo")
'foo
> (eq? 'foo (string->symbol "foo"))
#t
2 Likes