paiq / blackcoffee

CoffeeScript + hygienic macros
MIT License
105 stars 9 forks source link

Method Macro Expansion proposal #1

Closed xkxx closed 10 years ago

xkxx commented 10 years ago

Hello there,

I would like to propose adding support for method macros. They are similar to normal macros, except they take an extra object argument and are used in place of methods. This pull requests implements a rough example implementation of the concept. I used macro.defmethod to denote method macro in my implementation, but it serves as a placeholder only and the actual keyword is open for suggestion.

A short example (setting/getting DOM data-* fields):

macro.defmethod $data (el, name, val) ->
        if val?
                macro.codeToNode(-> el.dataset[name] = val).subst {el: el, name: name, val: val}
        else
                macro.codeToNode(-> el.dataset[name]).subst {el: el, name: name}

A more extensive example and my main use case for this feature, a JQuery-like utility that compiles to bare-bone HTML5:

macro $id (id) ->
    macro.codeToNode(-> document.getElementById id).subst {id: id}
macro $tag (name) -> 
    macro.codeToNode(-> document.getElementsByTagName name).subst {name: name}
macro $class (name) ->
    macro.codeToNode(-> document.getElementsByClassName name).subst {name, name}
macro $query (q) ->
    macro.codeToNode(-> document.querySelector q).subst {q, q}
macro $queryAll (q) ->
    macro.codeToNode(-> document.querySelectorAll q).subst {q, q}

macro.defmethod $data (el, name, val) ->
    if val?
        macro.codeToNode(-> el.dataset[name] = val).subst {el: el, name: name, val: val}
    else
        macro.codeToNode(-> el.dataset[name]).subst {el: el, name: name}

macro.defmethod $css (el, name, val) ->
    if val?
        macro.codeToNode(-> el.style.setPropertyValue(name)).subst {el: el, name: name, val: val}
    else
        macro.codeToNode(-> el.style.getPropertyValue(name)).subst {el: el, name: name}

macro.defmethod $hasClass (el, name) ->
    macro.codeToNode(-> el.classList.contains(name)).subst {el: el, name: name}

macro.defmethod $addClass (el, name) ->
    macro.codeToNode(-> el.classList.add(name)).subst {el: el, name: name}

macro.defmethod $toggleClass (el, name) ->
    macro.codeToNode(-> el.classList.toggle(name)).subst {el: el, name: name}

macro.defmethod $removeClass (el, name) ->
    macro.codeToNode(-> el.classList.remove(name)).subst {el: el, name: name}

$id("myform").$data "user", "foo"
$id("important_text").$css "fontSize", "24px"
vanviegen commented 10 years ago

Hey @xkxx,

Thanks for your pull request. I'm not really convinced that macro-methods would be a good idea though... in your example, it looks like $data should be somehow part of the specific object (or class) you calling it on. In fact, it behaves more like a global function. Wouldn't it be less confusing to just create a regular macro that takes the element as its first argument?

Regardless, if you really want this type of syntax for some sort of DSL, you could just implement defmethod using a regular macro that takes an AST to be walked. (Just like macro is itself a macro.) Would that work for you?

Looking over your patch, you're right that the implementation should be using hasOwnProperty for looking up macros. I'll do that the next version.

xkxx commented 10 years ago

A macro method is simply a syntax sugar for method(self, args...), but I would argue that it is an important one. Looking at other languages like Julia, Lua and Nimrod, they have methods are expressed in a similar fashion. It is true that we can use method(self, args...) rather than self.method(args...), but it makes it harder to see the program's flow with currying involved. Here is an example:


# without method macro

 $insert(
   $css(
     $toggleClass(
        $removeClass(
          $remove($id "element"), "a-class"),
          "another-class"),
         "border",
       "solid 1px #fff"),
  $id("root"))

# with method macro

$id("element").$remove().$removeClass("a-class").$toggleClass("another-class").$css("border", "solid 1px $fff").$insert($id("root"))
vanviegen commented 10 years ago

@xkxx, I see your point about method-syntax being convenient. But I hope you see my point as well: using method-syntax for what are basically global functions can be pretty confusing.

Anyway, if you want to use this for a DSL or something like that, you can still use macros to implement this syntax. I'm closing this issue as a wontfix for now.