3

I have the following code

def call1 = 4
call1 _

and get the following warning:

Methods without a parameter list and by-name params can no longer be converted to functions as m _, write a function literal () => m instead

I am not sure to fully grasp why and part of the sentence here. Obviously i do not have a by-name parameter here.

First of all is the warning splitable in two as such:

1)

Methods without a parameter list can no longer be converted to functions as m _, write a function literal () => m instead

by-name params can no longer be converted to functions as m _, write a function literal () => m instead

I am seeking an explanation for 1) and 2). And on point 2, I have a hard time visualising why and when a by-name params would be converted to functions.

halfer
  • 19,824
  • 17
  • 99
  • 186
MaatDeamon
  • 9,532
  • 9
  • 60
  • 127
  • It is not exactly the same, but might be helpful: [Getting MatchError when using a placeholder for an unused variable](https://stackoverflow.com/q/63701707/2359227) – Tomer Shetah Jan 13 '21 at 14:07
  • For the first one, the language simply no longer allows you to convert a nullary method into a function, why? I would guess simplicity. For the second point, **Scala 2** doesn't allow by-name parameters on **functions** only in **methods**, so the conversion would lose the laziness of that, as such the language maintainers decided to stop doing that under the hod and maybe surprising users and just force them to make that by themselves. – Luis Miguel Mejía Suárez Jan 13 '21 at 14:19
  • So you r saying point 2 is actually: methods with by-name parameter can no longer be eta_ expanded ? – MaatDeamon Jan 13 '21 at 14:58
  • On point one: what is the simplicity gain you see ? – MaatDeamon Jan 13 '21 at 15:00
  • 2
    @MaatDeamon they never could be eta-expanded in the sense that **functions** in **Scala 2** _(not sure about **Scala 3**)_ do not support by-name parameters. Previously what the compiler did, was create a normal function that referred to the method, as such the arguments that were supposed to be lazy were eagerly evaluated, which probably surprised people, so I guess the decision from the compiler team was to stop doing that and forcing users to make that explicitly so they are aware of the loose of lazyness. For the first point, I do not know the code of the compiler but it is way complex. – Luis Miguel Mejía Suárez Jan 13 '21 at 15:06
  • 1
    @LuisMiguelMejíaSuárez I am not sure I understand you right. I think I can use by-name parameters in functions without any issues, see https://scastie.scala-lang.org/BQfdhZ0iRwOvczueVKHXqg or https://scastie.scala-lang.org/v7Jrm5yjSIi2GTJeurZq2Q – Suma Jan 13 '21 at 17:25
  • @Suma you are right although your example didn't prove me wrong; but, as I have done all my life, [I did prove me wrong](https://scastie.scala-lang.org/BalmungSan/qWCZad5cS4i4Q6GltR1kcg/2). I always forgot that the `=> Foo` type exists but is only valid on parameters. - I guess this all boils down to simplicity and avoid confusions as Mateusz explained. – Luis Miguel Mejía Suárez Jan 13 '21 at 17:35

1 Answers1

5

Both can be demonstrated by something like:

def needFunction(f: () => String) = {
  println("calling function")
  println(f())
}

def paramLess: String = {
  println("calculating value")
  "foo"
}

I want to pass paramLess into needFunction. When will it be evaluates?

needFunction(() => paramLess) // (1)
// calling function
// calculating value
// foo

This is obvious - paramLess will be evaluated inside needFunction. But this?

needFunction(paramLess _) // (2)

(2) is the same as (1) but I needed to take a moment to think how it would behave and run it in REPL to be sure.

This would happen quite often, especially with by-name params - you want to make some API easy to use by the user so you take thunk: => A as argument, but internally you are passing () => A to know exactly when you are evaluating (passing one by-name into another by-name and so on hoping that nothing in between evaluates results in some very fragile code).

Functions and methods with parameter don't have readability issues - if you don't put () anywhere it is not evaluated. You can compose it, pass it, return it - but if there is no argument passed, there is no evaluation. Paramless methods evaluate every time you use their name, so they are tricky if they perform side-effects or some expensive computations - that however is avoided and discouraged, as you are suggested to add at least () parameter list to side-effecting method. At the same time, they kind of pretend to be values, which don't need arguments. Apparently, here community decided that if they want to pretend to be values, they should be used as values, with () => value as the way of creating a function - makes sense if we will not use them for side-effects.

By-name params are a similar case plus when it comes to readability, but - from what I saw - they have a lot of corner cases resulting in bugs when combined with eta-expansion and they also quite often (almost always?) appear in the context of side effects

def inSomeContext(context: Arguments)(thunk: => SideEffects) = ...
// Try.apply
// IO.apply
// option.fold(noneByName)(some => ...)
// ...

This makes them even more dangerous than paramless methods (where side-effects are discouraged) so a warning is even more justified.

The bottom line is that it is mostly about readability, safety and avoiding surprises:

  • use methods without parameters when they can be uses as values (initialization order might be one reason) without side effects and surprises (there are more warnings that warns against side-effects in paramless methods),
  • and by-name params as something that is nice to use in your API, but that is internally immediately and manually converted to a function (even () => A kind of function) or value (val evaluated = thunk) that you would pass on safely.

Warning against other usages of these help keep the code readable and without bad surprises.

Mateusz Kubuszok
  • 24,995
  • 4
  • 42
  • 64