Repository: hylang/hy

Issue:
:foo keywords? 



Discussion:
I've been thinking about keywords, and how to implement them, and wrapping HYString sounds easiest, so in the compiler, we can add them to the special types, and use `self.special_types` there (assuming #32 gets merged in one form or the other), to transform `(:foo bar)` into `(index bar :foo)`. Everywhere where keywords are not in function position, they can act exactly as a HYString would.

This sounds pretty interesting, I actually like how easy it sounds, and I'm tempted by the prospect of having keywords (a lot).

It also seems like this'd work in the case of:

``` clojure
(import-from foo bar :as brah)
```

which is even more interesting (and almost enough to call this blocking #20)

Well, `(import-from)` would need to be smart to notice keywords, but yes.

Well, right :)

I do like the idea of subclassing HYString into HYKeyword. I'd say I'm +1.

(for the record, here's what we said on IRC:)

How should we handle (function-call foo :foo ":foo" "foo") to native Python?

Should :foo == ":foo" ? What does (set [:foo ":foo"]) do?

How can we do it without depending on Hy? Should we depend on Hy?

What I was thinking recently, is to modify my patch above to prefix `:keyword`s with some special char to avoid clashes. That's pretty much how [ClojureScript does it](https://github.com/clojure/clojurescript/blob/69741f2af76035e4adf1e6333c502095581b2179/src/cljs/cljs/core.cljs#L1093-L1095). This also means that strings will need a similar, inverted check.

@algernon  I don't hate the idea to be sure. Bummer we won't be able to support:

``` clojure
(:foo {:foo "bar"})
```

(well, until our macro system grows up anyway)

What does this mean for Python interop, @algernon ?

If something external is expecting `{"foo": "bar"}`, it might be getting `{"_hy_prefix_string_keyword_foo": "bar"}`

Do we care, even? :)

For python interop, there's two ways to do it (and forgive me, I'm not up to date on recent hy developments, so I'm not sure if either one is feasible): one is to just translate `:foo` to a prefixed string and be done with it, in which case python code will get a prefixed string indeed. The other way is to 'leak' a Hy object. In either case, the thing will be sufficiently unique - if one wants perfect python interop, then one will have to make sure not to leak advanced lisp stuff out, whatever form that stuff may be in.

As for keywords being able to look themselves up in collections... a macro system will not allow that exact form, as far as I see. But again, I'm quite far behind on Hy news, so I'm not sure how it works internally at the moment. I'll see if I can figure that out Soon(tm), in the hopes of finding a way to make clojure-style keywords supported.

For the record, there's some work-in-progress code on the [f/hy.lang.keyword](https://github.com/algernon/hy/commits/f/hy.lang.keyword) branch of my repo, at algernon/hy@50a404c5f7114cb0e390969602b763d358b692ee in particular. It needs to be rebased onto master, tests added, `compile_keyword` fixed to not hardcode casting to `unicode`, and a little code cleanup.

Otherwise I'm fairly happy with the implementation, will likely submit a pull request later today.


Resolving patch:
diff --git a/hy/compiler.py b/hy/compiler.py
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -29,6 +29,7 @@
 from hy.models.symbol import HySymbol
 from hy.models.list import HyList
 from hy.models.dict import HyDict
+from hy.models.keyword import HyKeyword
 
 from hy.util import flatten_literal_list
 
@@ -768,6 +769,11 @@ def compile_expression(self, expression):
 
             if expression[0].startswith("."):
                 return self.compile_dotted_expression(expression)
+        if isinstance(fn, HyKeyword):
+            new_expr = HyExpression(["get", expression[1], fn])
+            new_expr.start_line = expression.start_line
+            new_expr.start_column = expression.start_column
+            return self.compile_index_expression(new_expr)
 
         return ast.Call(func=self.compile(fn),
                         args=[self.compile(x) for x in expression[1:]],
@@ -927,6 +933,14 @@ def compile_string(self, string):
         return ast.Str(s=ast_str(string), lineno=string.start_line,
                        col_offset=string.start_column)
 
+    @builds(HyKeyword)
+    def compile_keyword(self, keyword):
+        _str_type = str
+        if sys.version_info[0] < 3:
+            _str_type = unicode
+        return ast.Str(s=_str_type(keyword), lineno=keyword.start_line,
+                       col_offset=keyword.start_column)
+
     @builds(HyDict)
     def compile_dict(self, m):
         keys = []
diff --git a/hy/lex/states.py b/hy/lex/states.py
--- a/hy/lex/states.py
+++ b/hy/lex/states.py
@@ -22,6 +22,7 @@
 from hy.models.integer import HyInteger
 from hy.models.symbol import HySymbol
 from hy.models.string import HyString
+from hy.models.keyword import HyKeyword
 from hy.models.dict import HyDict
 from hy.models.list import HyList
 
@@ -61,6 +62,9 @@ def _resolve_atom(obj):
     if obj in table:
         return HySymbol(table[obj])
 
+    if obj.startswith(":"):
+        return HyKeyword(obj)
+
     if obj.startswith("*") and obj.endswith("*") and obj != "*":
         obj = obj[1:-1].upper()
 
diff --git a/hy/models/keyword.py b/hy/models/keyword.py
new file mode 100644
--- /dev/null
+++ b/hy/models/keyword.py
@@ -0,0 +1,39 @@
+# Copyright (c) 2013 Gergely Nagy <algernon@madhouse-project.org>
+#
+# Permission is hereby granted, free of charge, to any person obtaining a
+# copy of this software and associated documentation files (the "Software"),
+# to deal in the Software without restriction, including without limitation
+# the rights to use, copy, modify, merge, publish, distribute, sublicense,
+# and/or sell copies of the Software, and to permit persons to whom the
+# Software is furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL
+# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+# DEALINGS IN THE SOFTWARE.
+
+from __future__ import unicode_literals
+from hy.models import HyObject
+import sys
+
+
+if sys.version_info[0] >= 3:
+    _str_type = str
+else:
+    _str_type = unicode
+
+
+class HyKeyword(HyObject, _str_type):
+    """Generic Hy Keyword object. It's either a ``str`` or a ``unicode``,
+    depending on the Python version.
+    """
+
+    def __new__(cls, value):
+        obj = _str_type.__new__(cls, "\uFDD0" + value)
+        return obj


Test patch:
diff --git a/tests/native_tests/language.hy b/tests/native_tests/language.hy
--- a/tests/native_tests/language.hy
+++ b/tests/native_tests/language.hy
@@ -510,6 +510,18 @@
                  (pass)
                  ((fn [] 1))))))
 
+(defn test-keyword []
+  "NATIVE: test if keywords are recognised"
+
+  (assert (= :foo :foo))
+  (assert (= (get {:foo "bar"} :foo) "bar"))
+  (assert (= (get {:bar "quux"} (get {:foo :bar} :foo)) "quux")))
+
+(defn test-keyword-clash []
+  "NATIVE: test that keywords do not clash with normal strings"
+
+  (assert (= (get {:foo "bar" ":foo" "quux"} :foo) "bar"))
+  (assert (= (get {:foo "bar" ":foo" "quux"} ":foo") "quux")))
 
 (defn test-eval []
   "NATIVE: test eval"