Methods' composition

Consider

def gM(x: String, leftPar:Char, rightPar:Char): String = { "g"+leftPar+x+rightPar}
def fM(x: String): String = "f("+x+")"

Why this works

(fM _ andThen (gM(_: String, '[', ']')))("x")
//g[f(x)]

but not this

(fM _ andThen gM(_: String, '[', ']'))("x")

My previous attempt to explain how underscore works is here.

The underscore expands at the enclosing Expr which is not just a bare underscore.

f(_)

is

x => f(x)

as one might wish and not

f(x => x)

Here, “function argument” is an Expr.

f(_ + 1)

is

f(x => x + 1)

because the argument to f is not just an underscore, but an expression with underscore as a subexpression.

The nice feature of infix is that the operands are infix expressions and don’t “bind” the underscore.

Here is an example, although you wouldn’t normally write it this way:

List(42).flatMap(List(_ + 1))  // doesn't work, see previous

but

List(42).flatMap(List apply _ + 1)  // looks weird but works

In the working example, the underscore is not “bound” at the infix expressions, but at the enclosing expression which is the argument to flatMap. Normal precedence applies, so it means what the broken form looks like it should mean:

List(42).flatMap(x => List(x + 1))

Here is the function composition in Scala 3, where it’s easier to write f instead of f _:

scala> def g(s: String, left: Char, right: Char) = s"g$left$s$right"
def g(s: String, left: Char, right: Char): String

scala> def f(s:String) = s"f($s)"
def f(s: String): String

scala> f andThen g(_, '[', ']')
-- [E081] Type Error: --------------------------------------------------------------------------------------------------
1 |f andThen g(_, '[', ']')
  |            ^
  |            Missing parameter type
  |
  |            I could not infer the type of the parameter _$1 of expanded function:
  |            _$1 => f andThen g(_$1, '[', ']').
1 error found

scala> g(_, '[', ']')
val res0: String => String = Lambda$1450/0x00000008010c0410@6ceb11f9

scala> f andThen res0
val res1: String => String = scala.Function1$$Lambda$1469/0x00000008010dc520@31b650e9

scala> res1("X")
val res2: String = g[f(X)]

scala> f.andThen(g(_, '[', ']'))
val res3: String => String = scala.Function1$$Lambda$1469/0x00000008010dc520@6c06b1bc

This is the converse of the previous example, where the broken form doesn’t work because the infix expression which is the operand to andThen does not bind the underscore. Turning it into a regular argument in regular function call syntax makes it an Expr that does bind the underscore.

Both Scala 2 and 3 show the erroneous expansion of the broken form.

As a further nuance, x op y has infix expressions, but x op (y) turns the y into an Expr production, just the same as x.op(y). (x, y, z) is Exprs in parens, where it doesn’t matter whether you see them as args to a function or a tuple.

1 Like

I understand your explanation, but I wonder about the error messages (scala 2.13.8):

scala> (fF andThen gM(_: String, '[', ']'))("x")
                     ^
       error: type mismatch;
        found   : String
        required: String => ?

vs.

scala> (fF andThen gM(_, '[', ']'))("x")
                      ^
       error: missing parameter type for expanded function ((<x$1: error>) => fF.andThen(gM(x$1, '[', ']')))

The first one is totally confusing for me, whereas the second is quite informative and gives hints about the placeholder binding.

Would you say that in the infix notation a op b does not automatically translate to (a) op (b)?

a op b desugars to a.op(b)

We tend to say “desugar” to mean “syntax sweetener” with no change in behavior.

If we assume incorrectly that x is syntactically the same as (x), then we are confused why parens make a difference, because we’re used to wrapping subexpressions in parens for clarity.

The error message is pretty challenging. Here is an example session with Scala 3.

scala> f andThen g(_, '[', ']')
-- [E081] Type Error: --------------------------------------------------------------------------------------------------
1 |f andThen g(_, '[', ']')
  |            ^
  |            Missing parameter type
  |
  |            I could not infer the type of the parameter _$1 of expanded function:
  |            _$1 => f andThen g(_$1, '[', ']').
1 error found

scala> f andThen g(_: String, '[', ']')
-- [E007] Type Mismatch Error: -----------------------------------------------------------------------------------------
1 |f andThen g(_: String, '[', ']')
  |          ^^^^^^^^^^^^^^^^^^^^^^
  |          Found:    String
  |          Required: String => Any
  |
  | longer explanation available when compiling with `-explain`
1 error found

scala> :replay -explain
Unknown command: ":replay", run ":help" for a list of commands

scala> :help
The REPL has several commands available:

:help                    print this summary
:load <path>             interpret lines in a file
:quit                    exit the interpreter
:type <expression>       evaluate the type of the given expression
:doc <expression>        print the documentation for the given expression
:imports                 show import history
:reset [options]         reset the repl to its initial state, forgetting all session entries
:settings <options>      update compiler options, if possible


scala> :settings -explain

scala> f andThen g(_: String, '[', ']')
-- [E007] Type Mismatch Error: -----------------------------------------------------------------------------------------
1 |f andThen g(_: String, '[', ']')
  |          ^^^^^^^^^^^^^^^^^^^^^^
  |          Found:    String
  |          Required: String => Any
  |---------------------------------------------------------------------------------------------------------------------
  | Explanation (enabled by `-explain`)
  |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  |
  | Tree: g(_$1:String, '[', ']')
  | I tried to show that
  |   String
  | conforms to
  |   String => Nothing
  | but the comparison trace ended with `false`:
  |
  |   ==> String  <:  String => Nothing
  |     ==> String  <:  String => Nothing
  |     <== String  <:  String => Nothing = false
  |   <== String  <:  String => Nothing = false
  |
  | The tests were made under a constraint with:
  |  uninstantiated variables:
  |  constrained types: [A](g: String => A): String => A, [A](g: String => A): String => A
  |  bounds:
  |      A
  |      A
  |  ordering:
   ---------------------------------------------------------------------------------------------------------------------
1 error found

scala>

scala> :settings -explain -Vprint:parser,typer
Flag -explain set repeatedly

scala> f andThen g(_: String, '[', ']')
-- [E007] Type Mismatch Error: -----------------------------------------------------------------------------------------
1 |f andThen g(_: String, '[', ']')
  |          ^^^^^^^^^^^^^^^^^^^^^^
  |          Found:    String
  |          Required: String => Any
  |---------------------------------------------------------------------------------------------------------------------
  | Explanation (enabled by `-explain`)
  |- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
  |
  | Tree: g(_$1:String, '[', ']')
  | I tried to show that
  |   String
  | conforms to
  |   String => Nothing
  | but the comparison trace ended with `false`:
  |
  |   ==> String  <:  String => Nothing
  |     ==> String  <:  String => Nothing
  |     <== String  <:  String => Nothing = false
  |   <== String  <:  String => Nothing = false
  |
  | The tests were made under a constraint with:
  |  uninstantiated variables:
  |  constrained types: [A](g: String => A): String => A, [A](g: String => A): String => A
  |  bounds:
  |      A
  |      A
  |  ordering:
   ---------------------------------------------------------------------------------------------------------------------
[[syntax trees at end of                     typer]] // rs$line$6
package <empty> {
  final lazy module val rs$line$6: rs$line$6 = new rs$line$6()
  final module class rs$line$6() extends Object() { this: rs$line$6.type =>
    val res3: String => String => Nothing =
      {
        def $anonfun(_$1: String): String => Nothing =
          {
            def $anonfun(s: String): String = f(s)
            closure($anonfun)
          }.andThen[Nothing](g(_$1:String, '[', ']'))
        closure($anonfun)
      }
  }
}

1 error found

Here is the tree in Scala 2 under -Vprint:parser,typer:

scala> (f _) andThen g(_: String, '[', ']')
[[syntax trees at end of                    parser]] // <console>
package $line7 {
  sealed class $read extends _root_.scala.Serializable {
    def <init>() = {
      super.<init>();
      ()
    };
    val $line3$read: $line3.$read.INSTANCE.type = $line3.$read.INSTANCE;
    import $line3$read.$iw.g;
    val $line4$read: $line4.$read.INSTANCE.type = $line4.$read.INSTANCE;
    import $line4$read.$iw.f;
    sealed class $iw extends _root_.java.io.Serializable {
      def <init>() = {
        super.<init>();
        ()
      };
      val res2 = ((x$1: String) => (f: (() => <empty>)).andThen(g((x$1: String), '[', ']')))
    };
    val $iw = new $iw()
  };
  object $read extends scala.AnyRef {
    def <init>() = {
      super.<init>();
      ()
    };
    val INSTANCE = new $read()
  }
}

                      ^
       error: type mismatch;
        found   : String
        required: String => ?
[[syntax trees at end of                     typer]] // <console>
package $line7 {
  sealed class $read extends AnyRef with Serializable {
    def <init>(): $line7.$read = {
      $read.super.<init>();
      ()
    };
    private[this] val $line3$read: INSTANCE.type = $line3.$read.INSTANCE;
    <stable> <accessor> def $line3$read: INSTANCE.type = $read.this.$line3$read;
    import $read.this.$line3$read.$iw.g;
    private[this] val $line4$read: INSTANCE.type = $line4.$read.INSTANCE;
    <stable> <accessor> def $line4$read: INSTANCE.type = $read.this.$line4$read;
    import $read.this.$line4$read.$iw.f;
    sealed class $iw extends AnyRef with java.io.Serializable {
      def <init>(): $iw = {
        $iw.super.<init>();
        ()
      };
      private[this] val <res2: error>: String => <error> = ((x$1: String) => ((s: String) => $read.this.$line4$read.$iw.f(s)).andThen[A](g((x$1: String), '[', ']')));
      <stable> <accessor> def <res2: error>: String => <error> = $iw.this.<res2: error>
    };
    private[this] val $iw: $iw = new $read.this.$iw();
    <stable> <accessor> def $iw: $iw = $read.this.$iw
  };
  object $read extends scala.AnyRef with java.io.Serializable {
    def <init>(): type = {
      $read.super.<init>();
      ()
    };
    private[this] val INSTANCE: $line7.$read = new $read();
    <stable> <accessor> def INSTANCE: $line7.$read = $read.this.INSTANCE;
    <synthetic> private def writeReplace(): Object = new scala.runtime.ModuleSerializationProxy(classOf[$line7.$read$])
  }
}

It’s too bad the parser output comes before the error message and caret. Scala 3 underlines the whole expression, which is nice, while Scala 2 just puts the caret at the left paren of the application of g.

val res2 = ((x$1: String) => (f: (() => <empty>)).andThen(g((x$1: String), '[', ']')))

In this form, and maybe also in Scala 3’s closure notation, you can see that the String value it found is the result of g. Although Scala 3 underlines it, I agree that it’s confusing. It doesn’t tell us where a String value came from, and it doesn’t tell us what requires a String => Any or String => ?.

It could also detect that a function expansion is involved and show us the expansion. For “Missing parameter type”, it shows us the expansion in an addendum.

It could also notice that the expansion does not align with where a function type is expected but not found.

If it told us that much, -explain would really pull its weight.

1 Like

Thank you @som-snytt . I will study the debugging information, some messages reveal unexpected things :slight_smile: