Skip to content

Instantly share code, notes, and snippets.

@samuelgoto
Last active November 29, 2017 22:24
Show Gist options
  • Save samuelgoto/84b6f34be7e64a83cf0dd3282d3a4d1a to your computer and use it in GitHub Desktop.
Save samuelgoto/84b6f34be7e64a83cf0dd3282d3a4d1a to your computer and use it in GitHub Desktop.

Heavily inspired by kotlin and groovy, what would the following syntax simplication enable?

a {}

// syntact sugar for

a(function() {});

?

@samuelgoto
Copy link
Author

samuelgoto commented Sep 1, 2017

would it be possible to aid on the Map and Set thingies, e.g.

let a = new Map() {
  1 = "hi"
  2 = "hey"
  foo = "bar"
}

@samuelgoto
Copy link
Author

kotlin gradle dls:

https://rodm.github.io/blog/2017/04/teamcity-kotlin-dsl.html

buildscript {
    repositories {
        mavenCentral()
        maven {
            url "https://plugins.gradle.org/m2/"
        }
    }
    dependencies {
        classpath 'org.jetbrains.kotlin:kotlin-gradle-plugin:1.0.3'
        classpath 'com.github.rodm:gradle-teamcity-plugin:0.11'
    }
}

@samuelgoto
Copy link
Author

asserts:

https://artemzin.com/blog/ui-testing-separating-assertions-from-actions-with-kotlin-dsl/

@Test fun loginAndPasswordAreEntered() {
  loginScreen {

    login("artem_zin")
    password("*****")

    assert {
      loginButtonActivated()
      noLoginWarnings()
      noPasswordWarnings()
    }
  }
}

@samuelgoto
Copy link
Author

Testing DSL:

http://hadihariri.com/2013/01/21/extension-function-literals-in-kotlin-or-how-to-enforce-restrictions-on-your-dsl/

given("a calculator", {

  val calculator = Calculator()

  on("calling sum with two numbers", {

    val sum = calculator.sum(2, 3)

    it("should return the sum of the two numbers", {

      shouldEqual(5, sum)
    }
  }
}

@samuelgoto
Copy link
Author

This scoping:

class A { b() { console.log(this) } } 
> undefined
with (new A()) { b() }
> VM230:1 A {}

@samuelgoto
Copy link
Author

var p = new Proxy({}, {
    get: function(target, name) {
        return `hello world ${name}`;
    }
});
> undefined
> p.a
> "hello world a"
with (p) { console.log(a) }
> Uncaught ReferenceError: a is not defined

Why?

@samuelgoto
Copy link
Author

samuelgoto commented Oct 10, 2017

  • SQL?
  • open("filename.txt")?
  • css
  • regex
  • grep("regex") { do stuff }, sed
  • when/on for event handling
  • wait

builders

  • map, set ? (e.g. let a = map { put(1, 2) put(3, 4) })
  • dot dsl (e.g. graph("architecture") { link("a", "b")} link("b", "c") ... or digraph() { ... } )
  • antlr/yacc/bison
  • xpath
  • uml dsl (http://plantuml.com/sequence-diagram)
  • latex

flow control

NOTE(goto): how do you break from these loops? do you need to break() which throws an exception?

  • foreach(array) { console.log(item()); }
  • foreach(map) { console.log(${key()}: ${value()}); }
  • foreach (stream) { console.log(value()); }
  • each?
  • until
  • loop
  • VBs select, e.g. let a = select(expr) { when (expr) { foo } when (expr2) { bar } otherwise { "hello" } }
  • match (expression) { when(expr) { ... } when (expr2) { ... }}
  • using (C#s thingy that closes streams, like using (let a = ) {})
  • switf's guard, e.g. guard (expression) { ... }
  • swift's defer, e.g. defer { ... }

reserved words

  • enum (e.g. let Foo = enum { })
  • with

@samuelgoto
Copy link
Author

builders:

survey("TC39 Meeting Schedule") {
  question("Where should we host the European meeting?") {
    option("Paris")
    option("Barcelona")
    option("London")
  }
}

@samuelgoto
Copy link
Author

humm i'm wondering if it is absolutely necessary to have the requirement to change all of the calls to include this. i'm wondering if we pass things as function parameters, just basic parameter resolution would suffice. For example:

function a(b, c) {
  b()
  c()
}

// the problem is that the lambda block doesn't have any parameter names
// declared ..
a {
  // b was never declared as a parameter of the function ...
}

@samuelgoto
Copy link
Author

open("filename.txt", "r") {
// uses the file ...
}

@samuelgoto
Copy link
Author

languages that deliberately add support for DSLs:

@samuelgoto
Copy link
Author

samuelgoto commented Oct 16, 2017

Alternatives considered

implicit

In this formulation, the expansion would implicitly include the this binding. So, a { ... } would be equivalent to a.call(function { ... }).

let html = div {
  span("hello world") {}
}

this method resolution

In this formulation, the resolution of methods looks first for the presence in the this object for function calls before looking at the local scope and later at the global scope. e.g. a { b() } is equivalent to ```a(function() { (b in this ? this.b : b)() }).

For example:

let html = div {
  // "span" would first be looked at 'this' before looking at the global scope
  span {
  }
}

This may be isomorphic to the equivalency a { b() } to a(function() { with (this) { b() } })

bind operator

In this formulation, the expansion would be simply a { ... } to a(function() { ... }) and this would be passed via the bind operator

let html = div {
  ::div {
    ::span {
      ::p("hello world")
    }
  }
}

special character

In this formulation, we would pick a special syntax space to make the distinction between the this binding and regular function calls.

let html = <div> {
  <div> {
    <span> {
      <p>("hello world")
    }
  }
}

@samuelgoto
Copy link
Author

samuelgoto commented Oct 16, 2017

Extensions

This can open a stream of future extensions that would enable further constructs to be added. Here are some that occurred to us while developing this.

chaining

To enable something like if (arg1) { ... } else if (arg2) { ... } else { ... } you'd have to chain the various things together. @erights proposed something along the lines of making the chains be passed as parameters to the first function. So, that would transpile to something like if(arg1, function() { ... }, "else if", arg2, function { ... }, "else", function () { ... }).

Another notable example may be to enable try { ... } catch (e) { ... } finally { ... }

functization

To enable control structures that repeat over the lambda (e.g. for-loops), we would need to re-execute the stop condition. Something along the lines of:

repeat { ... } until ( expr ) we would want to turn expr into a function that evaluates expr so that it could be re-evaluated multiple times. For example repeat { ... } until (() => expr).

TODO(goto): should we do that by default with all parameters?

binding

There are a variety of cases where binding helps. Currently, we pass parameters back to the block via this. For example, we would want to enable something like the following:

foreach ({key, value} in map) { ... } to be given by the forach function implementation.

@samuelgoto
Copy link
Author

samuelgoto commented Oct 17, 2017

Principle of Abstraction

Tennents Correspondence Principle

These lambdas are particularly interesting. With arrow functions, we went as far as disabling break, continue and yield from top level statements, but we left return.

In this formulation, there are a few more cases that break the principle:

  • a SyntaxError when running into a return.
  • as opposed to arrow functions, the meaning of this isn't lexical.
  • like arrow functions, one can return values from the lambdas from one-line expressions.
  • TODO(goto): do we break things too by using the function scoped var? should we disallow it and force the block-scoped let?

throws should probably be supported.

TODO(goto): figure out how kotlin is planning to introduce break and continue into the closures.

http://yehudakatz.com/2012/01/10/javascript-needs-blocks/

In order to have a language with return (and possibly super and other similar keywords) that satisfies the correspondence principle, the language must, like Ruby and Smalltalk before it, have a function lambda and a block lambda. Keywords like return always return from the function lambda, even inside of block lambdas nested inside. At first glance, this appears a bit inelegant, and language partisans often accuse Ruby of unnecessarily having two types of "callables", in my experience as an author of large libraries in both Ruby and JavaScript, it results in more elegant abstractions in the end.
In contrast, when functions are used as callbacks, those keywords no longer make sense. What does it mean to return from a function that has already returned?

https://web.archive.org/web/20161123223104/http://wiki.ecmascript.org/doku.php?id=strawman:block_lambda_revival

return returns from the outer scope rather than the inner one.

@samuelgoto
Copy link
Author

Prior Art

@samuelgoto
Copy link
Author

Ruby's custom control structures

https://www.safaribooksonline.com/library/view/the-ruby-programming/9780596516178/ch08s08.html

after (100) {
  // run just once after 100ms
}

every (100) {
  // run forever every 100ms
}

@samuelgoto
Copy link
Author

samuelgoto commented Oct 17, 2017

Swift's custom control structures

https://hugotunius.se/2014/08/19/custom-control-structures-in-swift.html

Swift has a feature called @autoclosure which wraps any expression passed in a closure.

func _while(condition: @autoclosure () -> BooleanType, action: () -> ()) {
  while condition() {
    action()
  }
}

var i = 0
_while(i < 10) {
  println("\(i)")
  i += 1
}

Good discussion on reddit.

@samuelgoto
Copy link
Author

samuelgoto commented Oct 19, 2017

continue and break

break can break out of any block. For example:

foo: { a = 1; break foo; a = 2 }
console.log(a) // 1

Apparently, you need to be lexically nested to break to labels. For example:

foo: while(true) { console.log("hi"); break foo; } 
console.log("left foo") 
bar: while(true) { console.log("hey"); break foo; } // throws "undefined label foo" 
console.log("left bar")

Works with if:

foo: if (true) {
  console.log("this gets executed");
  break foo;
  console.log("this does not");
}
console.log("this does too")

Whether it is a modifier that gets used in the call site (e.g. for each(array) {}) or at the declaration site (e.g. ```loop function foreach() { ... }). For example, you used a modifier in the function call:

loop function a(expr, block) {
}
a(true) {
   ...
   break;
   ...
   continue;
   ...
}

It gets desugared to:

__exit__: a() {
  __block__: do {
    ...

    // all "breaks" here are replaced for "break __exit__"
    break __exit__;
 
    ...

    // all "continues" here are replaced for "continue __block__"
    continue __block__;

    ...
  } while (false);
}

For example:

loop function foreach(array, block) {
  for (item of array) {
    block.call(item);
  }
}
foreach (item in [0, 1, 2, 3]) {
  if (item < 2) {
    continue;
  } else {
    break;
  }
}

For example:

// a label gets added automatically at the top of the statement
__exit__:
foreach (item in [0, 1, 2, 3]) {
  // a labelled block gets added automatically wrapping the block param.
  __block__: do {
    if (item < 2) {
     // continue leaves the __block__
      continue __block__;
    } else {
      // arg-less break gets desugared into breaking to the head of the function.
      break __exit__;
    }
  } while (false);
}

FAQ

What happens with nested block params? E.g.

foreach (let item in array) {
  foreach (let other in item.array) {
    // will break and continue do the right things here?
    if (other.i == 1) {
      break;
    }
    continue;
  }
}

Annex

continue and break can take a label as a parameter:

foo: while (true) {
  console.log("this gets executed");
  break foo;
  console.log("this never gets executed");
}
console.log("this gets followed");

labels

foreach(let item in iterable) {
  if (item == 0) {
    break end;
  } else {
    continue foreach;
  }
}
end:

but if break and continue from "loops" such as arr.forEach are rare enough, why not use a real label for labeling the exit point?

@samuelgoto
Copy link
Author

@samuelgoto
Copy link
Author

This only works for statements (so, not allowed inside expressions), but ...

function unless(block) {
  if (!expr) {
    return block();
  }
}

while (true) {
  unless (false) {
    break;
  }
}

Could desugar to:

while (true) {
  {
    let result = unless (false, function() {
      return {break: true};
    });
    if (result.break) {
      break;
    } else if (result.continue) {
      break;
    } else if (result.return){
      return result.value;
    }
    // otherwise, just carry on ...
  }
}

@samuelgoto
Copy link
Author

Here is how ruby deals with these challenges:

def foreach(list)
  puts "Running foreach on #{list}"
  # you can call the block using the yield keyword  
  for i in list
    puts yield(i)
  end
  puts "End of method"
end  

puts ""
puts ""
puts "Sample #1: basic usage of blocks"
puts ""
puts ""

foreach (0..2) {
  # This is how you declare arguments to the block
  |value|
  puts "Hi from the block with args: #{value}"
  'A return value!'
}

puts ""
puts ""
puts "Sample #2: breaks inside blocks"
puts ""
puts ""

# breaks just return from the block and return null.
result = foreach (2..3) {
  puts "Hi from block with break!"
  break
}

puts "Result is nil, right? #{result == nil}."


puts ""
puts ""
puts "Sample #3: breaks inside blocks with for-loops nested"
puts ""
puts ""

# For example, break inside a lambda doesn't leave the
# outer lambda:
for i in 0..2
  puts "Iterating: #{i}!"
  foreach (0..i) {
    |value|
    puts "Iterating #{value}"
    
    # This only leaves the inner foreach, not the outer for
    break
  }
end

puts ""
puts ""
puts "Sample #4: what does continue do?"
puts ""
puts ""

# For example, break inside a lambda doesn't leave the
# outer lambda:
for i in 0..2
  puts "Iterating: #{i}!"
  foreach (0..i) {
    |value|
    puts "Iterating #{value}"
    # This only leaves the inner foreach, not the outer for
    next
  }
end


puts ""
puts ""
puts "Sample #5: what does return do?"
puts ""
puts ""
def foo
  foreach (0..0) {
    return "hello world"
  }
  return "not this"
end

# Returns "hello world" rather than "not this".
foo()

And console:

ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-linux]
   


Sample #1: basic usage of blocks


Running foreach on 0..2
Hi from the block with args: 0
A return value!
Hi from the block with args: 1
A return value!
Hi from the block with args: 2
A return value!
End of method


Sample #2: breaks inside blocks


Running foreach on 2..3
Hi from block with break!
Result is nil, right? true.


Sample #3: breaks inside blocks with for-loops nested


Iterating: 0!
Running foreach on 0..0
Iterating 0
Iterating: 1!
Running foreach on 0..1
Iterating 0
Iterating: 2!
Running foreach on 0..2
Iterating 0


Sample #4: what does continue do?


Iterating: 0!
Running foreach on 0..0
Iterating 0

End of method
Iterating: 1!
Running foreach on 0..1
Iterating 0

Iterating 1

End of method
Iterating: 2!
Running foreach on 0..2
Iterating 0

Iterating 1

Iterating 2

End of method


Sample #5: what does return do?


Running foreach on 0..0
=> "hello world"
   

@samuelgoto
Copy link
Author

Another example in Ruby:

def iffy(condition) 
  if (condition) then
    yield()
  end
end 

iffy (true) {
  puts "This gets executed!"
}
iffy (false) {
  puts "This does not"
}
for i in 0..1 
  puts "Running: #{i}"
  iffy (i == 0) {
    # This does not break from the outer loop!
    # Prints
    #
    # Running: 0 
    # Running: 1
    break
  }
end


for i in 0..1 
  iffy (i == 0) {
    # This does not continue from the outer loop!
    # Prints
    #
    # Running: 0 
    # Running: 1
    next
  }
  puts "Running: #{i}"
end

def foo() 
  iffy (false) {
    return "never executed"
  }
  iffy (true) {
    return "executed!"
  }
  return "blargh, never got here!"
end

# Prints "executed!"
foo()

@samuelgoto
Copy link
Author

samuelgoto commented Oct 30, 2017

Kotlin disallows break and continue inside blocks:

fun unless(condition: Boolean, block: () -> Unit) {
  if (condition) {
    block()
  }
}

fun main(args: Array<String>) {
  println("Hello, world!")
  while (true) {
    unless(true) {
      println("hello")
      // Error:(11, 12) 'break' or 'continue' jumps 
      // across a function or a class boundary
      // break

      // 'return' is not allowed here
      // return
    }
  }
}

@samuelgoto
Copy link
Author

fun case(condition: Boolean, block: () -> Unit) {
    println("This gets called!")
    block()
}

fun select(condition: Boolean, block: () -> Unit) {
  block()
}

fun main(args: Array<String>) {
  var expr: Boolean = true;
    
  select (expr) {
    case (true) {
        println("Totally true")
    }
    case (true) {
        println("Totally false")
    }
  }
}

@samuelgoto
Copy link
Author

samuelgoto commented Nov 29, 2017

Interesting use case in constructors for Ruby:

https://mixandgo.com/blog/mastering-ruby-blocks-in-less-than-5-minutes

let car = new Car() {
  ::color = 1;
  ::size = "large";
}

Could that lead to something like abstract classes?

sort([2, 3, 1, 4], new Comparator() {
  ::compare do (a, b) {
    return a < b;
  }
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment