Search code examples
scalafunctional-programmingcompositioncurryingpartial-application

Scala partial application via underscore when composing function literals


I am composing function literals, though unlike most examples I've seen I'm starting with a multi-argument function that is then curried.

I have:

//types
case class Thing1(v: Double)
case class Thing2(v: Double)
case class Thing3(v: Double)
type Multiplier = Double

//functions
val f1 = (t: Thing1, m: Multiplier) => Thing2(m * t.v)
val f2 = (t: Thing2) => Thing3(t.v)

I want to compose f1 and f2 to get a combined function

Thing1 => (Multiplier => Thing3)

As expected, the following doesn't compile:

val fcomposed1 = f1.curried.andThen(f2) // does not compile

By experimentation, I was able to work out that the following does compile and has the right signature for fcomposed:

val fcomposed2 = f1.curried(_:Thing1).andThen(f2)   

I've read various sources like What are all the uses of an underscore in Scala? and possibly relevant Why does Scala apply thunks automatically, sometimes? but unfortunately I still cannot work out exactly step-by-step what is happening here and why it works.

Furthermore, I would expect the above separated into two expressions to work identically to fcomposed2, however instead the second does not compile:

val f1partial = f1.curried(_:Thing1)
val fcomposed3 = f1partial.andThen(f2) // does not compile - same error as fcomposed1

Looks like f1partial returns the same signature as f1.curried, which makes me wonder further how the earlier fcomposed2 works.

Could someone please explain both behaviours step by step?


Solution

  • Here, the _ is acting as syntactical sugar for a lambda expression, but at a level you might not expect.

    f1.curried has type Thing1 => Multiplier => Thing2

    f1.curried(_:Thing1) is the same as { x: Thing1 => f1.curried(x) }. Since the result of f1.curried(x) has type Multiplier => Thing2, the final type of the whole expression is still Thing1 => Multiplier => Thing2. So it is not valid to call andThen(f2) on the result (f1partial) because the input type of function f2 (Thing2) is not the same as the output of the previous function (Multiplier => Thing2).

    By contrast, f1.curried(_:Thing1).andThen(f2) expands to { x: Thing1 => f1.curried(x).andThen(f2) }. f1.curried(x) evaluates to type Multiplier => Thing2, like stated earlier, so you can call andThen(f2) on that, resulting in a Multiplier => Thing3. So then the entire expression evaluates to a Thing1 => Multiplier => Thing3

    Perhaps it's more clear if you think about the differences between these two expressions:

    val fcomposed1 = { x: Thing1 => f1.curried(x).andThen(f2) } // valid
    val fcomposed2 = { x: Thing1 => f1.curried(x) }.andThen(f2) // error