Arc Forumnew | comments | leaders | submitlogin
Lisp-style definitions considered harmful
3 points by akkartik 4405 days ago | 16 comments
Over the past few months I've been gradually grokking why scheme says:

  (define (f x y z) ..)
and not:

  (define f (x y z) ..)
The reason: it directly maps the structure of a call to the code it expands to. This mapping is rendered starker when you throw infix into the mix:

  def (a = b)
    ..

  mac f.x
    `(,f ,x)

  mac ++n
    `(n <- (,n + 1))

  # basic type-based dispatch
  mac (lhs <- rhs) :case list?.lhs
    ..
All now legal wart: http://github.com/akkartik/wart/commit/ff999e5258. And I didn't need to make any changes to my reader, just redefine the macros def and mac.

---

As I've been playing with infix in recent weeks, I've been idly wondering how far you could push these ideas. Lisper dogma seems to be that only lisp can do macros well because it has parens and homoiconicity. I'm growing to think this is a canard. But if more complex syntaxes can get elegant macros, how much syntax is too much? I speculate that I can build elegant macros into any syntax -- as long as function calls group functions with their args. If I'm right, this is a very valuable insight, and lisp's syntax for defun makes it hard to see. Especially since C programmers like myself can start taking out the space:

  defun foo(x y z)
..thereby moving farther away from the light[1].

The notation f(x) needs to die.

[1] I didn't even really focus on this until evanrmurphy brought it up: http://arclanguage.org/item?id=12659



3 points by Pauan 4404 days ago | link

"Over the past few months I've been gradually grokking why scheme says"

I always thought that was just because Scheme tries to be minimal, and using the single special-form "define" for two different purposes helps with that. Scheme also does two different things with "let":

  (let ((foo ...)) ...)

  (let foo ((bar ...)) ...)

-----

2 points by Pauan 4404 days ago | link

Hmm... I tried to think of how to do this in a clean way... for instance, using macro-like pattern matching:

  {foo x y z} -> ...
  {bar x y z} -> ...
  {~   x y z} -> ...
But I'll have to spend a while mulling on that to see if it pans out. In the meantime, my plan for functions is to use -> syntax to define an anonymous function, meaning that this:

  -> x y z (+ x y z)
Is equivalent to this Arc code:

  (fn (x y z) (+ x y z))
This looks great when the functions are last, which is frequent with JavaScript callbacks. And then "def" is kinda like Arc's "=":

  # Nulan
  (def foo -> a b c ...)
  (def foo 5)

  ; Arc
  (= foo (fn (a b c) ...))
  (= foo 5)

-----

2 points by Pauan 4397 days ago | link

Okay. I've spent a bit of time mulling this over and trying out some stuff. Here's what I came up with.

How about a language that's like Haskell/Shen: currying everywhere. This language would be based heavily on functions, of course, which are expressed with "->" as follows:

  foo %x -> %x + 2
The above is equivalent to this in Arc:

  (def foo (x) (+ x 2))
Now, how this works is... everything to the left of the "->" is a pattern. Everything to the right is the function's body. If a variable starts with % it's local to the function, if not, it's global.

As a quick test of this system, I went through some semi-contrived examples taken from http://en.wikipedia.org/wiki/Duck_typing

  class Duck:
    def quack(self):
      print("Quack")
    def fly(self):
      print("Flap, Flap")
   
  class Person:
    def __init__(self, name):
      self.name = name
    def quack(self):
      print("{} walks in the forest and imitates ducks to draw them".format(self.name))
    def fly(self):
      print("{} takes an airplane".format(self.name))
   
  def quack_and_fly(duck):
    duck.quack()
    duck.fly()
   
  quack_and_fly(Duck())
  quack_and_fly(Person("Jules Verne"))
And here it is in this hypothetical currying-pattern-matching language:

  duck ->
    [ quack -> prn "Quack"
      fly   -> prn "Flap, Flap" ]

  person %n ->
    [ quack -> prn "@%n walks in the forest and imitates ducks to draw them"
      fly   -> prn "@%n takes an airplane" ]

  quack-and-fly [ quack %q fly %f ] -> %q; %f

  quack-and-fly duck
  quack-and-fly person "Jules Verne"
Wooow that is short! It also means that (except for % variables which are local) the pattern matching to the left of -> matches the function call.

If you wanted to, you could use parens like Shen instead of no-parens like Haskell.

Some downsides? Well, since it's using currying, there could be some issues with that. In particular, variable argument functions wouldn't be possible. There's also potentially some scoping issues and such.

Overall though, I think this idea is cool enough to actually try and implement it in a simple interpreter, to figure out all the kinks.

-----

1 point by Pauan 4396 days ago | link

Okay, I was able to solve a couple problems with my object pattern-matching...

  [ foo = 5 | bar = 10 ]
The above is a collection of patterns. Specifically, it has a "foo" pattern that maps to 5, and a "bar" pattern that maps to 10. Now, let's put this object into a variable:

  pattern = [ foo = 5 | bar = 10 ]
Now how do we extract the subpatterns? Like so:

  pattern [ foo ]
  pattern [ bar ]
The above returns 5, and then 10. And we can extract multiple patterns at once:

  pattern [ foo | bar ]
The above returns 10. This largely removes the need for "as" patterns, which is something I found cumbersome to use. You can think of | as being kinda like "do". It will first call the foo pattern, then the bar pattern.

Also, in Haskell you might write this:

  fib 0 = 0
  fib 1 = 1
  fib n = fib (n-1) + fib (n-2)
In this language you might write this:

  [ fib  0 = 0
  | fib  1 = 1
  | fib %n = fib (%n - 1) + fib (%n - 2) ]
Hmm... I'm still working out the kinks in the object system...

-----

1 point by Pauan 4396 days ago | link

Okay, even more thinking... what if all objects in this system were simply a collection of functions? Then you'd write something like this:

  duck =
    quack = prn "Quack"
    fly   = prn "Flap, Flap"

  person %n =
    quack = prn "@%n walks in the forest and imitates ducks to draw them"
    fly   = prn "@%n takes an airplane"

  quack-and-fly %x = %x quack; %x fly

  quack-and-fly: duck
  quack-and-fly: person "Jules Verne"
Wow! Even shorter! This reminds me of CoffeeScript's implicit objects: http://coffeescript.org/#objects_and_arrays

-----

1 point by Pauan 4397 days ago | link

For comparison, here it is with some parens:

  (duck) ->
    [ quack -> (prn "Quack")
      fly   -> (prn "Flap, Flap") ]

  (person %n) ->
    [ quack -> (prn "@%n walks in the forest and imitates ducks to draw them")
      fly   -> (prn "@%n takes an airplane") ]

  (quack-and-fly [ quack %q fly %f ]) -> (%q); (%f)

  (quack-and-fly (duck))
  (quack-and-fly (person "Jules Verne"))
I wasn't sure whether "quack" and "fly" should be in parens or not...

-----

1 point by Pauan 4397 days ago | link

Like Haskell, it may be better to use =

  duck =
    [ quack = (prn "Quack")
      fly   = (prn "Flap, Flap") ]

  person %n =
    [ quack = (prn "@%n walks in the forest and imitates ducks to draw them")
      fly   = (prn "@%n takes an airplane") ]

  quack-and-fly [ quack %q fly %f ] = %q; %f

  quack-and-fly duck
  quack-and-fly person "Jules Verne"
Oh my, am I going to invent a dynamically typed Haskell with immutable objects? I guess next will be laziness and monads...

-----

1 point by akkartik 4397 days ago | link

  quack-and-fly person "Jules Verne"
Haskell would keep it from conflicting with currying:

  quack-and-fly (person "Jules Verne")
Did you have some other way to avoid the conflict?

-----

1 point by Pauan 4396 days ago | link

"Did you have some other way to avoid the conflict?"

No, that's a reasonable way to solve it. But I still would like optional args, variable args, and vaus. Wheee a dynamic Haskell with vaus~

Hmm... with a little syntax, you could write this:

  quack-and-fly: person "Jules Verne"
That's like the . operator in Haskell.

-----

2 points by Pauan 4404 days ago | link

This reminds me of Shen macros[1], which are strictly more powerful than Arc-style macros:

  ; Arc
  (mac and args
    (if args
        (if (cdr args)
            `(if ,(car args) (and ,@(cdr args)))
            (car args))
        't))

  ; Shen
  (defmacro andmacro
    [and]       -> true
    [and X]     -> X
    [and X | R] -> [if X [and | R]])
Because Shen macros operate on the entire code structure rather than the arguments of the macro, you can do silly stuff like replace the number 2 with the number 2.5:

  (defmacro twomacro
    2 -> 2.5)
And now (+ 2 2) returns 5.

---

* [1]: http://shenlanguage.org/learn-shen/macros.html

-----

3 points by akkartik 4404 days ago | link

I thought there was a connection with pattern matching.

But why does Shen even bother naming these macros? What operations can you perform on andmacro?

-----

2 points by Pauan 4404 days ago | link

In Shen, macros are stored in a global list. I'm guessing you name them so that you can update/remove them later or something. I guess it can also serve as documentation? I dunno. I also thought it was weird that they were named.

-----

2 points by evanrmurphy 4404 days ago | link

I'm not sure I understand a couple of your examples:

  def (a = b)
    ..
Is this a function with parameter "a" assigned to value "b" by default? If so, what is the function's name?

  # basic type-based dispatch
  mac (lhs <- rhs) :case list?.lhs
    ..
No clue what's going on here, unfortunately. Would you mind walking me through it?

-----

1 point by Pauan 4404 days ago | link

"No clue what's going on here, unfortunately. Would you mind walking me through it?"

When wart sees a symbol that is punctuation, it treats it as an infix operator, so that (a + b) and (a = b) work, etc.

As for :case, that's basically equivalent to "extend" in Arc. In other words, in Arc you write this:

  (extend foo ... (bar ...))
But in wart you would write this:

  def foo ... :case (bar ...)
And this topic is about Common Lisp (defun foo ...) vs. Scheme (define (foo ...))

---

So, if you put it all together, that means this:

  def (a = b)
    ..
Is equivalent to this in Arc:

  (def = (a b)
    ..)
---

And this:

  mac (lhs <- rhs) :case list?.lhs
    ..
Is equivalent to this in Arc:

  (mac <- (lhs rhs)
    (when (list? lhs)
      ..))

-----

3 points by evanrmurphy 4404 days ago | link

Thanks. So when the function name is grouped with its parameters in definition (as it is in scheme), and infix is permitted, no longer does the function name have to come before its parameters.

I would have to get used to not always seeing the function name first, but I like the symmetry this produces between definition and call.

Added: So when wart got infix (http://arclanguage.org/item?id=16775), Kartik gave this example for defining your own infix ops:

  def (<>) (a b)
    (~iso a b)
"<>" had to be in parens so that wart's infixey reader wouldn't try to swap it with "def". Now thanks to scheme-style grouping of the function name with its params, this definition can be written:

  def (a <> b)
    (~iso a b)

-----

2 points by akkartik 4404 days ago | link

Exactly. Sorry, I think you're missing http://arclanguage.org/item?id=16826. In brief, = is now equality and <- is now assignment. And since both are composed of operator chars, (def (a <- b) ..) is transparently read as (def (<- a b) ..), which now maps directly to the scheme-like declaration syntax, etc., etc.

Thanks Pauan for the clarifying comments!

-----