Pre-SIP: Virtualization (feedback needed)


#1

Background

There once was a fork of Scalac, called Virtualized Scala. It expands Scala’s DSL capabilities by allowing to override keyword constructs like if-else , = , var , while , etc.
References:


Related Work



Why macros aren’t enough?

The alternatives, mentioned in the related work force the user of the DSL library to annotate everything. Some kind of @virtual or object virtual encapsulation.
I look at my DSL requirement as a Scala/DSL hybrid. I want to program in Scala normally, and have DSL-specific classes, types, operators, etc. If I’m using a DSL type, that means I’m programming in DSL. So for me, any kind of annotation is redundant. Types are my annotations. That is the safest approach, IMO.
Fact is I can override a + operator according to the types, but not override an if expression. I want to change this, and at the moment, there is no better alternative than modifying the compiler to do so.
This is a limitation of the language.

Goal

Development of Virtualized Scala stopped at 2.10.2. I want to re-implement some of the features which are not transparently available using macros.

Implementation

An initial PR was submitted long ago for experimentation, but there seemed to be a little chance for this PR to be accepted, so the work was put on hiatus to explore other methods (possibly a compiler-plugin solution). However, recent closing of this PR initiated a discussion that provides some hope, along with another contributor, @namin.

Your feedback is needed

Are you interested in this language feature?
The PR discussion suggested that Spark could benefit from this SIP. How?
What other related features should be included in this SIP?


#2

Do you want (in whatever DSL) to override just semantics of, for instance, if operator or even syntax too?

I easily can imagine (and even give you an example of) DSLs which basically can be implemeted purely as Scala-based DSL (using current Scala) except few things like if-operator syntax.


#3

My use-case primarily involves overriding if, so I can express:

val b : MyBool = ???
val be : MyBool = ???
if (b) {
  //Block A
} else if (!be) {
  //Block B
}

For my use-case, I need both Block A and Block B, no matter what b is (the blocks are fed into an implicit context).
match is also pretty important, but that might get too complicated and fragile to be expressed in a replaceable function format.


#4

Can’t you just write:

If (b) {
  //Block A
} ElseIf (!be) {
  //Block B
}

The benefits of enabling the native-if syntax seem rather small, compared to the extensive language changes its implementation would require (and the changes would have to be ported to Dotty).
As you’ve said, it’s not even clear how to virtualize some constructs (such as match in all its generality). But the good news is that it’s easy coming up with a DSL syntax to cover whatever subset of match you’re interested in, with a syntax like, for example, x.Match(Case(...),Case(...),...).


#5

Using a different If-Else has several drawbacks.

  1. From a (DSL) user perspective it’s more confusing.
  2. The syntax is VERY limited. Actually it would look like this:
If (b) {
  //Block A
}.ElseIf (!be) {
  //Block B
}.Else {
  //Block C
}
  • We must use a . in front of the Else and ElseIf (there is some workaround using implicit conversion, but it prevents any implicit conversion to happen inside the condition).
  • We cannot have a single line condition
If (b) //Block A
ElseIf (!be) //Block B

#6

I know this is not an option, but SuperCollider has a brilliant thing that translates

foo (bar, baz)

into

bar.foo(baz)

So if technically is a method on the predicate, the boolean, like

if (x > 1234) { ... }

being resolved as

(x > 1234).if { ... }   // receiver is Boolean

It also gives you for free things like

cos(pi*2)

which just translates to

(pi * 2).cos   // receiver is Float

best, …h.h…


#7

Bye the way, talking about DSLs. I did this for a sound synthesis DSL:

val res: GE = If (freq > 100) Then {
  SinOsc.ar(freq)
} Else {
  WhiteNoise.ar
}

I wish people would realise what a damn great thing a then keyword will be, instead of just resisting historical progress.

Best, …h.h…


#8

I’m all ears. Tell me what’s so great about it.


#9

Great about what exactly? Unified standalone function and method syntax? Or great about having a then keyword? Well, the ability to define a DSL that has the same tokens. Which is difficult if you rely on parentheses on the built-in language level.


#10

the ability to define a DSL that has the same tokens

What’s wrong with

def If[A](p: Boolean)(f: => A) = IfThen(p, f _)
case class IfThen[A](p: Boolean, f0: () => A) {
  def Else[B >: A](f1: => B) = IfThenElse(p, f0, f1 _)
}
case class IfThenElse[A](p: Boolean, f0: () => A, f1: () => A) {
  def run() = if (p) f0() else f1()
}

And then (no pun intended)

If (true) {
  1
} Else {
  2
}

#11

Good point. I must admit, it’s a while back that I wrote it, but there was a reason this wasn’t working out in the end. Of the top of my head, you are forced to use curly braces, so If (true) 1 Else 2 doesn’t work. (the other possibility is there was some ugliness with else-if catenation).

Edit: Although I think with current syntax, you will also not be able to drop the parens for If:

def If(p: Boolean): If = new If(p)

class If(p: Boolean) {
  def Then[A](thenBranch: => A) = new IfThen[A](p, thenBranch)
}

class IfThen[A](p: Boolean, thenBranch: => A) {
  def Else[B >: A](elseBranch: => B) = new IfThenElse[A, B](p, thenBranch, elseBranch)
}

class IfThenElse[A, B](p: Boolean, thenBranch: => A, elseBranch: => B)

If (true) Then 1 Else 3  // ok
If true Then 1 Else 3  // nope

#12

It gives up tail recursion: your thunk parameters can’t be optimized.


#13

Eh sure… But I’m sure the same goes for the If Then Else version.


#14

What’s wrong with
def If[A](p: Boolean)(f: => A) = IfThen(p, f _)
case class IfThen[A](p: Boolean, f0: () => A) {
def Else[B >: A](f1: => B) = IfThenElse(p, f0, f1 _)
}
case class IfThenElse[A](p: Boolean, f0: () => A, f1: () => A) {
def run() = if § f0() else f1()
}

Try adding Else If and see what happens.


#15

That was a fun exercise. https://scastie.scala-lang.org/Jasper-M/27EobeRLQ3SAyqBVd5bfBw/1

I know it’s not perfect of course…


#16

Yes, as you’ve probably noticed, the implicit conversion fails if applied with a combination of the implicit class Foo. The workaround is to directly use .ElseIf which is rather annoying. Truthfully, I’d rather maintain a scala fork rather than enforce bad syntax on my users. Hopefully, virtualization will be part scala or a compiler plugin.


#17

As always I’m against any of such changes.

DSLs are not worth of introducing such dangerous changes. Each change in compiler has impact. To be honest i expect that in 1 year this feature’ll be dead.

There is nice sbt plugin https://github.com/lihaoyi/Scalite that introduces awsome syntax to scala. How many people uses it right now? I guess… none.

There is many things in scala community that could be better (compiler performance, build tools, documentation), and i encourage you to not waste time in things that no one is interested about.

If you still want to do that I propose to see what is in https://github.com/lihaoyi/Scalite.
Maybe you could use similar approach.


#18

Scalite is self-described as:

…the culmination of about 30 hours of work, and isn’t ready to be used for anything at all. The semantics are full of bugs, and the implementation is a rats nest of complexity, but it works, and hopefully will inspire or convince someone somewhere that a whitespace-based syntax is something worth trying out.

Which makes it somewhat disingenuous to hold scalite up as a straw-man here! There’s a world of difference between wanting more consistency and DSL flexibility, and being willing to use a library that announces it’s full of bugs, and will necessarily trail official Scala releases due to the way it works.


#19

Yea You are right. Scalite was probably created as creation craze. Nevertheless motivation is not so important here. There is lot of abandoned libraries/solutions that has no (or very little) value and high risk. I’m just scary that this project’ll take lot of your time without measurable profit for anyone.

But who knows… maybe I’m wrong.
I wish you successes whatever decision you’ve made :slight_smile:


#20

Hello,

There is this school of thought that Scala is great for internal DSLs,
because you can omit some dots and parentheses and make it look more or
less like a human language.

As some one who has used Scala to design internal DSLs, I don’t buy into
this school of thought. I think Scala is great for that purpose, mostly due
to implicits and string interpolation, but not because to omitted dots and
parentheses.

Trying to make your DSL look like human language is unlikely to work out
well, because the meaning of the human language you are trying to
approximate is almost certainly not going to be aligned with the semantic
model your DSL is based on. It may work well for a few examples, but soon
you will run into discrepancies between what the DSL actually means and
what one would expect based on the human language it seems to represent.

Braces and parentheses are very nice, because everyone already knows what
they mean: the beginning and end of a phrase. Simple and powerful.

If instead you want to specify that a certain word means the beginning and
another means the end, you have two choices: you can always use the same
words, like “begin” and “end”, like in Pascal, and then you just added
verbosity and gained nothing. Or you use different words, and then it
becomes quickly complicated.

Best, Oliver