SIP Proposal: Use parenthesis for call-by-name

Current:

def someFunct(f1: => Int) = {
  val x = f1
}

Proposed:

def someFunct(f1: => Int) = {
  val x = f1()
}

Motivation:

  1. by-name becomes similar to invoking Function0 - passing Function0 is semantically equivalent to passing by-name parameters.

  2. We can seamlessly pass by-name params to other by-name arguments e.g.

def foo(f1: => Int) = {
  val x = f1
  bar(x)  // passes f1 by name
  val y = f1()
  bar(y) // passes y instead of f1 by name
}

def bar(f1: => Int) = {}
  1. You cannot accidentally call an expensive by-name parameter deep down your function - alternatively if you change an argument to be by-name, you are forced to change it at the use-sites too.
3 Likes

I don’t understand the point about Function1[Unit, A].

Removed that comment.

This seems to introduce inconsistencies in the syntax. Are you saying that def foo(f1: => Int) = bar(f1) and def foo(f1: => Int) = bar(f1()) would be equivalent? Only the latter should type check, if you want to be consistent.

I’ve also been bitten by by-name parameters in the past, because in a big function body I tend to forget that they are by name and that referring to them triggers a computation, so I may inadvertently duplicate work/effects. So now I name all my by-name parameter mkXyz instead of just xyz, so that it’s always clear that it’s doing something.

4 Likes

How would you call def foo(f1: => () => Int)

would it then be f1()()?

There’s also the issue that generally speaking, methods that do not have side effects should not have parenthesis.

def foo(): Int implies a side effect
def foo: Int implies no side effect.

In fact, in later case, semantically we do not care whether the method being called is a getter for a val or a method actually performing some interim computation.

What you really are aiming for here, is to syntactically show computationally expensive calls.
Whether a method/function takes a while to compute or not, is not the responsibility of the type system.
Nor is it the responsibility of the language syntax.

This is the case both with regular def’s, and call by name parameters. I don’t forsee this working.

If you keep methods with CPN parameters in them small, and trivial, you should never have an issue. Regardless of whether or not they are expensive, or have side effects.

4 Likes

I had a similar discussion here. There were some ideas but it didn’t go anywhere concrete. If we could come up with something for by-name to make it more explicit without making it more verbose, that’d be wonderful.

FWIW, I think it would make more sense to make => T a proper type and an alias of, say, scala.Thunk[T] which would have a single apply method with no parameter list, and which would be created implicitly based on the expected type:

def foo(t: => Int) = println(t.apply)
// equivalent to:
def foo(t: Thunk[Int]) = println(t.apply)

foo(readInt) // ok, creates a thunk implicitly
val f: Thunk[Int] = readInt
foo(f) // also ok

OR even better, once we have implicit function types there is no more need for a special by-name abstraction –– it can be encoded directly!

The following already works in Dotty:

def foo(t: implicit Unit => Int) = println(t(())) // Dotty does not accept just `t()`
// or, if you choose, you can make the application implicit:
implicit val unit: Unit = ()
def foo(t: implicit Unit => Int) = println(t)

foo(readInt) // ok, abstracts implicitly
val f = implicit (_: Unit) => readInt
foo(f) // also ok

These encoding solutions also give us a natural semantics for things like varargs of by-name arguments (which have been a problem before). For example:

def foo(ts: (implicit Unit => Int)*): Unit = {
  ts foreach { f => val v = f(()); if (v < 0) return else print(s"$v "); }
  println
}
foo(readInt,readInt,readInt,readInt)
// ^ reads+print 4 times unless user stops mid-way by entering negative number
5 Likes

@LPTK would that encoding allow more than one by-name argument with the same type?

@shawjef3 yes:

> dotr
Starting dotty REPL...
scala> implicit val u: Unit = Unit
scala> def foo(x: implicit Unit=>Int, y: implicit Unit=>String) = println(y * x)
scala> var v = 1
scala> foo({v+=1;v}, s"ok $v! ")
ok 1! ok 1!

Thinking about this more, we should replace call-by-name syntax i.e. def foo(f: => Int) with only Function0 syntax i.e. def foo(f: () => Int). This would solve all issues - we would be forced to use f() when used making it easier for the reader to know what is happening and one less concept in the language.

3 Likes

@pathikrit I’m for that.

I pushed a branch that removes by-name parameters.

no-by-name2

1 Like

The entire point of by-name parameters is the transparent call sites.

1 Like

I agree with this.

I too have had nasty bugs because when using the by-name parameter there is no hint that it’s reevaluating. I think forcing an () is a good hint of what’s actually happening. In terms of use-site ergonomics, nothing changes.

1 Like

After removing by-name parameters, this would enable the old style of using by-name parameters. Writing this makes me see kind of how awful it is.

implicit def byName[A](name: () => A): A = name()

@pathikrit Would you like to put up together a SIP proposal for this? Others that have commented on the ticket: feel free to join forces with Pathikrit.

1 Like

How awful is what? The by-name syntax or the proposed Function0 syntax??

@pathikrit The part I meant is awful is the part where just mentioning a name causes evaluation. IE automatic conversion of a Function0[A] to A.

A by-name parameter is implicitly a function. I’d say the way it is in Scala it’s the way is supposed to be, though I agree that lazy evaluation of by-name parameters is more intuitive.

Another problem is that => A is not even really a type. It’s a Function0 with the semantics of a Function0 but the syntax of a value. I say it’s not even really a type, because only a subset of the syntax rules that expect a type allow by-name types.

scala> val x: => Int = 3
<console>:1: error: identifier expected but '=>' found.
       val x: => Int = 3