Repository: hylang/hy

Issue:
apply doesn't use shadowed operators
Now this is possible:

``` clj
(let [[op +]] (apply op ["a" "b" "c"]))
```

and also this:

``` clj
(reduce + ["a" "b" "c"])
```

However this is not:

``` clj
(apply + ["a" "b" "c"])
```

which yields `HyTypeError: compiling the application of`+' didn't return a function call, but `BinOp'`

@paultag <b>says</b>:

Since it defers back to compile expression, it'll defer to @builds("+"), which means we get a BinOp back. Which is *hilarious&. At the top there should be a self.compile call since it'll actually treat the first argument as the argument in the call position when it needs to be an  ast.Call.


Discussion:
Kodiologist: Oh great, Travis is showing that it segfaults on 3.3 and 3.4 for some reason. Probably because PEP 448 only made it into 3.5. I'll gate those tests appropriately and see what happens.
Kodiologist: Nope. I guess this won't be so easy. There were probably changes to the desired AST.

But please do take a look at this before I start hacking away at that.
gilch: Having written three of those four issues, I approve of the additional syntax. However, I didn't say anything about removing `apply`. But now that I think about it, if it's made redundant then it should be removed. It would certainly be easy to re-implement in terms of `#*` and `#**`, but when would we even *need* to?

To answer my own question, perhaps in macros and higher-order functions? Would we ever need the old `apply` in `->` or `doto`? Is there a non-reader syntax for these special forms, like `quote` is for `'`? Sometimes it's easier if you don't need the reader syntax when writing macros, at least in the case of `quote`. Our current `apply` doesn't work as a HOF anyway, but maybe it should be shadowed. I might need to think about this some more.

One other issue I take with the proposed implementation is how you lex the tokens. A tag macro can be any symbol, yet the tests demonstrate `(setv [a #*b c] "ghijklmno")` would unpack `b`. But what happens if you have a tag macro with symbol `*b`? Would it apply to `c` or just be unusable? 

I think whitespace should be required between a `#*` and a symbol. But it should be fine to put it directly before a list with no space `#*[...`, likewise, a `#**` would work on a dict display `#**{...`, but not before a symbol.

Kodiologist: > Is there a non-reader syntax for these special forms, like `quote` is for `'`? 

Yes, `#* foo` is just shorthand for `(unpack-iterable foo)`, and `#** foo` for `(unpack-mapping foo)`.

> A tag macro can be any symbol

Not if you want to be able to call it. Here are two examples:

    => (deftag "{b" [x] "hello")
    <function <lambda> at 0x7f579c0d2d08>
    => (setv b "foo")
    => (print #{b 1 2})
    {1, 2, 'foo'}
    => (deftag "[b" [x] "hello")
    <function <lambda> at 0x7f913d8e0048>
    => (print #[b 1 2])
    […] NameError: name '#' is not defined

So the effect of this change is that a tag macro's name can't begin with an asterisk, the same way it can't begin with `{` or `[`. Because such a macro would look like unpacking, it's a bad idea anyway.
Kodiologist: >  But it should be fine to put it directly before a list with no space `#*[...`, likewise, a `#**` would work on a dict display `#**{...`, but not before a symbol.

Beyond testing Hy itself, why would you construct a literal list or dictionary only to unpack it?
gilch: > Beyond testing Hy itself, why would you construct a literal list or dictionary only to unpack it?

I'm saying it should be valid syntactically, not that I had a use case in mind. But think about how this could interact with the threading macros `->` and `->>`. Could you thread in multiple arguments at once? Macros can do interesting things.

In the case of a dictionary, even in Python, you can use dictionary unpacking to put the kwarg name in a variable.
```
>>> def bar(*, a=None, b=None):
	print(a,b)

	
>>> foo = 'a'
>>> bar(**{foo: 1})
1 None
>>> foo = 'b'
>>> bar(**{foo: 1})
None 1
```
Kodiologist: > 

In the case of a dictionary, even in Python, you can use dictionary unpacking to put the kwarg name in a variable.

Oh, that's a good point.
gilch: > Not if you want to be able to call it.

I said any symbol, not any string. I meant the kind of symbol you can type in directly, without going through a `HySymbol` call to convert a string. Those can't contain things like `[` or `{`. See also #1117.

We do have a `*map` symbol in core. We also have the `*earmuffs*` convention. It's not that weird for a symbol to start with an asterisk in Lisp.
Kodiologist: > > Not if you want to be able to call it.
>
> I said any symbol, not any string. I meant the kind of symbol you can type in directly, without going through a `HySymbol` call to convert a string.

Okay, here's another counterexample:

    => (deftag "!a" [x] "hello")
    <function <lambda> at 0x7f83b0b8ef28>
    => #!a 3
    [Returns None]
    => (repr #!a 3)
      File "<stdin>", line 1, column 7

      (repr #!a 3)
            ^
    LexException: Ran into a HASHBANG where it wasn't expected.
gilch: > HASHBANG

Yikes! I'd call that one that a bug in the lexer. I think we were only using that for the shebang on the first line? We should be able to use `#!` as a tag macro elsewhere.
Kodiologist: So, to be clear, you want `#*b` to be parsed as calling a tag macro named `b` instead of the same way as `#* b`?
gilch: It seems much more consistent to me to implement `#*` and `#**` as tag macros that happen to expand into the new special forms `unpack-iterable`, and `unpack-mapping`, respectively, without messing with the current lexer at all. (I mean, besides fixing that HASHBANG thing in a separate issue.)
gilch: `#*` and `#**` could be implemented like this:
```Hy
(deftag * [form] `(unpack-iterable ~form))
(deftag ** [form] `(unpack-mapping ~form))
```
Then you don't need the special case in the compiler, and don't have to change the lexer. You still need the new special forms.

> So, to be clear, you want `#*b` to be parsed as calling a tag macro named `b` instead of the same way as `#* b`?

No, `#*b` should be a tag macro named `*b`. So `#*b c` calls the `*b` tag macro with argument `c`. But `#* b c` calls the `*` tag macro with argument `b`.

Kodiologist: All right, that makes sense.

Patch:
diff --git a/NEWS b/NEWS
index fe7ffb98..3bae0cae 100644
--- a/NEWS
+++ b/NEWS
@@ -2,6 +2,8 @@ Changes from 0.13.0
 
    [ Language Changes ]
    * `yield-from` is no longer supported under Python 2
+   * `apply` has been replaced with Python-style unpacking operators `#*` and
+     `#**` (e.g., `(f #* args #** kwargs)`)
    * Single-character "sharp macros" changed to "tag macros", which can have
      longer names
    * Periods are no longer allowed in keywords
diff --git a/docs/language/api.rst b/docs/language/api.rst
index 5241b3ab..3904e863 100644
--- a/docs/language/api.rst
+++ b/docs/language/api.rst
@@ -154,41 +154,6 @@ it appends it as the last argument. The following code demonstrates this:
     5 10
 
 
-apply
------
-
-``apply`` is used to apply an optional list of arguments and an
-optional dictionary of kwargs to a function. The symbol mangling
-transformations will be applied to all keys in the dictionary of
-kwargs, provided the dictionary and its keys are defined in-place.
-
-Usage: ``(apply fn-name [args] [kwargs])``
-
-Examples:
-
-.. code-block:: clj
-
-    (defn thunk []
-      "hy there")
-
-    (apply thunk)
-    ;=> "hy there"
-
-    (defn total-purchase [price amount &optional [fees 1.05] [vat 1.1]]
-      (* price amount fees vat))
-
-    (apply total-purchase [10 15])
-    ;=> 173.25
-
-    (apply total-purchase [10 15] {"vat" 1.05})
-    ;=> 165.375
-
-    (apply total-purchase [] {"price" 10 "amount" 15 "vat" 1.05})
-    ;=> 165.375
-
-    (apply total-purchase [] {:price 10 :amount 15 :vat 1.05})
-    ;=> 165.375
-
 and
 ---
 
@@ -596,8 +561,8 @@ Parameters may have the following keywords in front of them:
         parameter_1 1
         parameter_2 2
 
-        ; to avoid the mangling of '-' to '_', use apply:
-        => (apply print-parameters [] {"parameter-1" 1 "parameter-2" 2})
+        ; to avoid the mangling of '-' to '_', use unpacking:
+        => (print-parameters #** {"parameter-1" 1 "parameter-2" 2})
         parameter-1 1
         parameter-2 2
 
@@ -634,19 +599,19 @@ Parameters may have the following keywords in front of them:
 
     .. code-block:: clj
 
-        => (defn compare [a b &kwonly keyfn [reverse false]]
+        => (defn compare [a b &kwonly keyfn [reverse False]]
         ...  (setv result (keyfn a b))
         ...  (if (not reverse)
         ...    result
         ...    (- result)))
-        => (apply compare ["lisp" "python"]
-        ...        {"keyfn" (fn [x y]
-        ...                   (reduce - (map (fn [s] (ord (first s))) [x y])))})
+        => (compare "lisp" "python"
+        ...         :keyfn (fn [x y]
+        ...                  (reduce - (map (fn [s] (ord (first s))) [x y]))))
         -4
-        => (apply compare ["lisp" "python"]
-        ...        {"keyfn" (fn [x y]
+        => (compare "lisp" "python"
+        ...         :keyfn (fn [x y]
         ...                   (reduce - (map (fn [s] (ord (first s))) [x y])))
-        ...         "reverse" True})
+        ...         :reverse True)
         4
 
     .. code-block:: python
@@ -1576,6 +1541,49 @@ the given conditional is ``False``. The following shows the expansion of this ma
       (do statement))
 
 
+unpack-iterable, unpack-mapping
+-------------------------------
+
+``unpack-iterable`` and ``unpack-mapping`` allow an iterable or mapping
+object (respectively) to provide positional or keywords arguments
+(respectively) to a function.
+
+.. code-block:: clj
+
+    => (defn f [a b c d] [a b c d])
+    => (f (unpack-iterable [1 2]) (unpack-mapping {"c" 3 "d" 4}))
+    [1, 2, 3, 4]
+
+``unpack-iterable`` is usually written with the shorthand ``#*``, and
+``unpack-mapping`` with ``#**``.
+
+.. code-block:: clj
+
+    => (f #* [1 2] #** {"c" 3 "d" 4})
+    [1, 2, 3, 4]
+
+With Python 3, you can unpack in an assignment list (:pep:`3132`).
+
+.. code-block:: clj
+
+    => (setv [a #* b c] [1 2 3 4 5])
+    => [a b c]
+    [1, [2, 3, 4], 5]
+
+With Python 3.5 or greater, unpacking is allowed in more contexts than just
+function calls, and you can unpack more than once in the same expression
+(:pep:`448`).
+
+.. code-block:: clj
+
+    => [#* [1 2] #* [3 4]]
+    [1, 2, 3, 4]
+    => {#** {1 2} #** {3 4}}
+    {1: 2, 3: 4}
+    => (f #* [1] #* [2] #** {"c" 3} #** {"d" 4})
+    [1, 2, 3, 4]
+
+
 unquote
 -------
 
diff --git a/docs/language/core.rst b/docs/language/core.rst
index 21c111b4..4983af9b 100644
--- a/docs/language/core.rst
+++ b/docs/language/core.rst
@@ -1216,9 +1216,9 @@ if *from-file* ends before a complete expression can be parsed.
 
    => (import io)
    => (def buffer (io.StringIO "(+ 2 2)\n(- 2 1)"))
-   => (eval (apply read [] {"from_file" buffer}))
+   => (eval (read :from_file buffer))
    4
-   => (eval (apply read [] {"from_file" buffer}))
+   => (eval (read :from_file buffer))
    1
 
    => ; assuming "example.hy" contains:
diff --git a/docs/tutorial.rst b/docs/tutorial.rst
index ceeab775..3883abe6 100644
--- a/docs/tutorial.rst
+++ b/docs/tutorial.rst
@@ -423,8 +423,7 @@ The same thing in Hy::
   => (optional-arg 1 2 3 4)
   [1 2 3 4]
 
-If you're running a version of Hy past 0.10.1 (eg, git master),
-there's also a nice new keyword argument syntax::
+You can call keyword arguments like this::
 
   => (optional-arg :keyword1 1
   ...              :pos2 2
@@ -432,21 +431,13 @@ there's also a nice new keyword argument syntax::
   ...              :keyword2 4)
   [3, 2, 1, 4]
 
-Otherwise, you can always use `apply`.  But what's `apply`?
-
-Are you familiar with passing in `*args` and `**kwargs` in Python?::
-
-  >>> args = [1 2]
-  >>> kwargs = {"keyword2": 3
-  ...           "keyword1": 4}
-  >>> optional_arg(*args, **kwargs)
-
-We can reproduce this with `apply`::
+You can unpack arguments with the syntax ``#* args`` and ``#** kwargs``,
+similar to `*args` and `**kwargs` in Python::
 
   => (setv args [1 2])
   => (setv kwargs {"keyword2" 3
   ...              "keyword1" 4})
-  => (apply optional-arg args kwargs)
+  => (optional-arg #* args #** kwargs)
   [1, 2, 4, 3]
 
 There's also a dictionary-style keyword arguments construction that
@@ -460,7 +451,7 @@ looks like:
 The difference here is that since it's a dictionary, you can't rely on
 any specific ordering to the arguments.
 
-Hy also supports ``*args`` and ``**kwargs``.  In Python::
+Hy also supports ``*args`` and ``**kwargs`` in parameter lists.  In Python::
 
   def some_func(foo, bar, *args, **kwargs):
     import pprint
diff --git a/hy/compiler.py b/hy/compiler.py
index b9903324..5fd073fd 100755
--- a/hy/compiler.py
+++ b/hy/compiler.py
@@ -359,6 +359,13 @@ def checkargs(exact=None, min=None, max=None, even=None, multiple=None):
     return _dec
 
 
+def is_unpack(kind, x):
+    return (isinstance(x, HyExpression)
+            and len(x) > 0
+            and isinstance(x[0], HySymbol)
+            and x[0] == "unpack_" + kind)
+
+
 class HyASTCompiler(object):
 
     def __init__(self, module_name):
@@ -441,7 +448,8 @@ class HyASTCompiler(object):
 
         raise HyCompileError(Exception("Unknown type: `%s'" % _type))
 
-    def _compile_collect(self, exprs, with_kwargs=False):
+    def _compile_collect(self, exprs, with_kwargs=False, dict_display=False,
+                         oldpy_unpack=False):
         """Collect the expression contexts from a list of compiled expression.
 
         This returns a list of the expression contexts, and the sum of the
@@ -451,10 +459,39 @@ class HyASTCompiler(object):
         compiled_exprs = []
         ret = Result()
         keywords = []
+        oldpy_starargs = None
+        oldpy_kwargs = None
 
         exprs_iter = iter(exprs)
         for expr in exprs_iter:
-            if with_kwargs and isinstance(expr, HyKeyword):
+
+            if not PY35 and oldpy_unpack and is_unpack("iterable", expr):
+                if oldpy_starargs:
+                    raise HyTypeError(expr, "Pythons < 3.5 allow only one "
+                                            "`unpack-iterable` per call")
+                oldpy_starargs = self.compile(expr[1])
+                ret += oldpy_starargs
+                oldpy_starargs = oldpy_starargs.force_expr
+
+            elif is_unpack("mapping", expr):
+                ret += self.compile(expr[1])
+                if PY35:
+                    if dict_display:
+                        compiled_exprs.append(None)
+                        compiled_exprs.append(ret.force_expr)
+                    elif with_kwargs:
+                        keywords.append(ast.keyword(
+                            arg=None,
+                            value=ret.force_expr,
+                            lineno=expr.start_line,
+                            col_offset=expr.start_column))
+                elif oldpy_unpack:
+                    if oldpy_kwargs:
+                        raise HyTypeError(expr, "Pythons < 3.5 allow only one "
+                                                "`unpack-mapping` per call")
+                    oldpy_kwargs = ret.force_expr
+
+            elif with_kwargs and isinstance(expr, HyKeyword):
                 try:
                     value = next(exprs_iter)
                 except StopIteration:
@@ -474,11 +511,15 @@ class HyASTCompiler(object):
                                             value=compiled_value.force_expr,
                                             lineno=expr.start_line,
                                             col_offset=expr.start_column))
+
             else:
                 ret += self.compile(expr)
                 compiled_exprs.append(ret.force_expr)
 
-        return compiled_exprs, ret, keywords
+        if oldpy_unpack:
+            return compiled_exprs, ret, keywords, oldpy_starargs, oldpy_kwargs
+        else:
+            return compiled_exprs, ret, keywords
 
     def _compile_branch(self, exprs):
         return _branch(self.compile(expr) for expr in exprs)
@@ -610,6 +651,9 @@ class HyASTCompiler(object):
             new_name = ast.Subscript(value=name.value, slice=name.slice)
         elif isinstance(name, ast.Attribute):
             new_name = ast.Attribute(value=name.value, attr=name.attr)
+        elif PY3 and isinstance(name, ast.Starred):
+            new_name = ast.Starred(
+                value=self._storeize(expr, name.value, func))
         else:
             raise HyTypeError(expr,
                               "Can't assign or delete a %s" %
@@ -717,6 +761,23 @@ class HyASTCompiler(object):
         raise HyTypeError(expr,
                           "`%s' can't be used at the top-level" % expr[0])
 
+    @builds("unpack_iterable")
+    @checkargs(exact=1)
+    def compile_unpack_iterable(self, expr):
+        if not PY3:
+            raise HyTypeError(expr, "`unpack-iterable` isn't allowed here")
+        ret = self.compile(expr[1])
+        ret += ast.Starred(value=ret.force_expr,
+                           lineno=expr.start_line,
+                           col_offset=expr.start_column,
+                           ctx=ast.Load())
+        return ret
+
+    @builds("unpack_mapping")
+    @checkargs(exact=1)
+    def compile_unpack_mapping(self, expr):
+        raise HyTypeError(expr, "`unpack-mapping` isn't allowed here")
+
     @builds("do")
     def compile_do(self, expression):
         expression.pop(0)
@@ -1526,115 +1587,6 @@ class HyASTCompiler(object):
             generators=expr.generators)
         return ret
 
-    @builds("apply")
-    @checkargs(min=1, max=3)
-    def compile_apply_expression(self, expr):
-        expr.pop(0)  # apply
-
-        ret = Result()
-
-        fun = expr.pop(0)
-
-        # We actually defer the compilation of the function call to
-        # @builds(HyExpression), allowing us to work on method calls
-        call = HyExpression([fun]).replace(fun)
-
-        if isinstance(fun, HySymbol) and fun.startswith("."):
-            # (apply .foo lst) needs to work as lst[0].foo(*lst[1:])
-            if not expr:
-                raise HyTypeError(
-                    expr, "apply of a method needs to have an argument"
-                )
-
-            # We need to grab the arguments, and split them.
-
-            # Assign them to a variable if they're not one already
-            if type(expr[0]) == HyList:
-                if len(expr[0]) == 0:
-                    raise HyTypeError(
-                        expr, "apply of a method needs to have an argument"
-                    )
-                call.append(expr[0].pop(0))
-            else:
-                if isinstance(expr[0], HySymbol):
-                    tempvar = expr[0]
-                else:
-                    tempvar = HySymbol(self.get_anon_var()).replace(expr[0])
-                    assignment = HyExpression(
-                        [HySymbol("setv"), tempvar, expr[0]]
-                    ).replace(expr[0])
-
-                    # and add the assignment to our result
-                    ret += self.compile(assignment)
-
-                # The first argument is the object on which to call the method
-                # So we translate (apply .foo args) to (.foo (get args 0))
-                call.append(HyExpression(
-                    [HySymbol("get"), tempvar, HyInteger(0)]
-                ).replace(tempvar))
-
-                # We then pass the other arguments to the function
-                expr[0] = HyExpression(
-                    [HySymbol("cut"), tempvar, HyInteger(1)]
-                ).replace(expr[0])
-
-        ret += self.compile(call)
-
-        if not isinstance(ret.expr, ast.Call):
-            raise HyTypeError(
-                fun, "compiling the application of `{}' didn't return a "
-                "function call, but `{}'".format(fun, type(ret.expr).__name__)
-            )
-        if ret.expr.starargs or ret.expr.kwargs:
-            raise HyTypeError(
-                expr, "compiling the function application returned a function "
-                "call with arguments"
-            )
-
-        if expr:
-            stargs = expr.pop(0)
-            if stargs is not None:
-                stargs = self.compile(stargs)
-                if PY35:
-                    stargs_expr = stargs.force_expr
-                    ret.expr.args.append(
-                        ast.Starred(stargs_expr, ast.Load(),
-                                    lineno=stargs_expr.lineno,
-                                    col_offset=stargs_expr.col_offset)
-                    )
-                else:
-                    ret.expr.starargs = stargs.force_expr
-                ret = stargs + ret
-
-        if expr:
-            kwargs = expr.pop(0)
-            if isinstance(kwargs, HyDict):
-                new_kwargs = []
-                for k, v in kwargs.items():
-                    if isinstance(k, HySymbol):
-                        pass
-                    elif isinstance(k, HyString):
-                        k = HyString(hy_symbol_mangle(str_type(k))).replace(k)
-                    elif isinstance(k, HyKeyword):
-                        sym = hy_symbol_mangle(str_type(k)[2:])
-                        k = HyString(sym).replace(k)
-                    new_kwargs += [k, v]
-                kwargs = HyDict(new_kwargs).replace(kwargs)
-
-            kwargs = self.compile(kwargs)
-            if PY35:
-                kwargs_expr = kwargs.force_expr
-                ret.expr.keywords.append(
-                    ast.keyword(None, kwargs_expr,
-                                lineno=kwargs_expr.lineno,
-                                col_offset=kwargs_expr.col_offset)
-                )
-            else:
-                ret.expr.kwargs = kwargs.force_expr
-            ret = kwargs + ret
-
-        return ret
-
     @builds("not")
     @builds("~")
     @checkargs(1)
@@ -2001,9 +1953,15 @@ class HyASTCompiler(object):
             return self._compile_keyword_call(expression)
 
         if isinstance(fn, HySymbol):
-            ret = self.compile_atom(fn, expression)
-            if ret:
-                return ret
+            # First check if `fn` is a special form, unless it has an
+            # `unpack_iterable` in it, since Python's operators (`+`,
+            # etc.) can't unpack. An exception to this exception is that
+            # tuple literals (`,`) can unpack.
+            if fn == "," or not (
+                    any(is_unpack("iterable", x) for x in expression[1:])):
+                ret = self.compile_atom(fn, expression)
+                if ret:
+                    return ret
 
             if fn.startswith("."):
                 # (.split "test test") -> "test test".split()
@@ -2054,14 +2012,14 @@ class HyASTCompiler(object):
         else:
             with_kwargs = True
 
-        args, ret, kwargs = self._compile_collect(expression[1:],
-                                                  with_kwargs)
+        args, ret, keywords, oldpy_starargs, oldpy_kwargs = self._compile_collect(
+            expression[1:], with_kwargs, oldpy_unpack=True)
 
         ret += ast.Call(func=func.expr,
                         args=args,
-                        keywords=kwargs,
-                        starargs=None,
-                        kwargs=None,
+                        keywords=keywords,
+                        starargs=oldpy_starargs,
+                        kwargs=oldpy_kwargs,
                         lineno=expression.start_line,
                         col_offset=expression.start_column)
 
@@ -2583,7 +2541,7 @@ class HyASTCompiler(object):
 
     @builds(HyDict)
     def compile_dict(self, m):
-        keyvalues, ret, _ = self._compile_collect(m)
+        keyvalues, ret, _ = self._compile_collect(m, dict_display=True)
 
         ret += ast.Dict(lineno=m.start_line,
                         col_offset=m.start_column,
diff --git a/hy/contrib/hy_repr.hy b/hy/contrib/hy_repr.hy
index 20535044..ea67efc4 100644
--- a/hy/contrib/hy_repr.hy
+++ b/hy/contrib/hy_repr.hy
@@ -37,6 +37,10 @@
               (+ "~" (f (second x) q))
             (= (first x) 'unquote_splice)
               (+ "~@" (f (second x) q))
+            (= (first x) 'unpack_iterable)
+              (+ "#* " (f (second x) q))
+            (= (first x) 'unpack_mapping)
+              (+ "#** " (f (second x) q))
             ; else
               (+ "(" (catted) ")"))
           (+ "(" (catted) ")"))
diff --git a/hy/contrib/loop.hy b/hy/contrib/loop.hy
index 9ab9eb8d..34172569 100644
--- a/hy/contrib/loop.hy
+++ b/hy/contrib/loop.hy
@@ -27,7 +27,7 @@
     (when (not (first active))
       (assoc active 0 True)
       (while (> (len accumulated) 0)
-        (setv result (apply f (.pop accumulated))))
+        (setv result (f #* (.pop accumulated))))
       (assoc active 0 False)
       result)))
 
diff --git a/hy/contrib/multi.hy b/hy/contrib/multi.hy
index 639c96ad..8c894861 100644
--- a/hy/contrib/multi.hy
+++ b/hy/contrib/multi.hy
@@ -29,7 +29,7 @@
     (setv output None)
     (for [[i f] (.items (get self._fns self.f.__module__ self.f.__name__))]
       (when (.fn? self i args kwargs)
-        (setv output (apply f args kwargs))
+        (setv output (f #* args #** kwargs))
         (break)))
     (if output
       output
@@ -37,10 +37,10 @@
 
 (defn multi-decorator [dispatch-fn]
   (setv inner (fn [&rest args &kwargs kwargs]
-                (setv dispatch-key (apply dispatch-fn args kwargs))
+                (setv dispatch-key (dispatch-fn #* args #** kwargs))
                 (if (in dispatch-key inner.--multi--)
-                  (apply (get inner.--multi-- dispatch-key) args kwargs)
-                  (apply inner.--multi-default-- args kwargs))))
+                  ((get inner.--multi-- dispatch-key) #* args #** kwargs)
+                  (inner.--multi-default-- #* args #** kwargs))))
   (setv inner.--multi-- {})
   (setv inner.--doc-- dispatch-fn.--doc--)
   (setv inner.--multi-default-- (fn [&rest args &kwargs kwargs] None))
diff --git a/hy/contrib/profile.hy b/hy/contrib/profile.hy
index afee3b07..42ef1a48 100644
--- a/hy/contrib/profile.hy
+++ b/hy/contrib/profile.hy
@@ -10,7 +10,7 @@
   `(do
      (import [pycallgraph [PyCallGraph]]
              [pycallgraph.output [GraphvizOutput]])
-     (with* [(apply PyCallGraph [] {"output" (GraphvizOutput)})]
+     (with* [(PyCallGraph :output (GraphvizOutput)))]
            ~@body)))
 
 
@@ -29,6 +29,6 @@
      (.disable ~g!hy-pr)
      (setv ~g!hy-s (StringIO))
      (setv ~g!hy-ps
-           (.sort-stats (apply pstats.Stats [~g!hy-pr] {"stream" ~g!hy-s})))
+           (.sort-stats (pstats.Stats ~g!hy-pr :stream ~g!hy-s)))
      (.print-stats ~g!hy-ps)
      (print (.getvalue ~g!hy-s))))
diff --git a/hy/core/language.hy b/hy/core/language.hy
index 6bcdb793..9c29d276 100644
--- a/hy/core/language.hy
+++ b/hy/core/language.hy
@@ -37,7 +37,7 @@
                 first-f (next rfs)
                 fs (tuple rfs))
           (fn [&rest args &kwargs kwargs]
-            (setv res (apply first-f args kwargs))
+            (setv res (first-f #* args #** kwargs))
             (for* [f fs]
               (setv res (f res)))
             res))))
@@ -45,7 +45,7 @@
 (defn complement [f]
   "Create a function that reverses truth value of another function"
   (fn [&rest args &kwargs kwargs]
-    (not (apply f args kwargs))))
+    (not (f #* args #** kwargs))))
 
 (defn cons [a b]
   "Return a fresh cons cell with car = a and cdr = b"
@@ -160,8 +160,8 @@
 (defn drop-last [n coll]
   "Return a sequence of all but the last n elements in coll."
   (setv iters (tee coll))
-  (map first (apply zip [(get iters 0)
-                         (drop n (get iters 1))])))
+  (map first (zip #* [(get iters 0)
+                      (drop n (get iters 1))])))
 
 (defn empty? [coll]
   "Return True if `coll` is empty"
@@ -250,7 +250,7 @@
 
 (defn interleave [&rest seqs]
   "Return an iterable of the first item in each of seqs, then the second etc."
-  (chain.from-iterable (apply zip seqs)))
+  (chain.from-iterable (zip #* seqs)))
 
 (defn interpose [item seq]
   "Return an iterable of the elements of seq separated by item"
@@ -275,7 +275,7 @@
    set of arguments and collects the results into a list."
   (setv fs (cons f fs))
   (fn [&rest args &kwargs kwargs]
-    (list-comp (apply f args kwargs) [f fs])))
+    (list-comp (f #* args #** kwargs) [f fs])))
 
 (defn last [coll]
   "Return last item from `coll`"
@@ -285,7 +285,7 @@
   "Return a dotted list construed from the elements of the argument"
   (if (not tl)
     hd
-    (cons hd (apply list* tl))))
+    (cons hd (list* #* tl))))
 
 (defn macroexpand [form]
   "Return the full macro expansion of form"
@@ -350,8 +350,8 @@
    slices (genexpr (islice (get coll-clones start) start None step)
                    [start (range n)]))
   (if (is fillvalue -sentinel)
-    (apply zip slices)
-    (apply zip-longest slices {"fillvalue" fillvalue})))
+    (zip #* slices)
+    (zip-longest #* slices :fillvalue fillvalue)))
 
 (defn pos? [n]
   "Return true if n is > 0"
diff --git a/hy/core/macros.hy b/hy/core/macros.hy
index 27eadf63..82cf3ae4 100644
--- a/hy/core/macros.hy
+++ b/hy/core/macros.hy
@@ -207,7 +207,7 @@
   (setv retval (gensym))
   `(when (= --name-- "__main__")
      (import sys)
-     (setv ~retval (apply (fn [~@args] ~@body) sys.argv))
+     (setv ~retval ((fn [~@args] ~@body) #* sys.argv))
      (if (integer? ~retval)
        (sys.exit ~retval))))
 
diff --git a/hy/lex/lexer.py b/hy/lex/lexer.py
index c6b5636b..9779eda7 100755
--- a/hy/lex/lexer.py
+++ b/hy/lex/lexer.py
@@ -26,6 +26,7 @@ lg.add('QUASIQUOTE', r'`%s' % end_quote)
 lg.add('UNQUOTESPLICE', r'~@%s' % end_quote)
 lg.add('UNQUOTE', r'~%s' % end_quote)
 lg.add('HASHBANG', r'#!.*[^\r\n]')
+lg.add('HASHSTARS', r'#\*+')
 lg.add('HASHOTHER', r'#%s' % identifier)
 
 # A regexp which matches incomplete strings, used to support
diff --git a/hy/lex/parser.py b/hy/lex/parser.py
index 1be896b2..d60cec5a 100755
--- a/hy/lex/parser.py
+++ b/hy/lex/parser.py
@@ -197,6 +197,22 @@ def term_unquote_splice(p):
     return HyExpression([HySymbol("unquote_splice"), p[1]])
 
 
+@pg.production("term : HASHSTARS term")
+@set_quote_boundaries
+def term_hashstars(p):
+    n_stars = len(p[0].getstr()[1:])
+    if n_stars == 1:
+        sym = "unpack_iterable"
+    elif n_stars == 2:
+        sym = "unpack_mapping"
+    else:
+        raise LexException(
+            "Too many stars in `#*` construct (if you want to unpack a symbol "
+            "beginning with a star, separate it with whitespace)",
+            p[0].source_pos.lineno, p[0].source_pos.colno)
+    return HyExpression([HySymbol(sym), p[1]])
+
+
 @pg.production("term : HASHOTHER term")
 @set_quote_boundaries
 def hash_other(p):


Test patch:
diff --git a/conftest.py b/conftest.py
index 72fa5530..0735cf74 100644
--- a/conftest.py
+++ b/conftest.py
@@ -1,12 +1,13 @@
 import _pytest
 import hy
-from hy._compat import PY3
+from hy._compat import PY3, PY35
 
 def pytest_collect_file(parent, path):
     if (path.ext == ".hy"
             and "/tests/native_tests/" in path.dirname + "/"
             and path.basename != "__init__.hy"
-            and not ("py3_only" in path.basename and not PY3)):
+            and not ("py3_only" in path.basename and not PY3)
+            and not ("py35_only" in path.basename and not PY35)):
         m = _pytest.python.pytest_pycollect_makemodule(path, parent)
         # Spoof the module name to avoid hitting an assertion in pytest.
         m.name = m.name[:-len(".hy")] + ".py"
diff --git a/tests/native_tests/contrib/hy_repr.hy b/tests/native_tests/contrib/hy_repr.hy
index 9bbdd095..a9c04755 100644
--- a/tests/native_tests/contrib/hy_repr.hy
+++ b/tests/native_tests/contrib/hy_repr.hy
@@ -28,8 +28,8 @@
     [1 2 3] (, 1 2 3) #{1 2 3} (frozenset #{1 2 3})
     '[1 2 3] '(, 1 2 3) '#{1 2 3} '(frozenset #{1 2 3})
     {"a" 1 "b" 2 "a" 3} '{"a" 1 "b" 2 "a" 3}
-    [1 [2 3] (, 4 (, 'mysymbol :mykeyword)) {"a" b"hello"}]
-    '[1 [2 3] (, 4 (, mysymbol :mykeyword)) {"a" b"hello"}]])
+    [1 [2 3] (, 4 (, 'mysymbol :mykeyword)) {"a" b"hello"} '(f #* a #** b)]
+    '[1 [2 3] (, 4 (, mysymbol :mykeyword)) {"a" b"hello"} (f #* a #** b)]])
   (for [original-val values]
     (setv evaled (eval (read-str (hy-repr original-val))))
     (assert (= evaled original-val))
@@ -59,7 +59,8 @@
     "{1 20}"
     "'{1 10 1 20}"
     "'asymbol"
-    ":akeyword"])
+    ":akeyword"
+    "'(f #* args #** kwargs)"])
   (for [original-str strs]
     (setv rep (hy-repr (eval (read-str original-str))))
     (assert (= rep original-str))))
diff --git a/tests/native_tests/contrib/multi.hy b/tests/native_tests/contrib/multi.hy
index 90c85dcc..9b0294ca 100644
--- a/tests/native_tests/contrib/multi.hy
+++ b/tests/native_tests/contrib/multi.hy
@@ -95,9 +95,9 @@
     ([&optional [a "nop"] [b "p"]] (+ a b)))
    
   (assert (= (fun 1) 1))
-  (assert (= (apply fun [] {"a" "t"}) "t"))
-  (assert (= (apply fun ["hello "] {"b" "world"}) "hello world"))
-  (assert (= (apply fun [] {"a" "hello " "b" "world"}) "hello world")))
+  (assert (= (fun :a "t") "t"))
+  (assert (= (fun "hello " :b "world") "hello world"))
+  (assert (= (fun :a "hello " :b "world") "hello world")))
 
 
 (defn test-docs []
diff --git a/tests/native_tests/language.hy b/tests/native_tests/language.hy
index cbef01e6..51e221fd 100644
--- a/tests/native_tests/language.hy
+++ b/tests/native_tests/language.hy
@@ -369,37 +369,25 @@
   (assert (is (isfile ".") False)))
 
 
+(defn test-star-unpacking []
+  ; Python 3-only forms of unpacking are in py3_only_tests.hy
+  (setv l [1 2 3])
+  (setv d {"a" "x" "b" "y"})
+  (defn fun [&optional x1 x2 x3 x4 a b c] [x1 x2 x3 x4 a b c])
+  (assert (= (fun 5 #* l) [5 1 2 3 None None None]))
+  (assert (= (+ #* l) 6))
+  (assert (= (fun 5 #** d) [5 None None None "x" "y" None]))
+  (assert (= (fun 5 #* l #** d) [5 1 2 3 "x" "y" None])))
+
+
+
 (defn test-kwargs []
   "NATIVE: test kwargs things."
-  (assert (= (apply kwtest [] {"one" "two"}) {"one" "two"}))
+  (assert (= (kwtest :one "two") {"one" "two"}))
   (setv mydict {"one" "three"})
-  (assert (= (apply kwtest [] mydict) mydict))
-  (assert (= (apply kwtest [] ((fn [] {"one" "two"}))) {"one" "two"})))
-
-
-(defn test-apply []
-  "NATIVE: test working with args and functions"
-  (defn sumit [a b c] (+ a b c))
-  (assert (= (apply sumit [1] {"b" 2 "c" 3}) 6))
-  (assert (= (apply sumit [1 2 2]) 5))
-  (assert (= (apply sumit [] {"a" 1 "b" 1 "c" 2}) 4))
-  (assert (= (apply sumit ((fn [] [1 1])) {"c" 1}) 3))
-  (defn noargs [] [1 2 3])
-  (assert (= (apply noargs) [1 2 3]))
-  (defn sumit-mangle [an-a a-b a-c a-d] (+ an-a a-b a-c a-d))
-  (def Z "a_d")
-  (assert (= (apply sumit-mangle [] {"an-a" 1 :a-b 2 'a-c 3 Z 4}) 10)))
-
-
-(defn test-apply-with-methods []
-  "NATIVE: test apply to call a method"
-  (setv str "foo {bar}")
-  (assert (= (apply .format [str] {"bar" "baz"})
-             (apply .format ["foo {0}" "baz"])
-             "foo baz"))
-  (setv lst ["a {0} {1} {foo} {bar}" "b" "c"])
-  (assert (= (apply .format lst {"foo" "d" "bar" "e"})
-             "a b c d e")))
+  (assert (= (kwtest #** mydict) mydict))
+  (assert (= (kwtest #** ((fn [] {"one" "two"}))) {"one" "two"})))
+
 
 
 (defn test-dotted []
@@ -418,20 +406,20 @@
   (assert (= (.meth m) "meth"))
   (assert (= (.meth m "foo" "bar") "meth foo bar"))
   (assert (= (.meth :b "1" :a "2" m "foo" "bar") "meth foo bar 2 1"))
-  (assert (= (apply .meth [m "foo" "bar"]) "meth foo bar"))
+  (assert (= (.meth m #* ["foo" "bar"]) "meth foo bar"))
 
   (setv x.p m)
   (assert (= (.p.meth x) "meth"))
   (assert (= (.p.meth x "foo" "bar") "meth foo bar"))
   (assert (= (.p.meth :b "1" :a "2" x "foo" "bar") "meth foo bar 2 1"))
-  (assert (= (apply .p.meth [x "foo" "bar"]) "meth foo bar"))
+  (assert (= (.p.meth x #* ["foo" "bar"]) "meth foo bar"))
 
   (setv x.a (X))
   (setv x.a.b m)
   (assert (= (.a.b.meth x) "meth"))
   (assert (= (.a.b.meth x "foo" "bar") "meth foo bar"))
   (assert (= (.a.b.meth :b "1" :a "2" x "foo" "bar") "meth foo bar 2 1"))
-  (assert (= (apply .a.b.meth [x "foo" "bar"]) "meth foo bar"))
+  (assert (= (.a.b.meth x #* ["foo" "bar"]) "meth foo bar"))
 
   (assert (is (.isdigit :foo) False)))
 
@@ -1173,8 +1161,8 @@
   "NATIVE: test &key function arguments"
   (defn foo [&key {"a" None "b" 1}] [a b])
   (assert (= (foo) [None 1]))
-  (assert (= (apply foo [] {"a" 2}) [2 1]))
-  (assert (= (apply foo [] {"b" 42}) [None 42])))
+  (assert (= (foo :a 2) [2 1]))
+  (assert (= (foo :b 42) [None 42])))
 
 
 (defn test-optional-arguments []
diff --git a/tests/native_tests/native_macros.hy b/tests/native_tests/native_macros.hy
index 3a0d4287..d3efcb6e 100644
--- a/tests/native_tests/native_macros.hy
+++ b/tests/native_tests/native_macros.hy
@@ -84,12 +84,12 @@
   "NATIVE: test macro calling a plain function"
   (assert (= 3 (bar 1 2))))
 
-(defn test-optional-and-apply-in-macro []
+(defn test-optional-and-unpacking-in-macro []
   ; https://github.com/hylang/hy/issues/1154
   (defn f [&rest args]
     (+ "f:" (repr args)))
   (defmacro mac [&optional x]
-   `(apply f [~x]))
+   `(f #* [~x]))
   (assert (= (mac) "f:(None,)")))
 
 (defn test-midtree-yield []
diff --git a/tests/native_tests/operators.hy b/tests/native_tests/operators.hy
index b49c754a..be40cc5a 100644
--- a/tests/native_tests/operators.hy
+++ b/tests/native_tests/operators.hy
@@ -288,8 +288,3 @@
   (assert (is (f 3 [1 2]) (!= f-name "in")))
   (assert (is (f 2 [1 2]) (= f-name "in")))
   (forbid (f 2 [1 2] [3 4])))
-
-#@(pytest.mark.xfail
-(defn test-apply-op []
-  ; https://github.com/hylang/hy/issues/647
-  (assert (= (eval '(apply + ["a" "b" "c"])) "abc"))))
diff --git a/tests/native_tests/py35_only_tests.hy b/tests/native_tests/py35_only_tests.hy
new file mode 100644
index 00000000..716a9f80
--- /dev/null
+++ b/tests/native_tests/py35_only_tests.hy
@@ -0,0 +1,26 @@
+;; Copyright 2017 the authors.
+;; This file is part of Hy, which is free software licensed under the Expat
+;; license. See the LICENSE.
+
+;; Tests where the emitted code relies on Python ≥3.5.
+;; conftest.py skips this file when running on Python <3.5.
+
+
+(defn test-unpacking-pep448-1star []
+  (setv l [1 2 3])
+  (setv p [4 5])
+  (assert (= ["a" #*l "b" #*p #*l] ["a" 1 2 3 "b" 4 5 1 2 3]))
+  (assert (= (, "a" #*l "b" #*p #*l) (, "a" 1 2 3 "b" 4 5 1 2 3)))
+  (assert (= #{"a" #*l "b" #*p #*l} #{"a" "b" 1 2 3 4 5}))
+  (defn f [&rest args] args)
+  (assert (= (f "a" #*l "b" #*p #*l) (, "a" 1 2 3 "b" 4 5 1 2 3)))
+  (assert (= (+ #*l #*p) 15))
+  (assert (= (and #*l) 3)))
+
+
+(defn test-unpacking-pep448-2star []
+  (setv d1 {"a" 1 "b" 2})
+  (setv d2 {"c" 3 "d" 4})
+  (assert (= {1 "x" #**d1 #**d2 2 "y"} {"a" 1 "b" 2 "c" 3 "d" 4 1 "x" 2 "y"}))
+  (defn fun [&optional a b c d e f] [a b c d e f])
+  (assert (= (fun #**d1 :e "eee" #**d2) [1 2 3 4 "eee" None])))
diff --git a/tests/native_tests/py3_only_tests.hy b/tests/native_tests/py3_only_tests.hy
index b376f6cd..84b8053b 100644
--- a/tests/native_tests/py3_only_tests.hy
+++ b/tests/native_tests/py3_only_tests.hy
@@ -16,15 +16,15 @@
   "NATIVE: test keyword-only arguments"
   ;; keyword-only with default works
   (defn kwonly-foo-default-false [&kwonly [foo False]] foo)
-  (assert (= (apply kwonly-foo-default-false) False))
-  (assert (= (apply kwonly-foo-default-false [] {"foo" True}) True))
+  (assert (= (kwonly-foo-default-false) False))
+  (assert (= (kwonly-foo-default-false :foo True) True))
   ;; keyword-only without default ...
   (defn kwonly-foo-no-default [&kwonly foo] foo)
   (setv attempt-to-omit-default (try
                                 (kwonly-foo-no-default)
                                 (except [e [Exception]] e)))
   ;; works
-  (assert (= (apply kwonly-foo-no-default [] {"foo" "quux"}) "quux"))
+  (assert (= (kwonly-foo-no-default :foo "quux") "quux"))
   ;; raises TypeError with appropriate message if not supplied
   (assert (isinstance attempt-to-omit-default TypeError))
   (assert (in "missing 1 required keyword-only argument: 'foo'"
@@ -32,11 +32,20 @@
   ;; keyword-only with other arg types works
   (defn function-of-various-args [a b &rest args &kwonly foo &kwargs kwargs]
     (, a b args foo kwargs))
-  (assert (= (apply function-of-various-args
-                    [1 2 3 4] {"foo" 5 "bar" 6 "quux" 7})
+  (assert (= (function-of-various-args 1 2 3 4 :foo 5 :bar 6 :quux 7)
              (, 1 2 (, 3 4)  5 {"bar" 6 "quux" 7}))))
 
 
+(defn test-extended-unpacking-1star-lvalues []
+  (setv [x #*y] [1 2 3 4])
+  (assert (= x 1))
+  (assert (= y [2 3 4]))
+  (setv [a #*b c] "ghijklmno")
+  (assert (= a "g"))
+  (assert (= b (list "hijklmn")))
+  (assert (= c "o")))
+
+
 (defn test-yield-from []
   "NATIVE: testing yield from"
   (defn yield-from-test []
diff --git a/tests/native_tests/tag_macros.hy b/tests/native_tests/tag_macros.hy
index ba586f65..18beaa79 100644
--- a/tests/native_tests/tag_macros.hy
+++ b/tests/native_tests/tag_macros.hy
@@ -100,9 +100,8 @@
     "Increments each argument passed to the decorated function."
     ((wraps func)
        (fn [&rest args &kwargs kwargs]
-         (apply func
-                (map inc args)
-                (dict-comp k (inc v) [[k v] (.items kwargs)])))))
+         (func #* (map inc args)
+               #** (dict-comp k (inc v) [[k v] (.items kwargs)])))))
 
   #@(increment-arguments
      (defn foo [&rest args &kwargs kwargs]
diff --git a/tests/test_bin.py b/tests/test_bin.py
index c2e31e96..6df4d437 100644
--- a/tests/test_bin.py
+++ b/tests/test_bin.py
@@ -7,7 +7,7 @@
 import os
 import subprocess
 import re
-from hy._compat import PY3
+from hy._compat import PY3, PY35
 from hy.importer import get_bytecode_path
 import pytest
 
@@ -210,12 +210,13 @@ def test_hy2py():
             if f.endswith(".hy"):
                 if f == "py3_only_tests.hy" and not PY3:
                     continue
-                else:
-                    i += 1
-                    output, err = run_cmd("hy2py -s -a " +
-                                          os.path.join(dirpath, f))
-                    assert len(output) > 1, f
-                    assert len(err) == 0, f
+                if f == "py35_only_tests.hy" and not PY35:
+                    continue
+                i += 1
+                output, err = run_cmd("hy2py -s -a " +
+                                      os.path.join(dirpath, f))
+                assert len(output) > 1, f
+                assert len(err) == 0, f
     assert i