diff --git a/_tour/higher-order-functions.md b/_tour/higher-order-functions.md index f60f910b8f..3756d6257c 100644 --- a/_tour/higher-order-functions.md +++ b/_tour/higher-order-functions.md @@ -20,27 +20,50 @@ In a pure Object Oriented world a good practice is to avoid exposing methods par One of the most common examples is the higher-order function `map` which is available for collections in Scala. -```scala mdoc -val salaries = Seq(20000, 70000, 40000) + +{% tabs map_example_1 class=tabs-scala-version %} + +{% tab 'Scala 2 and 3' for=map_example_1 %} +```scala +val salaries = Seq(20_000, 70_000, 40_000) val doubleSalary = (x: Int) => x * 2 val newSalaries = salaries.map(doubleSalary) // List(40000, 140000, 80000) ``` +{% endtab %} + +{% endtabs %} + `doubleSalary` is a function which takes a single Int, `x`, and returns `x * 2`. In general, the tuple on the left of the arrow `=>` is a parameter list and the value of the expression on the right is what gets returned. On line 3, the function `doubleSalary` gets applied to each element in the list of salaries. To shrink the code, we could make the function anonymous and pass it directly as an argument to map: -```scala:nest -val salaries = Seq(20000, 70000, 40000) + +{% tabs map_example_2 class=tabs-scala-version %} + +{% tab 'Scala 2 and 3' for=map_example_2 %} +```scala +val salaries = Seq(20_000, 70_000, 40_000) val newSalaries = salaries.map(x => x * 2) // List(40000, 140000, 80000) ``` +{% endtab %} + +{% endtabs %} + Notice how `x` is not declared as an Int in the above example. That's because the compiler can infer the type based on the type of function map expects (see [Currying](/tour/multiple-parameter-lists.html)). An even more idiomatic way to write the same piece of code would be: -```scala mdoc:nest -val salaries = Seq(20000, 70000, 40000) +{% tabs map_example_3 class=tabs-scala-version %} + +{% tab 'Scala 2 and 3' for=map_example_3 %} +```scala +val salaries = Seq(20_000, 70_000, 40_000) val newSalaries = salaries.map(_ * 2) ``` +{% endtab %} + +{% endtabs %} + Since the Scala compiler already knows the type of the parameters (a single Int), you just need to provide the right side of the function. The only caveat is that you need to use `_` in place of a parameter name (it was `x` in @@ -49,6 +72,10 @@ the previous example). ## Coercing methods into functions It is also possible to pass methods as arguments to higher-order functions because the Scala compiler will coerce the method into a function. + +{% tabs Coercing_methods_into_functions class=tabs-scala-version %} + +{% tab 'Scala 2' for=Coercing_methods_into_functions %} ```scala mdoc case class WeeklyWeatherForecast(temperatures: Seq[Double]) { @@ -57,6 +84,20 @@ case class WeeklyWeatherForecast(temperatures: Seq[Double]) { def forecastInFahrenheit: Seq[Double] = temperatures.map(convertCtoF) // <-- passing the method convertCtoF } ``` +{% endtab %} + +{% tab 'Scala 3' for=Coercing_methods_into_functions %} +```scala +case class WeeklyWeatherForecast(temperatures: Seq[Double]): + + private def convertCtoF(temp: Double) = temp * 1.8 + 32 + + def forecastInFahrenheit: Seq[Double] = temperatures.map(convertCtoF) // <-- passing the method convertCtoF +``` +{% endtab %} + +{% endtabs %} + Here the method `convertCtoF` is passed to the higher order function `map`. This is possible because the compiler coerces `convertCtoF` to the function `x => convertCtoF(x)` (note: `x` will be a generated name which is guaranteed to be unique within its scope). @@ -64,6 +105,9 @@ Here the method `convertCtoF` is passed to the higher order function `map`. This One reason to use higher-order functions is to reduce redundant code. Let's say you wanted some methods that could raise someone's salaries by various factors. Without creating a higher-order function, it might look something like this: +{% tabs Functions_that_accept_functions_1 class=tabs-scala-version %} + +{% tab 'Scala 2' for=Functions_that_accept_functions_1 %} ```scala mdoc object SalaryRaiser { @@ -77,10 +121,31 @@ object SalaryRaiser { salaries.map(salary => salary * salary) } ``` +{% endtab %} + +{% tab 'Scala 3' for=Functions_that_accept_functions_1 %} +```scala +object SalaryRaiser: + + def smallPromotion(salaries: List[Double]): List[Double] = + salaries.map(salary => salary * 1.1) + + def greatPromotion(salaries: List[Double]): List[Double] = + salaries.map(salary => salary * math.log(salary)) + + def hugePromotion(salaries: List[Double]): List[Double] = + salaries.map(salary => salary * salary) +``` +{% endtab %} + +{% endtabs %} Notice how each of the three methods vary only by the multiplication factor. To simplify, you can extract the repeated code into a higher-order function like so: +{% tabs Functions_that_accept_functions_2 class=tabs-scala-version %} + +{% tab 'Scala 2' for=Functions_that_accept_functions_2 %} ```scala mdoc:nest object SalaryRaiser { @@ -97,6 +162,27 @@ object SalaryRaiser { promotion(salaries, salary => salary * salary) } ``` +{% endtab %} + +{% tab 'Scala 3' for=Functions_that_accept_functions_2 %} +```scala +object SalaryRaiser: + + private def promotion(salaries: List[Double], promotionFunction: Double => Double): List[Double] = + salaries.map(promotionFunction) + + def smallPromotion(salaries: List[Double]): List[Double] = + promotion(salaries, salary => salary * 1.1) + + def greatPromotion(salaries: List[Double]): List[Double] = + promotion(salaries, salary => salary * math.log(salary)) + + def hugePromotion(salaries: List[Double]): List[Double] = + promotion(salaries, salary => salary * salary) +``` +{% endtab %} + +{% endtabs %} The new method, `promotion`, takes the salaries plus a function of type `Double => Double` (i.e. a function that takes a Double and returns a Double) and returns the product. @@ -108,6 +194,9 @@ Methods and functions usually express behaviours or data transformations, theref There are certain cases where you want to generate a function. Here's an example of a method that returns a function. +{% tabs Functions_that_return_functions class=tabs-scala-version %} + +{% tab 'Scala 2' for=Functions_that_return_functions %} ```scala mdoc def urlBuilder(ssl: Boolean, domainName: String): (String, String) => String = { val schema = if (ssl) "https://" else "http://" @@ -120,6 +209,23 @@ val endpoint = "users" val query = "id=1" val url = getURL(endpoint, query) // "https://www.example.com/users?id=1": String ``` +{% endtab %} + +{% tab 'Scala 3' for=Functions_that_return_functions %} +```scala +def urlBuilder(ssl: Boolean, domainName: String): (String, String) => String = + val schema = if ssl then "https://" else "http://" + (endpoint: String, query: String) => s"$schema$domainName/$endpoint?$query" + +val domainName = "www.example.com" +def getURL = urlBuilder(ssl=true, domainName) +val endpoint = "users" +val query = "id=1" +val url = getURL(endpoint, query) // "https://www.example.com/users?id=1": String +``` +{% endtab %} + +{% endtabs %} Notice the return type of urlBuilder `(String, String) => String`. This means that the returned anonymous function takes two Strings and returns a String. In this case,