samuelgoto / proposal-block-params

A syntactical simplification in JS to enable DSLs
204 stars 8 forks source link

Access arguments? #11

Open dead-claudia opened 6 years ago

dead-claudia commented 6 years ago

In many DSLs, it's convenient to have the ability to pass arguments to the callback as well - a good example of this is with arguments for a makefile task, but also, most of the binding-related stuff could be addressed similarly by simply accepting arguments.

My first instinct would be something like this (obvious Ruby inspiration):

forEach(items) do (item) {
    // ...
}
samuelgoto commented 6 years ago

Yep, indeed. I left this as an extension here:

https://github.com/samuelgoto/proposal-block-params#bindings

WDYT?

dead-claudia commented 6 years ago

Eh...it makes sense in some contexts (like iteration), but not in others (makefile task arguments).

I did read that after I filed this, but that idea really didn't feel right, especially if you have more than one parameter passed to the block (consider task name + dependencies).

samuelgoto commented 6 years ago

Yep, agreed that the syntax needs more work. Can you help me identify what are the most common use cases where you'd want arguments?

especially if you have more than one parameter passed to the block (consider task name + dependencies).

fwiw, in the current formulation,multiple parameters could be set:

foreach ({key, value} in map) {
  // ...
}
dead-claudia commented 6 years ago

The most common use cases would be in iteration-like contexts, but not all iteration is synchronous (like above). One example where multiple distinct arguments would be nice would be with native forEach, which would work out-of-the-box with explicit parameters:

list.forEach do (item, i) {
    // Do something that requires both the current item and its index.
}

fwiw, in the current formulation,multiple parameters could be set:

To clarify, I'm speaking about the callee's side, not the callback's side:

task("foo", ["dep1", "dep2"]) do (args) {
    // ...
}

While looking at porting some of my own code (a WIP DOM framework) over to use this, I ran into one case, although your binding proposal would work:

// Original
r.dynamic(stream, (r, item) => {
    // re-render on each received item
})

// This proposal
dynamic(stream) do (item) {
    // re-render on each received item
}

// Binding proposal
dynamic(item in stream) {
    // ...
}

Also, previously, I had my components defined like this:

// Original
const Component = m.component((r, attrs, children) => {
    // ...
})

// What it could be
const Component = m.component do (attrs, children) {
    // ...
}
samuelgoto commented 6 years ago

The most common use cases would be in iteration-like contexts, but not all iteration is synchronous (like above). One example where multiple distinct arguments would be nice would be with native forEach, which would work out-of-the-box with explicit parameters:

list.forEach do (item, i) {
    // Do something that requires both the current item and its index.
}

In the current formulation, this is what the syntax would look like:

foreach ({item, index} in list) {
    // Do something that requires both the current item and its index.
}

Which would desugar to:

foreach (list, function({item, index}) {
   // Do something that requires both the current item and its index.
});

Which foreach could use to pass parameters to the callee (IIUC your terminology):

function foreach(list, block) {
  for (let i = 0; i < list.length; i++) {
    block({item: list[i], index: i});
  }
}

Would that work?

samuelgoto commented 6 years ago

WIP DOM framework

I would love to hear more about your DOM framework :) This was very much started because I feel in love with Kotlin's examples of DSLs for DOM construction so hoping it will help this area.

Care to share any links?

dead-claudia commented 6 years ago

@samuelgoto

In the current formulation, this is what the syntax would look like: [...]

I'm aware. I was responding with that awareness in mind.

Care to share any links?

It's currently private and purely local, since I'm only just now getting to the beginning stages of implementing the logic, and haven't implemented any tests (after spending literal months figuring out what I wanted the API to be).

Edit: I hided this inside a <details> element, to make it easier to scroll by.

Here's an example ripped off ofbased on Elm's radio buttons example: ```js import * as m from "my-framework/hyperscript" import marked from "marked" const Sizes = { Small: "0.8em", Medium: "1em", Large: "1.2em", } const Toggle = m.component((r, {change, name}) => { r.h("label[style.padding=20px]", {onclick() { change.send(name) }}, r => { r.h("input[type=radio][name=font-size]") r.t(name) }) }) const MyApp = m.component((r, {update}) => { const change = m.ref.cell("Medium") r.h("fieldset", r => { r.c(Toggle, {change, name: "Small"}) r.c(Toggle, {change, name: "Medium"}) r.c(Toggle, {change, name: "Large"}) }) r.h("div", { style: {fontSize: change.pipe( m.ref.map(name => Sizes[name]) )}, props: {innerHTML: update.pipe( m.ref.map(update => marked(update.content)) )}, }) }) m.render(document.getElementById("app"), r => { const Intro = ` # Anna Karenina ## Chapter 1 Happy families are all alike; every unhappy family is unhappy in its own way. Everything was in confusion in the Oblonskys’ house. The wife had discovered that the husband was carrying on an intrigue with a French girl, who had been a governess in their family, and she had announced to her husband that she could not go on living in the same house with him... ` r.c(MyApp, {update: m.ref.of({content: Intro})}) }) ```
dead-claudia commented 6 years ago

Edit: I hided this inside a <details> element, to make it easier to scroll by.

If I were to write it using this proposal, I'd probably do something like this: ```js import * as m from "my-framework/block" import marked from "marked" const Sizes = { Small: "0.8em", Medium: "1em", Large: "1.2em", } const Toggle = m.component do ({change, name}) { h("label[style.padding=20px]", {onclick() { change.send(name) }}) { h("input[type=radio][name=font-size]") {} t(name) {} } } const MyApp = m.component do ({update}) { const change = m.ref.cell("Medium") h("fieldset") { c(Toggle, {change, name: "Small"}) {} c(Toggle, {change, name: "Medium"}) {} c(Toggle, {change, name: "Large"}) {} } h("div", { style: {fontSize: change.pipe( m.ref.map(name => Sizes[name]) )}, props: {innerHTML: update.pipe( m.ref.map(update => marked(update.content)) )}, }) {} } m.render(document.getElementById("app")) { const Intro = ` # Anna Karenina ## Chapter 1 Happy families are all alike; every unhappy family is unhappy in its own way. Everything was in confusion in the Oblonskys’ house. The wife had discovered that the husband was carrying on an intrigue with a French girl, who had been a governess in their family, and she had announced to her husband that she could not go on living in the same house with him... ` c(MyApp, {update: m.ref.of({content: Intro})}) {} } ``` Kotlin bindings, you could imagine, would look similar.
samuelgoto commented 6 years ago

What about the following?

const Toggle = m.component do ({change, name}) {
    label({style: {padding: 20px}}]) {
      ::onclick() { change.send(name) }})

       input({type: radio, name: font-size}) {
       }

       span(name) {}
    }
}
dead-claudia commented 6 years ago

I've considered going a similar route to that, except I ran into a few glitches:

  1. It's unclear how to integrate custom components.
  2. It's unclear how to integrate custom events.
  3. I'd rather avoid introducing the overhead of a proxy for this to discern the difference between the two.

Of course, this is more of a hypothetical front-end, and I'm developing a shared backend you can write the front-end stuff based on. It is similar to incremental-dom and virtual-dom in that regard, and you could write your own front-end that works that way.

Mind if you could find me on Gitter, and we can talk more on this privately? I'd rather avoid polluting the issue further on this topic, since it's not even public yet.

littledan commented 6 years ago

Arguments seem pretty important to me, especially with how you have examples with arguments (e.g., under "C#'s foreach").

Bikeshed syntax: maybe it's odd, but could we reuse the arrow function arrow?

foreach(list) item => {
}

Otherwise I like the foreach (map) { |key, value| } syntax.

samuelgoto commented 6 years ago

I dislike ruby's foreach (map) { |key, value| } syntax, but I think

foreach(list) item => {
}

or

foreach(list) do (item) {
}

Is growing on me.

May I ask what are your first impressions / reservations for the in syntax (totally cool if the impressions are purely subjective/bikeshedding)? Doesn't that look more consistent with the current usage of

for (a in list) {
}

Don't you think it feels more natural to re-use that syntax/construct for params?

foreach (a in list) {
}

?

littledan commented 6 years ago

I like either of those too.

My immediate reaction to in is a little hesitant. It makes me think of a for-in loop, where things would be different in two ways:

Even though it's not supposed to be any of those things, it raises my subconscious JS linter flags.

samuelgoto commented 6 years ago

just as another data point, @erights also suggested the following syntax:

foreach(list) do (item) {
}

Which I think is a reasonable form and seems to map better to people's intuition than my original form. I'm going to update the text to reflect that (done).

littledan commented 6 years ago

SGTM