Literal Abstractions

I have been playing with a new language feature I haven’t seen anywhere and I wanted to share it with you. I am currently implementing a prototype for my own language but it would be absolutely gorgeous to have it in Scala as well. I call it Literal Abstraction because it does just that, it abstracts over a variable in a term using a literal supplied at compile time:

def foreach[A, B](literal x, seq: Seq[A])(f[x]: A => B) = seq.foreach(f)

In the above definition, the literal parameter comes first in the parameter list. More importantly though, f[x] means “when applying an argument for f, make sure to abstract over the variable bound to x” This means that all parameters that are concerned with literal abstractions (i.e. the literal parameter itself and terms binding the variable) have to be filled in at once. Sadly, this would somewhat restrict currying, even preventing it entirely in cases where the first and last parameter “know about something regarding literals”. Anyway, an example invocation of the function declared above could like this:

foreach(x, List(1,2,3)) {
    println(x * x)
}

This is a simple case where the literal abstraction basically replaces the need to introduce a named parameter for the lambda. A slightly more interesting case would be custom syntax for lambda expressions such as

// using the new extension method syntax of dotty <3
def [A, B](literal x) ~> (f[x]: A => B) = f

val lambda = a ~> println(a)

Something that might require some tinkering is literal parameters with varying arity. After all, how could one define multi-param lambdas this way?

type VarTypeArg = ??? // As far as I know, this isn't possible
def [VarTypeArg, B](literal ...xs) ~> (f[xs]: VarTypeArg => B) = f

val lambda = (a, b) ~> println(a * b)

The Scala community seems to be incredibly creative so I am pretty sure that people will find very practical usecases and I don’t think it would be too hard to implement either. With literal as soft keyword, the core language would not change much, while adding a significant amount of flexibility to API designers. I think the symbol lookup would have to be changed slightly because it would also need to consider symbols introduced by literal arguments. It’s worth noting that this feature might not have as much of an impact as it would in certain other languages, I still felt like proposing it though.

I’m not sure I understand what this is intended to do, can you confirm if this would be equivalent to this:

List(1,2,3).foreach { x =>
  println(x * x) 
}
2 Likes

Yes, absolutely. As I said the feature might not have the same potential in Scala that it has in other languages but I still think it would be an interesting experiment. Literal abstractions could become more useful when the scope where a literal is known in the AST is expanded across partial applications wherever possible. I don’t know to which degree this is possible in Scala though.

first: This proposition isn’t precise at all. Two totally different trivial examples, that even doesn’t show motivation for such feature. What is use-case for it?

second: what is type of lambda from first example? Is it Function[Any, Unit] or what? Where types [A,B] goes? where they was inferred?

third: I guess such flexibility will produce code hard to maintain, and it will over complicate scala compilier and language. I’m not telling it is totally wrong… but in current shape this proposition is useless.

forth: Introducing new terms into context is desirable in many dsls, but in my opinion this proposition does not seem to solve the problem :(. Nevertheless thanks for this idea. If You really believe it is good concept, show code that will benefit from this syntax. Motivating such propositions is hard… every change to language like scala is risky and there needs to be really beneficial.

ps: Be patient. There is lot of such propositions, and core dotty/scala contributors has not enough time to consider all of them quickly (especially when they don’t see use-case for it).
Most of such concepts are rewritten many times before they are considered as “nice”, and even then most of them land in trash.
Now dotty is stabilizing. I assume that for ~1year there is no place for new revolutionary ideas.

3 Likes

Thanks for the constructive feedback!

My motivation is that I would sometimes have the ability to define when a new identifier is introduced. in the example by @morgen-peschke

you can see that the identifier is introduced on the left-hand side of the arrow =>. While, as you say, it is good to have a stable foundation where developers can argue that they know what’s going on, it would be nice to expand upon it. In particular, DSLs / sub-languages would profit the most from this feature. Consider the following minimalist testing library:

// == some library == //
def createTestLang( 
    literal res,
    literal ...vals
)(f[res, vals]: => Unit) = f

// unary functions
// prop: >>(R, A)<< in same order as in f[res, vals]!!
def [A, R](prop: (R, A) => Unit) of (func: A => R) = {
    val a = genValue[A]
    val res = func(a)
    prop(res, a)
}

// for binary functions
def [A, R](prop: (R, A, B) => Unit) of (func: (A, B) => R) = {
    val a = genValue[A]
    val b = genValue[B]
    val res = func(a, b)
    prop(res, a, b)
}

// == user configuration == //
val defineProperty  = createTestLang(theResult, leftOp, rightOp)
// or something along those lines
def itDoesntHold = throw library.PropFailedException()

// == test code == //
// given a definition like "def multiply(x: Int, y: Int) = x * y"
defineProperty {
    if leftOp != 0 && rightOp != 0 then theResult != 0 else itDoesntHold
} of multiply

This would be equivalent to

defineProperty { (leftOp: Int, rightOp: Int, theResult: Int) =>
    if leftOp != 0 && rightOp != 0 then theResult != 0 else itDoesntHold
} of multiply

Note that, without those literal abstractions, the parameter list for the lambda in the latter case would have to be supplied for each new property whereas the identifiers introduced by createTestLang could be used for lookup in all “final” applications of the curried function. With context functions in Dotty, something similar can be achieved as the docs show with ensuring. In the case of literal abstractions, however, the identifiers are chosen by the library user whereas context functions let the library author decide on the nomenclature.

While users could always explicitly rename the import to import testlib.{result => theResult}, for example, the user would also have to always rename the import to achieve consistency.

I see that this can further increase language complexity for users as it adds another rather esoteric layer of flexibility. Nevertheless, it could increase the expressiveness of APIs when used purposefully. I do not know how one could prevent any abuse of this feature, I am fully aware that it could harm maintainability.

Regarding your second point: Typing and inference works like regular lambdas since the term { println(x*x) } is simply rewritten to { x => println(x*x) } once regular lookup for the identifier x fails and the compiler starts searching for any literals previously bound as part of earlier applications of the same function.

if I understand your example correctly, there is similar proposal:

The main drawback of such aproach is:

Dotty allows to implement “it pattern”.

Nevertheless I am sure that such such ability is very important for dsl especially for typeclasses because an extension has no sence with “it” prefix.

There are also many use cases for this feature.

2 Likes