diff --git a/_overviews/scala3-book/control-structures.md b/_overviews/scala3-book/control-structures.md index 61a315ffab..ce62617865 100644 --- a/_overviews/scala3-book/control-structures.md +++ b/_overviews/scala3-book/control-structures.md @@ -2,7 +2,7 @@ title: Control Structures type: chapter description: This page provides an introduction to Scala's control structures, including if/then/else, 'for' loops, 'for' expressions, 'match' expressions, try/catch/finally, and 'while' loops. -languages: [zh-cn] +languages: [ru, zh-cn] num: 18 previous-page: first-look-at-types next-page: domain-modeling-intro diff --git a/_overviews/scala3-book/domain-modeling-fp.md b/_overviews/scala3-book/domain-modeling-fp.md index 669f5269eb..f141548bb0 100644 --- a/_overviews/scala3-book/domain-modeling-fp.md +++ b/_overviews/scala3-book/domain-modeling-fp.md @@ -2,7 +2,7 @@ title: FP Modeling type: section description: This chapter provides an introduction to FP domain modeling with Scala 3. -languages: [zh-cn] +languages: [ru, zh-cn] num: 22 previous-page: domain-modeling-oop next-page: methods-intro diff --git a/_overviews/scala3-book/domain-modeling-intro.md b/_overviews/scala3-book/domain-modeling-intro.md index 1089997a9b..578cf001df 100644 --- a/_overviews/scala3-book/domain-modeling-intro.md +++ b/_overviews/scala3-book/domain-modeling-intro.md @@ -2,7 +2,7 @@ title: Domain Modeling type: chapter description: This chapter provides an introduction to domain modeling in Scala 3. -languages: [zh-cn] +languages: [ru, zh-cn] num: 19 previous-page: control-structures next-page: domain-modeling-tools diff --git a/_overviews/scala3-book/domain-modeling-oop.md b/_overviews/scala3-book/domain-modeling-oop.md index 304dca159b..8a55131a20 100644 --- a/_overviews/scala3-book/domain-modeling-oop.md +++ b/_overviews/scala3-book/domain-modeling-oop.md @@ -2,7 +2,7 @@ title: OOP Modeling type: section description: This chapter provides an introduction to OOP domain modeling with Scala 3. -languages: [zh-cn] +languages: [ru, zh-cn] num: 21 previous-page: domain-modeling-tools next-page: domain-modeling-fp diff --git a/_overviews/scala3-book/domain-modeling-tools.md b/_overviews/scala3-book/domain-modeling-tools.md index 2c485e3a5f..a5c02b2a49 100644 --- a/_overviews/scala3-book/domain-modeling-tools.md +++ b/_overviews/scala3-book/domain-modeling-tools.md @@ -2,7 +2,7 @@ title: Tools type: section description: This chapter provides an introduction to the available domain modeling tools in Scala 3, including classes, traits, enums, and more. -languages: [zh-cn] +languages: [ru, zh-cn] num: 20 previous-page: domain-modeling-intro next-page: domain-modeling-oop diff --git a/_overviews/scala3-book/fun-anonymous-functions.md b/_overviews/scala3-book/fun-anonymous-functions.md index fbf589e7a3..faf32eb76d 100644 --- a/_overviews/scala3-book/fun-anonymous-functions.md +++ b/_overviews/scala3-book/fun-anonymous-functions.md @@ -2,7 +2,7 @@ title: Anonymous Functions type: section description: This page shows how to use anonymous functions in Scala, including examples with the List class 'map' and 'filter' functions. -languages: [zh-cn] +languages: [ru, zh-cn] num: 28 previous-page: fun-intro next-page: fun-function-variables diff --git a/_overviews/scala3-book/fun-eta-expansion.md b/_overviews/scala3-book/fun-eta-expansion.md index e9590a65f0..ab483c5d48 100644 --- a/_overviews/scala3-book/fun-eta-expansion.md +++ b/_overviews/scala3-book/fun-eta-expansion.md @@ -2,7 +2,7 @@ title: Eta Expansion type: section description: This page discusses Eta Expansion, the Scala technology that automatically and transparently converts methods into functions. -languages: [zh-cn] +languages: [ru, zh-cn] num: 30 previous-page: fun-function-variables next-page: fun-hofs diff --git a/_overviews/scala3-book/fun-function-variables.md b/_overviews/scala3-book/fun-function-variables.md index 29d1a755df..70805a5240 100644 --- a/_overviews/scala3-book/fun-function-variables.md +++ b/_overviews/scala3-book/fun-function-variables.md @@ -1,8 +1,8 @@ --- title: Function Variables type: section -description: This page shows how to use anonymous functions in Scala, including examples with the List class 'map' and 'filter' functions. -languages: [zh-cn] +description: This page shows how to use function variables in Scala. +languages: [ru, zh-cn] num: 29 previous-page: fun-anonymous-functions next-page: fun-eta-expansion diff --git a/_overviews/scala3-book/fun-hofs.md b/_overviews/scala3-book/fun-hofs.md index 484193a4c2..90ebba7396 100644 --- a/_overviews/scala3-book/fun-hofs.md +++ b/_overviews/scala3-book/fun-hofs.md @@ -2,7 +2,7 @@ title: Higher-Order Functions type: section description: This page demonstrates how to create and use higher-order functions in Scala. -languages: [zh-cn] +languages: [ru, zh-cn] num: 31 previous-page: fun-eta-expansion next-page: fun-write-map-function diff --git a/_overviews/scala3-book/fun-intro.md b/_overviews/scala3-book/fun-intro.md index e2b9cd798f..aa4345a188 100644 --- a/_overviews/scala3-book/fun-intro.md +++ b/_overviews/scala3-book/fun-intro.md @@ -2,7 +2,7 @@ title: Functions type: chapter description: This chapter looks at all topics related to functions in Scala 3. -languages: [zh-cn] +languages: [ru, zh-cn] num: 27 previous-page: methods-summary next-page: fun-anonymous-functions diff --git a/_overviews/scala3-book/fun-summary.md b/_overviews/scala3-book/fun-summary.md index 4b23367b47..99089f5dec 100644 --- a/_overviews/scala3-book/fun-summary.md +++ b/_overviews/scala3-book/fun-summary.md @@ -1,8 +1,8 @@ --- title: Summary type: section -description: This page shows how to use anonymous functions in Scala, including examples with the List class 'map' and 'filter' functions. -languages: [zh-cn] +description: This page provides a summary of the previous 'Functions' sections. +languages: [ru, zh-cn] num: 34 previous-page: fun-write-method-returns-function next-page: packaging-imports diff --git a/_overviews/scala3-book/fun-write-map-function.md b/_overviews/scala3-book/fun-write-map-function.md index bc3114fcf3..c7c9234760 100644 --- a/_overviews/scala3-book/fun-write-map-function.md +++ b/_overviews/scala3-book/fun-write-map-function.md @@ -2,7 +2,7 @@ title: Write Your Own map Method type: section description: This page demonstrates how to create and use higher-order functions in Scala. -languages: [zh-cn] +languages: [ru, zh-cn] num: 32 previous-page: fun-hofs next-page: fun-write-method-returns-function diff --git a/_overviews/scala3-book/fun-write-method-returns-function.md b/_overviews/scala3-book/fun-write-method-returns-function.md index 6ca9f31002..eb2bb039a1 100644 --- a/_overviews/scala3-book/fun-write-method-returns-function.md +++ b/_overviews/scala3-book/fun-write-method-returns-function.md @@ -2,7 +2,7 @@ title: Creating a Method That Returns a Function type: section description: This page demonstrates how to create and use higher-order functions in Scala. -languages: [zh-cn] +languages: [ru, zh-cn] num: 33 previous-page: fun-write-map-function next-page: fun-summary diff --git a/_overviews/scala3-book/methods-intro.md b/_overviews/scala3-book/methods-intro.md index 9170ec39b2..2bf6be1247 100644 --- a/_overviews/scala3-book/methods-intro.md +++ b/_overviews/scala3-book/methods-intro.md @@ -2,7 +2,7 @@ title: Methods type: chapter description: This section introduces methods in Scala 3. -languages: [zh-cn] +languages: [ru, zh-cn] num: 23 previous-page: domain-modeling-fp next-page: methods-most diff --git a/_overviews/scala3-book/methods-main-methods.md b/_overviews/scala3-book/methods-main-methods.md index acc0cc1c0b..3415ef3496 100644 --- a/_overviews/scala3-book/methods-main-methods.md +++ b/_overviews/scala3-book/methods-main-methods.md @@ -2,7 +2,7 @@ title: Main Methods in Scala 3 type: section description: This page describes how 'main' methods and the '@main' annotation work in Scala 3. -languages: [zh-cn] +languages: [ru, zh-cn] num: 25 previous-page: methods-most next-page: methods-summary diff --git a/_overviews/scala3-book/methods-most.md b/_overviews/scala3-book/methods-most.md index 3551a96fe0..a1d17c318e 100644 --- a/_overviews/scala3-book/methods-most.md +++ b/_overviews/scala3-book/methods-most.md @@ -2,7 +2,7 @@ title: Method Features type: section description: This section introduces Scala 3 methods, including main methods, extension methods, and more. -languages: [zh-cn] +languages: [ru, zh-cn] num: 24 previous-page: methods-intro next-page: methods-main-methods diff --git a/_overviews/scala3-book/methods-summary.md b/_overviews/scala3-book/methods-summary.md index 1148e37160..54abeef761 100644 --- a/_overviews/scala3-book/methods-summary.md +++ b/_overviews/scala3-book/methods-summary.md @@ -2,7 +2,7 @@ title: Summary type: section description: This section summarizes the previous sections on Scala 3 methods. -languages: [zh-cn] +languages: [ru, zh-cn] num: 26 previous-page: methods-main-methods next-page: fun-intro diff --git a/_ru/scala3/book/control-structures.md b/_ru/scala3/book/control-structures.md new file mode 100644 index 0000000000..b72b476278 --- /dev/null +++ b/_ru/scala3/book/control-structures.md @@ -0,0 +1,1016 @@ +--- +layout: multipage-overview +title: Структуры управления +scala3: true +partof: scala3-book +overview-name: "Scala 3 — Book" +type: chapter +description: На этой странице представлено введение в структуры управления Scala, включая if/then/else, циклы for, выражения for, выражения match, try/catch/finally и циклы while. +language: ru +num: 18 +previous-page: first-look-at-types +next-page: domain-modeling-intro +--- + + +В Scala есть все структуры управления, которые вы ожидаете найти в языке программирования, в том числе: + +- `if`/`then`/`else` +- циклы `for` +- циклы `while` +- `try`/`catch`/`finally` + +Здесь также есть две другие мощные конструкции, которые вы, возможно, не видели раньше, +в зависимости от вашего опыта программирования: + +- `for` выражения (также известные как _`for` comprehensions_) +- `match` выражения + +Все они продемонстрированы в следующих разделах. + + +## Конструкция if/then/else + +Однострочный Scala оператор `if` выглядит так: + +{% tabs control-structures-1 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-1 %} +```scala +if (x == 1) println(x) +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-1 %} +```scala +if x == 1 then println(x) +``` +{% endtab %} +{% endtabs %} + +Когда необходимо выполнить несколько строк кода после `if`, используется синтаксис: + +{% tabs control-structures-2 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-2 %} +```scala +if (x == 1) { + println("x is 1, as you can see:") + println(x) +} +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-2 %} +```scala +if x == 1 then + println("x is 1, as you can see:") + println(x) +``` +{% endtab %} +{% endtabs %} + +`if`/`else` синтаксис выглядит так: + +{% tabs control-structures-3 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-3 %} +```scala +if (x == 1) { + println("x is 1, as you can see:") + println(x) +} else { + println("x was not 1") +} +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-3 %} +```scala +if x == 1 then + println("x is 1, as you can see:") + println(x) +else + println("x was not 1") +``` +{% endtab %} +{% endtabs %} + +А это синтаксис `if`/`else if`/`else`: + +{% tabs control-structures-4 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-4 %} +```scala +if (x < 0) + println("negative") +else if (x == 0) + println("zero") +else + println("positive") +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-4 %} +```scala +if x < 0 then + println("negative") +else if x == 0 then + println("zero") +else + println("positive") +``` +{% endtab %} +{% endtabs %} + +### Утверждение `end if` + +
+  Это новое в Scala 3 и не поддерживается в Scala 2. +
+ +При желании можно дополнительно включить оператор `end if` в конце каждого выражения: +```scala +if x == 1 then + println("x is 1, as you can see:") + println(x) +end if +``` + +### `if`/`else` выражения всегда возвращают результат + +Сравнения `if`/`else` образуют _выражения_ - это означает, что они возвращают значение, которое можно присвоить переменной. +Поэтому нет необходимости в специальном тернарном операторе: + +{% tabs control-structures-6 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-6 %} +```scala +val minValue = if (a < b) a else b +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-6 %} +```scala +val minValue = if a < b then a else b +``` +{% endtab %} +{% endtabs %} + + +Поскольку эти выражения возвращают значение, то выражения `if`/`else` можно использовать в качестве тела метода: + +{% tabs control-structures-7 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-7 %} +```scala +def compare(a: Int, b: Int): Int = + if (a < b) + -1 + else if (a == b) + 0 + else + 1 +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-7 %} +```scala +def compare(a: Int, b: Int): Int = + if a < b then + -1 + else if a == b then + 0 + else + 1 +``` +{% endtab %} +{% endtabs %} + +### В сторону: программирование, ориентированное на выражения + +Кратко о программировании в целом: когда каждое написанное вами выражение возвращает значение, +такой стиль называется _программированием, ориентированным на выражения_, +или EOP (_expression-oriented programming_). +Например, это _выражение_: + +{% tabs control-structures-8 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-8 %} +```scala +val minValue = if (a < b) a else b +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-8 %} +```scala +val minValue = if a < b then a else b +``` +{% endtab %} +{% endtabs %} + +И наоборот, строки кода, которые не возвращают значения, называются _операторами_ +и используются для получения _побочных эффектов_. +Например, эти строки кода не возвращают значения, поэтому они используются для побочных эффектов: + +{% tabs control-structures-9 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-9 %} +```scala +if (a == b) action() +println("Hello") +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-9 %} +```scala +if a == b then action() +println("Hello") +``` +{% endtab %} +{% endtabs %} + +В первом примере метод `action` запускается как побочный эффект, когда `a` равно `b`. +Второй пример используется для побочного эффекта печати строки в STDOUT. +Когда вы узнаете больше о Scala, то обнаружите, что пишете больше _выражений_ и меньше _операторов_. + +## Циклы `for` + +В самом простом случае цикл `for` в Scala можно использовать для перебора элементов в коллекции. +Например, имея последовательность целых чисел, +вы можете перебрать ее элементы и вывести их значения следующим образом: + +{% tabs control-structures-10 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-10 %} +```scala +val ints = Seq(1, 2, 3) +for (i <- ints) println(i) +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-10 %} +```scala +val ints = Seq(1, 2, 3) +for i <- ints do println(i) +``` +{% endtab %} +{% endtabs %} + + +Код `i <- ints` называется _генератором_. + +Вот как выглядит результат в Scala REPL: + +{% tabs control-structures-11 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-11 %} +```` +scala> val ints = Seq(1,2,3) +ints: Seq[Int] = List(1, 2, 3) + +scala> for (i <- ints) println(i) +1 +2 +3 +```` +{% endtab %} +{% tab 'Scala 3' for=control-structures-11 %} +```` +scala> val ints = Seq(1,2,3) +ints: Seq[Int] = List(1, 2, 3) + +scala> for i <- ints do println(i) +1 +2 +3 +```` +{% endtab %} +{% endtabs %} + + +Если вам нужен многострочный блок кода после генератора `for`, используйте следующий синтаксис: + +{% tabs control-structures-12 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-12 %} +```scala +for (i <- ints) { + val x = i * 2 + println(s"i = $i, x = $x") +} +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-12 %} +```scala +for i <- ints +do + val x = i * 2 + println(s"i = $i, x = $x") +``` +{% endtab %} +{% endtabs %} + + +### Несколько генераторов + +Циклы `for` могут иметь несколько генераторов, как показано в этом примере: + +{% tabs control-structures-13 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-13 %} +```scala +for { + i <- 1 to 2 + j <- 'a' to 'b' + k <- 1 to 10 by 5 +} { + println(s"i = $i, j = $j, k = $k") +} +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-13 %} +```scala +for + i <- 1 to 2 + j <- 'a' to 'b' + k <- 1 to 10 by 5 +do + println(s"i = $i, j = $j, k = $k") +``` +{% endtab %} +{% endtabs %} + + +Это выражение выводит следующее: + +```` +i = 1, j = a, k = 1 +i = 1, j = a, k = 6 +i = 1, j = b, k = 1 +i = 1, j = b, k = 6 +i = 2, j = a, k = 1 +i = 2, j = a, k = 6 +i = 2, j = b, k = 1 +i = 2, j = b, k = 6 +```` + +### "Стражники" + +Циклы `for` также могут содержать операторы `if`, известные как _стражники_ (_guards_): + +{% tabs control-structures-14 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-14 %} +```scala +for { + i <- 1 to 5 + if i % 2 == 0 +} { + println(i) +} +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-14 %} +```scala +for + i <- 1 to 5 + if i % 2 == 0 +do + println(i) +``` +{% endtab %} +{% endtabs %} + + +Результат этого цикла: + +```` +2 +4 +```` + +Цикл `for` может содержать столько стражников, сколько необходимо. +В этом примере показан один из способов печати числа `4`: + +{% tabs control-structures-15 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-15 %} +```scala +for { + i <- 1 to 10 + if i > 3 + if i < 6 + if i % 2 == 0 +} { + println(i) +} +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-15 %} +```scala +for + i <- 1 to 10 + if i > 3 + if i < 6 + if i % 2 == 0 +do + println(i) +``` +{% endtab %} +{% endtabs %} + +### Использование `for` с Maps + +Вы также можете использовать циклы `for` с `Map`. +Например, если задана такая `Map` с аббревиатурами штатов и их полными названиями: + +{% tabs map %} +{% tab 'Scala 2 и 3' for=map %} +```scala +val states = Map( + "AK" -> "Alaska", + "AL" -> "Alabama", + "AR" -> "Arizona" +) +``` +{% endtab %} +{% endtabs %} + +, то можно распечатать ключи и значения, используя `for`. Например: + +{% tabs control-structures-16 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-16 %} +```scala +for ((abbrev, fullName) <- states) println(s"$abbrev: $fullName") +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-16 %} +```scala +for (abbrev, fullName) <- states do println(s"$abbrev: $fullName") +``` +{% endtab %} +{% endtabs %} + +Вот как это выглядит в REPL: + +{% tabs control-structures-17 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-17 %} +```scala +scala> for ((abbrev, fullName) <- states) println(s"$abbrev: $fullName") +AK: Alaska +AL: Alabama +AR: Arizona +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-17 %} +```scala +scala> for (abbrev, fullName) <- states do println(s"$abbrev: $fullName") +AK: Alaska +AL: Alabama +AR: Arizona +``` +{% endtab %} +{% endtabs %} + +Когда цикл `for` перебирает карту, каждая пара ключ/значение привязывается +к переменным `abbrev` и `fullName`, которые находятся в кортеже: + +```scala +(abbrev, fullName) <- states +``` + +По мере выполнения цикла переменная `abbrev` принимает значение текущего _ключа_ в карте, +а переменная `fullName` - соответствующему ключу _значению_. + +## Выражение `for` + +В предыдущих примерах циклов `for` все эти циклы использовались для _побочных эффектов_, +в частности для вывода этих значений в STDOUT с помощью `println`. + +Важно знать, что вы также можете создавать _выражения_ `for`, которые возвращают значения. +Вы создаете выражение `for`, добавляя ключевое слово `yield` и возвращаемое выражение, например: + +{% tabs control-structures-18 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-18 %} +```scala +val list = + for (i <- 10 to 12) + yield i * 2 + +// list: IndexedSeq[Int] = Vector(20, 22, 24) +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-18 %} +```scala +val list = + for i <- 10 to 12 + yield i * 2 + +// list: IndexedSeq[Int] = Vector(20, 22, 24) +``` +{% endtab %} +{% endtabs %} + + +После выполнения этого выражения `for` переменная `list` содержит `Vector` с отображаемыми значениями. +Вот как работает выражение: + +1. Выражение `for` начинает перебирать значения в диапазоне `(10, 11, 12)`. + Сначала оно работает со значением `10`, умножает его на `2`, затем выдает результат - `20`. +2. Далее берет `11` — второе значение в диапазоне. Умножает его на `2`, а затем выдает значение `22`. + Можно представить эти полученные значения как накопление во временном хранилище. +3. Наконец, цикл берет число `12` из диапазона, умножает его на `2`, получая число `24`. + Цикл завершается в этой точке и выдает конечный результат - `Vector(20, 22, 24)`. + +Хотя целью этого раздела является демонстрация выражений `for`, полезно знать, +что показанное выражение `for` эквивалентно вызову метода `map`: + +{% tabs map-call %} +{% tab 'Scala 2 и 3' for=map-call %} +```scala +val list = (10 to 12).map(i => i * 2) +``` +{% endtab %} +{% endtabs %} + +Выражения `for` можно использовать в любой момент, когда вам нужно просмотреть все элементы в коллекции +и применить алгоритм к этим элементам для создания нового списка. + +Вот пример, который показывает, как использовать блок кода после `yield`: + +{% tabs control-structures-19 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-19 %} +```scala +val names = List("_olivia", "_walter", "_peter") + +val capNames = for (name <- names) yield { + val nameWithoutUnderscore = name.drop(1) + val capName = nameWithoutUnderscore.capitalize + capName +} + +// capNames: List[String] = List(Olivia, Walter, Peter) +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-19 %} +```scala +val names = List("_olivia", "_walter", "_peter") + +val capNames = for name <- names yield + val nameWithoutUnderscore = name.drop(1) + val capName = nameWithoutUnderscore.capitalize + capName + +// capNames: List[String] = List(Olivia, Walter, Peter) +``` +{% endtab %} +{% endtabs %} + +### Использование выражения `for` в качестве тела метода + + +Поскольку выражение `for` возвращает результат, его можно использовать в качестве тела метода, +который возвращает полезное значение. +Этот метод возвращает все значения в заданном списке целых чисел, которые находятся между `3` и `10`: + +{% tabs control-structures-20 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-20 %} +```scala +def between3and10(xs: List[Int]): List[Int] = + for { + x <- xs + if x >= 3 + if x <= 10 + } yield x + +between3and10(List(1, 3, 7, 11)) // : List[Int] = List(3, 7) +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-20 %} +```scala +def between3and10(xs: List[Int]): List[Int] = + for + x <- xs + if x >= 3 + if x <= 10 + yield x + +between3and10(List(1, 3, 7, 11)) // : List[Int] = List(3, 7) +``` +{% endtab %} +{% endtabs %} + +## Циклы `while` + +Синтаксис цикла `while` в Scala выглядит следующим образом: + +{% tabs control-structures-21 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-21 %} +```scala +var i = 0 + +while (i < 3) { + println(i) + i += 1 +} +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-21 %} +```scala +var i = 0 + +while i < 3 do + println(i) + i += 1 +``` +{% endtab %} +{% endtabs %} + +## `match` выражения + +Сопоставление с образцом (_pattern matching_) является основой функциональных языков программирования. +Scala включает в себя pattern matching, обладающий множеством возможностей. + +В самом простом случае можно использовать выражение `match`, подобное оператору Java `switch`, +сопоставляя на основе целочисленного значения. +Как и предыдущие структуры, сопоставление с образцом - это действительно выражение, поскольку оно вычисляет результат: + +{% tabs control-structures-22 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-22 %} +```scala +// `i` is an integer +val day = i match { + case 0 => "Sunday" + case 1 => "Monday" + case 2 => "Tuesday" + case 3 => "Wednesday" + case 4 => "Thursday" + case 5 => "Friday" + case 6 => "Saturday" + case _ => "invalid day" // по умолчанию, перехватывает остальное +} +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-22 %} +```scala +// `i` is an integer +val day = i match + case 0 => "Sunday" + case 1 => "Monday" + case 2 => "Tuesday" + case 3 => "Wednesday" + case 4 => "Thursday" + case 5 => "Friday" + case 6 => "Saturday" + case _ => "invalid day" // по умолчанию, перехватывает остальное +``` +{% endtab %} +{% endtabs %} + + +В этом примере переменная `i` сопоставляется с заданными числами. +Если она находится между `0` и `6`, `day` принимает значение строки, представляющей день недели. +В противном случае она соответствует подстановочному знаку, представленному символом `_`, +и `day` принимает значение строки `"invalid day"`. + +Поскольку сопоставляемые значения рассматриваются в том порядке, в котором они заданы, +и используется первое совпадение, +случай по умолчанию, соответствующий любому значению, должен идти последним. +Любые сопоставляемые случаи после значения по умолчанию будут помечены как недоступные и будет выведено предупреждение. + +> При написании простых выражений соответствия, подобных этому, рекомендуется использовать аннотацию `@switch` для переменной `i`. +> Эта аннотация содержит предупреждение во время компиляции, если switch не может быть скомпилирован в `tableswitch` +> или `lookupswitch`, которые лучше подходят с точки зрения производительности. + + +### Использование значения по умолчанию + +Когда необходимо получить доступ к универсальному значению по умолчанию в `match` выражении, +просто укажите вместо `_` имя переменной в левой части оператора `case`, +а затем используйте это имя переменной в правой части оператора при необходимости: + +{% tabs control-structures-23 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-23 %} +```scala +i match { + case 0 => println("1") + case 1 => println("2") + case what => println(s"You gave me: $what") +} +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-23 %} +```scala +i match + case 0 => println("1") + case 1 => println("2") + case what => println(s"You gave me: $what") +``` +{% endtab %} +{% endtabs %} + +Имя, используемое в шаблоне, должно начинаться со строчной буквы. +Имя, начинающееся с заглавной буквы, не представляет собой новую переменную, +но соответствует значению в области видимости: + +{% tabs control-structures-24 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-24 %} +```scala +val N = 42 +i match { + case 0 => println("1") + case 1 => println("2") + case N => println("42") + case n => println(s"You gave me: $n" ) +} +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-24 %} +```scala +val N = 42 +i match + case 0 => println("1") + case 1 => println("2") + case N => println("42") + case n => println(s"You gave me: $n" ) +``` +{% endtab %} +{% endtabs %} + +Если `i` равно `42`, то оно будет соответствовать `case N` и напечатает строку `"42"`. +И не достигнет случая по умолчанию. + +### Обработка нескольких возможных совпадений в одной строке + +Как уже упоминалось, `match` выражения многофункциональны. +В этом примере показано, как в каждом операторе `case` использовать несколько возможных совпадений: + +{% tabs control-structures-25 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-25 %} +```scala +val evenOrOdd = i match { + case 1 | 3 | 5 | 7 | 9 => println("odd") + case 2 | 4 | 6 | 8 | 10 => println("even") + case _ => println("some other number") +} +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-25 %} +```scala +val evenOrOdd = i match + case 1 | 3 | 5 | 7 | 9 => println("odd") + case 2 | 4 | 6 | 8 | 10 => println("even") + case _ => println("some other number") +``` +{% endtab %} +{% endtabs %} + +### Использование `if` стражников в `case` предложениях + +В case выражениях также можно использовать стражников. +В этом примере второй и третий case используют стражников для сопоставления нескольких целочисленных значений: + +{% tabs control-structures-26 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-26 %} +```scala +i match { + case 1 => println("one, a lonely number") + case x if x == 2 || x == 3 => println("two’s company, three’s a crowd") + case x if x > 3 => println("4+, that’s a party") + case _ => println("i’m guessing your number is zero or less") +} +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-26 %} +```scala +i match + case 1 => println("one, a lonely number") + case x if x == 2 || x == 3 => println("two’s company, three’s a crowd") + case x if x > 3 => println("4+, that’s a party") + case _ => println("i’m guessing your number is zero or less") +``` +{% endtab %} +{% endtabs %} + + +Вот еще один пример, который показывает, как сопоставить заданное значение с диапазоном чисел: + +{% tabs control-structures-27 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-27 %} +```scala +i match { + case a if 0 to 9 contains a => println(s"0-9 range: $a") + case b if 10 to 19 contains b => println(s"10-19 range: $b") + case c if 20 to 29 contains c => println(s"20-29 range: $c") + case _ => println("Hmmm...") +} +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-27 %} +```scala +i match + case a if 0 to 9 contains a => println(s"0-9 range: $a") + case b if 10 to 19 contains b => println(s"10-19 range: $b") + case c if 20 to 29 contains c => println(s"20-29 range: $c") + case _ => println("Hmmm...") +``` +{% endtab %} +{% endtabs %} + + +#### Case классы и сопоставление с образцом + +Вы также можете извлекать поля из `case` классов — и классов, которые имеют корректно написанные методы `apply`/`unapply` — +и использовать их в своих условиях. +Вот пример использования простого case класса `Person`: + +{% tabs control-structures-28 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-28 %} +```scala +case class Person(name: String) + +def speak(p: Person) = p match { + case Person(name) if name == "Fred" => println(s"$name says, Yubba dubba doo") + case Person(name) if name == "Bam Bam" => println(s"$name says, Bam bam!") + case _ => println("Watch the Flintstones!") +} + +speak(Person("Fred")) // "Fred says, Yubba dubba doo" +speak(Person("Bam Bam")) // "Bam Bam says, Bam bam!" +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-28 %} +```scala +case class Person(name: String) + +def speak(p: Person) = p match + case Person(name) if name == "Fred" => println(s"$name says, Yubba dubba doo") + case Person(name) if name == "Bam Bam" => println(s"$name says, Bam bam!") + case _ => println("Watch the Flintstones!") + +speak(Person("Fred")) // "Fred says, Yubba dubba doo" +speak(Person("Bam Bam")) // "Bam Bam says, Bam bam!" +``` +{% endtab %} +{% endtabs %} + +### Использование `match` выражения в качестве тела метода + +Поскольку `match` выражения возвращают значение, их можно использовать в качестве тела метода. +Метод `isTruthy` принимает в качестве входного параметра значение `Matchable` +и возвращает `Boolean` на основе сопоставления с образцом: + +{% tabs control-structures-29 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-29 %} +```scala +def isTruthy(a: Matchable) = a match { + case 0 | "" | false => false + case _ => true +} +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-29 %} +```scala +def isTruthy(a: Matchable) = a match + case 0 | "" | false => false + case _ => true +``` +{% endtab %} +{% endtabs %} + +Входной параметр `a` имеет [тип `Matchable`][matchable], являющийся основой всех типов Scala, +для которых может выполняться сопоставление с образцом. +Метод реализован путем сопоставления на входе в метод, обрабатывая два варианта: +первый проверяет, является ли заданное значение целым числом `0`, пустой строкой или `false` и в этом случае возвращается `false`. +Для любого другого значения в случае по умолчанию мы возвращаем `true`. +Примеры ниже показывают, как работает этот метод: + +{% tabs is-truthy-call %} +{% tab 'Scala 2 и 3' for=is-truthy-call %} +```scala +isTruthy(0) // false +isTruthy(false) // false +isTruthy("") // false +isTruthy(1) // true +isTruthy(" ") // true +isTruthy(2F) // true +``` +{% endtab %} +{% endtabs %} + +Использование сопоставления с образцом в качестве тела метода очень распространено. + +#### Использование различных шаблонов в сопоставлении с образцом + +Для выражения `match` можно использовать множество различных шаблонов. +Например: + +- Сравнение с константой (такое как `case 3 =>`) +- Сравнение с последовательностями (такое как `case List(els : _*) =>`) +- Сравнение с кортежами (такое как `case (x, y) =>`) +- Сравнение с конструктором класса (такое как `case Person(first, last) =>`) +- Сравнение по типу (такое как `case p: Person =>`) + +Все эти виды шаблонов показаны в следующем методе `pattern`, +который принимает входной параметр типа `Matchable` и возвращает `String`: + +{% tabs control-structures-30 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-30 %} +```scala +def pattern(x: Matchable): String = x match { + + // сравнение с константой + case 0 => "zero" + case true => "true" + case "hello" => "you said 'hello'" + case Nil => "an empty List" + + // сравнение с последовательностями + case List(0, _, _) => "a 3-element list with 0 as the first element" + case List(1, _*) => "list, starts with 1, has any number of elements" + case Vector(1, _*) => "vector, starts w/ 1, has any number of elements" + + // сравнение с кортежами + case (a, b) => s"got $a and $b" + case (a, b, c) => s"got $a, $b, and $c" + + // сравнение с конструктором класса + case Person(first, "Alexander") => s"Alexander, first name = $first" + case Dog("Zeus") => "found a dog named Zeus" + + // сравнение по типу + case s: String => s"got a string: $s" + case i: Int => s"got an int: $i" + case f: Float => s"got a float: $f" + case a: Array[Int] => s"array of int: ${a.mkString(",")}" + case as: Array[String] => s"string array: ${as.mkString(",")}" + case d: Dog => s"dog: ${d.name}" + case list: List[?] => s"got a List: $list" + case m: Map[?, ?] => m.toString + + // значение по умолчанию с подстановочным знаком + case _ => "Unknown" +} +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-30 %} +```scala +def pattern(x: Matchable): String = x match + + // сравнение с константой + case 0 => "zero" + case true => "true" + case "hello" => "you said 'hello'" + case Nil => "an empty List" + + // сравнение с последовательностями + case List(0, _, _) => "a 3-element list with 0 as the first element" + case List(1, _*) => "list, starts with 1, has any number of elements" + case Vector(1, _*) => "vector, starts w/ 1, has any number of elements" + + // сравнение с кортежами + case (a, b) => s"got $a and $b" + case (a, b, c) => s"got $a, $b, and $c" + + // сравнение с конструктором класса + case Person(first, "Alexander") => s"Alexander, first name = $first" + case Dog("Zeus") => "found a dog named Zeus" + + // сравнение по типу + case s: String => s"got a string: $s" + case i: Int => s"got an int: $i" + case f: Float => s"got a float: $f" + case a: Array[Int] => s"array of int: ${a.mkString(",")}" + case as: Array[String] => s"string array: ${as.mkString(",")}" + case d: Dog => s"dog: ${d.name}" + case list: List[?] => s"got a List: $list" + case m: Map[?, ?] => m.toString + + // значение по умолчанию с подстановочным знаком + case _ => "Unknown" +``` +{% endtab %} +{% endtabs %} + +## try/catch/finally + +Как и в Java, в Scala есть конструкция `try`/`catch`/`finally`, позволяющая перехватывать исключения и управлять ими. +Для обеспечения согласованности Scala использует тот же синтаксис, что и выражения `match`, +и поддерживает сопоставление с образцом для различных возможных исключений. + +В следующем примере `openAndReadAFile` - это метод, который выполняет то, что следует из его названия: +он открывает файл и считывает из него текст, присваивая результат изменяемой переменной `text`: + +{% tabs control-structures-31 class=tabs-scala-version %} +{% tab 'Scala 2' for=control-structures-31 %} +```scala +var text = "" +try { + text = openAndReadAFile(filename) +} catch { + case fnf: FileNotFoundException => fnf.printStackTrace() + case ioe: IOException => ioe.printStackTrace() +} finally { + // здесь необходимо закрыть ресурсы + println("Came to the 'finally' clause.") +} +``` +{% endtab %} +{% tab 'Scala 3' for=control-structures-31 %} +```scala +var text = "" +try + text = openAndReadAFile(filename) +catch + case fnf: FileNotFoundException => fnf.printStackTrace() + case ioe: IOException => ioe.printStackTrace() +finally + // здесь необходимо закрыть ресурсы + println("Came to the 'finally' clause.") +``` +{% endtab %} +{% endtabs %} + +Предполагая, что метод `openAndReadAFile` использует Java `java.io.*` классы для чтения файла +и не перехватывает его исключения, попытка открыть и прочитать файл может привести как к `FileNotFoundException`, +так и к `IOException`, и эти два исключения перехватываются в блоке `catch` этого примера. + +[matchable]: {{ site.scala3ref }}/other-new-features/matchable.html diff --git a/_ru/scala3/book/domain-modeling-fp.md b/_ru/scala3/book/domain-modeling-fp.md new file mode 100644 index 0000000000..e085a0dc06 --- /dev/null +++ b/_ru/scala3/book/domain-modeling-fp.md @@ -0,0 +1,825 @@ +--- +layout: multipage-overview +title: Моделирование ФП +scala3: true +partof: scala3-book +overview-name: "Scala 3 — Book" +type: section +description: В этой главе представлено введение в моделирование предметной области с использованием ФП в Scala 3. +language: ru +num: 22 +previous-page: domain-modeling-oop +next-page: methods-intro +--- + + +В этой главе представлено введение в моделирование предметной области +с использованием функционального программирования (ФП) в Scala 3. +При моделировании окружающего нас мира с помощью ФП обычно используются следующие конструкции Scala: + +- Перечисления +- Case классы +- Trait-ы + +> Если вы не знакомы с алгебраическими типами данных (ADT) и их обобщенной версией (GADT), +> то можете прочитать главу ["Алгебраические типы данных"][adts], прежде чем читать этот раздел. + +## Введение + +В ФП *данные* и *операции над этими данными* — это две разные вещи; вы не обязаны инкапсулировать их вместе, как в ООП. + +Концепция аналогична числовой алгебре. +Когда вы думаете о целых числах, больших либо равных нулю, +у вас есть *набор* возможных значений, который выглядит следующим образом: + +```` +0, 1, 2 ... Int.MaxValue +```` + +Игнорируя деление целых чисел, возможные *операции* над этими значениями такие: + +```` ++, -, * +```` + +Схема ФП реализуется аналогичным образом: + +- описывается свой набор значений (данные) +- описываются операции, которые работают с этими значениями (функции) + +> Как будет видно, рассуждения о программах в этом стиле сильно отличаются от объектно-ориентированного программирования. +> Проще говоря о данных в ФП: +> Отделение функциональности от данных позволяет проверять свои данные, не беспокоясь о поведении. + +В этой главе мы смоделируем данные и операции для “пиццы” в пиццерии. +Будет показано, как реализовать часть “данных” модели Scala/ФП, +а затем - несколько различных способов организации операций с этими данными. + +## Моделирование данных + +В Scala достаточно просто описать модель данных: + +- если необходимо смоделировать данные с различными вариантами, то используется конструкция `enum` (или `case object` в Scala 2) +- если необходимо только сгруппировать сущности (или нужен более детальный контроль), то используются case class-ы + +### Описание вариантов + +Данные, которые просто состоят из различных вариантов, таких как размер корочки, тип корочки и начинка, +кратко моделируются с помощью перечислений: + +{% tabs data_1 class=tabs-scala-version %} +{% tab 'Scala 2' for=data_1 %} + +В Scala 2 перечисления выражаются комбинацией `sealed class` и нескольких `case object`, которые расширяют класс: + +```scala +sealed abstract class CrustSize +object CrustSize { + case object Small extends CrustSize + case object Medium extends CrustSize + case object Large extends CrustSize +} + +sealed abstract class CrustType +object CrustType { + case object Thin extends CrustType + case object Thick extends CrustType + case object Regular extends CrustType +} + +sealed abstract class Topping +object Topping { + case object Cheese extends Topping + case object Pepperoni extends Topping + case object BlackOlives extends Topping + case object GreenOlives extends Topping + case object Onions extends Topping +} +``` + +{% endtab %} +{% tab 'Scala 3' for=data_1 %} + +В Scala 3 перечисления кратко выражаются конструкцией `enum`: + +```scala +enum CrustSize: + case Small, Medium, Large + +enum CrustType: + case Thin, Thick, Regular + +enum Topping: + case Cheese, Pepperoni, BlackOlives, GreenOlives, Onions +``` + +{% endtab %} +{% endtabs %} + +> Типы данных, которые описывают различные варианты (например, `CrustSize`), +> также иногда называются суммированными типами (_sum types_). + +### Описание основных данных + +Пиццу можно рассматривать как _составной_ контейнер с различными атрибутами, указанными выше. +Мы можем использовать `case class`, чтобы описать, +что `Pizza` состоит из `crustSize`, `crustType` и, возможно, нескольких `toppings`: + +{% tabs data_2 class=tabs-scala-version %} +{% tab 'Scala 2' for=data_2 %} + +```scala +import CrustSize._ +import CrustType._ +import Topping._ + +case class Pizza( + crustSize: CrustSize, + crustType: CrustType, + toppings: Seq[Topping] +) +``` + +{% endtab %} +{% tab 'Scala 3' for=data_2 %} + +```scala +import CrustSize.* +import CrustType.* +import Topping.* + +case class Pizza( + crustSize: CrustSize, + crustType: CrustType, + toppings: Seq[Topping] +) +``` + +{% endtab %} +{% endtabs %} + +> Типы данных, объединяющие несколько компонентов (например, `Pizza`), также иногда называют типами продуктов (_product types_). + +И все. Это модель данных для системы доставки пиццы в стиле ФП. +Решение очень лаконично, поскольку оно не требует объединения модели данных с операциями с пиццей. +Модель данных легко читается, как объявление дизайна для реляционной базы данных. +Также очень легко создавать значения нашей модели данных и проверять их: + +{% tabs data_3 %} +{% tab 'Scala 2 и 3' for=data_3 %} + +```scala +val myFavPizza = Pizza(Small, Regular, Seq(Cheese, Pepperoni)) +println(myFavPizza.crustType) // печатает Regular +``` + +{% endtab %} +{% endtabs %} + +#### Подробнее о модели данных + +Таким же образом можно было бы смоделировать всю систему заказа пиццы. +Вот несколько других `case class`-ов, которые используются для моделирования такой системы: + +{% tabs data_4 %} +{% tab 'Scala 2 и 3' for=data_4 %} + +```scala +case class Address( + street1: String, + street2: Option[String], + city: String, + state: String, + zipCode: String +) + +case class Customer( + name: String, + phone: String, + address: Address +) + +case class Order( + pizzas: Seq[Pizza], + customer: Customer +) +``` + +{% endtab %} +{% endtabs %} + +#### “Узкие доменные объекты” + +В своей книге *Functional and Reactive Domain Modeling*, Debasish Ghosh утверждает, +что там, где специалисты по ООП описывают свои классы как “широкие модели предметной области”, +которые инкапсулируют данные и поведение, +модели данных ФП можно рассматривать как “узкие объекты предметной области”. +Это связано с тем, что, как показано выше, модели данных определяются как `case` классы с атрибутами, +но без поведения, что приводит к коротким и лаконичным структурам данных. + +## Моделирование операций + +Возникает интересный вопрос: поскольку ФП отделяет данные от операций над этими данными, +то как эти операции реализуются в Scala? + +Ответ на самом деле довольно прост: пишутся функции (или методы), работающие со значениями смоделированных данных. +Например, можно определить функцию, которая вычисляет цену пиццы. + +{% tabs data_5 class=tabs-scala-version %} +{% tab 'Scala 2' for=data_5 %} + +```scala +def pizzaPrice(p: Pizza): Double = p match { + case Pizza(crustSize, crustType, toppings) => { + val base = 6.00 + val crust = crustPrice(crustSize, crustType) + val tops = toppings.map(toppingPrice).sum + base + crust + tops + } +} +``` + +{% endtab %} + +{% tab 'Scala 3' for=data_5 %} + +```scala +def pizzaPrice(p: Pizza): Double = p match + case Pizza(crustSize, crustType, toppings) => + val base = 6.00 + val crust = crustPrice(crustSize, crustType) + val tops = toppings.map(toppingPrice).sum + base + crust + tops +``` + +{% endtab %} +{% endtabs %} + +Можно заметить, что реализация функции просто повторяет форму данных: поскольку `Pizza` является case class-ом, +используется сопоставление с образцом для извлечения компонентов, +а затем вызываются вспомогательные функции для вычисления отдельных цен. + +{% tabs data_6 class=tabs-scala-version %} +{% tab 'Scala 2' for=data_6 %} + +```scala +def toppingPrice(t: Topping): Double = t match { + case Cheese | Onions => 0.5 + case Pepperoni | BlackOlives | GreenOlives => 0.75 +} +``` + +{% endtab %} + +{% tab 'Scala 3' for=data_6 %} + +```scala +def toppingPrice(t: Topping): Double = t match + case Cheese | Onions => 0.5 + case Pepperoni | BlackOlives | GreenOlives => 0.75 +``` + +{% endtab %} +{% endtabs %} + +Точно так же, поскольку `Topping` является перечислением, +используется сопоставление с образцом, чтобы разделить варианты. +Сыр и лук продаются по 50 центов за штуку, остальные — по 75. + +{% tabs data_7 class=tabs-scala-version %} +{% tab 'Scala 2' for=data_7 %} + +```scala +def crustPrice(s: CrustSize, t: CrustType): Double = + (s, t) match { + // если размер корочки маленький или средний, тип не важен + case (Small | Medium, _) => 0.25 + case (Large, Thin) => 0.50 + case (Large, Regular) => 0.75 + case (Large, Thick) => 1.00 + } +``` + +{% endtab %} + +{% tab 'Scala 3' for=data_7 %} + +```scala +def crustPrice(s: CrustSize, t: CrustType): Double = + (s, t) match + // если размер корочки маленький или средний, тип не важен + case (Small | Medium, _) => 0.25 + case (Large, Thin) => 0.50 + case (Large, Regular) => 0.75 + case (Large, Thick) => 1.00 +``` + +{% endtab %} +{% endtabs %} + +Чтобы рассчитать цену корки, мы одновременно сопоставляем образцы как по размеру, так и по типу корки. + +> Важным моментом во всех показанных выше функциях является то, что они являются чистыми функциями (_pure functions_): +> они не изменяют данные и не имеют других побочных эффектов (таких, как выдача исключений или запись в файл). +> Всё, что они делают - это просто получают значения и вычисляют результат. + +## Как организовать функциональность? + +При реализации функции `pizzaPrice`, описанной выше, не было сказано, _где_ ее определять. +Scala предоставляет множество отличных инструментов для организации логики в различных пространствах имен и модулях. + +Существует несколько способов реализации и организации поведения: + +- определить функции в сопутствующих объектах (companion object) +- использовать модульный стиль программирования +- использовать подход “функциональных объектов” +- определить функциональность в методах расширения + +Эти различные решения показаны в оставшейся части этого раздела. + +### Сопутствующий объект + +Первый подход — определить поведение (функции) в сопутствующем объекте. + +> Как обсуждалось в разделе [“Инструменты”][modeling-tools], +> _сопутствующий объект_ — это `object` с тем же именем, что и у класса, и объявленный в том же файле, что и класс. + +При таком подходе в дополнение к `enum` или `case class` также определяется сопутствующий объект с таким же именем, +который содержит поведение (функции). + +{% tabs org_1 class=tabs-scala-version %} +{% tab 'Scala 2' for=org_1 %} + +```scala +case class Pizza( + crustSize: CrustSize, + crustType: CrustType, + toppings: Seq[Topping] +) + +// сопутствующий объект для case class Pizza +object Pizza { + // тоже самое, что и `pizzaPrice` + def price(p: Pizza): Double = ... +} + +sealed abstract class Topping + +// сопутствующий объект для перечисления Topping +object Topping { + case object Cheese extends Topping + case object Pepperoni extends Topping + case object BlackOlives extends Topping + case object GreenOlives extends Topping + case object Onions extends Topping + + // тоже самое, что и `toppingPrice` + def price(t: Topping): Double = ... +} +``` + +{% endtab %} +{% tab 'Scala 3' for=org_1 %} + +```scala +case class Pizza( + crustSize: CrustSize, + crustType: CrustType, + toppings: Seq[Topping] +) + +// сопутствующий объект для case class Pizza +object Pizza: + // тоже самое, что и `pizzaPrice` + def price(p: Pizza): Double = ... + +enum Topping: + case Cheese, Pepperoni, BlackOlives, GreenOlives, Onions + +// сопутствующий объект для перечисления Topping +object Topping: + // тоже самое, что и `toppingPrice` + def price(t: Topping): Double = ... +``` + +{% endtab %} +{% endtabs %} + +При таком подходе можно создать `Pizza` и вычислить ее цену следующим образом: + +{% tabs org_2 %} +{% tab 'Scala 2 и 3' for=org_2 %} + +```scala +val pizza1 = Pizza(Small, Thin, Seq(Cheese, Onions)) +Pizza.price(pizza1) +``` + +{% endtab %} +{% endtabs %} + +Группировка функциональности с помощью сопутствующих объектов имеет несколько преимуществ: + +- связывает функциональность с данными и облегчает их поиск программистам (и компилятору). +- создает пространство имен и, например, позволяет использовать `price` в качестве имени метода, не полагаясь на перегрузку. +- реализация `Topping.price` может получить доступ к значениям перечисления, таким как `Cheese`, без необходимости их импорта. + +Однако также есть несколько компромиссов, которые следует учитывать: + +- модель данных тесно связывается с функциональностью. + В частности, сопутствующий объект должен быть определен в том же файле, что и `case class`. +- неясно, где определять такие функции, как `crustPrice`, + которые с одинаковым успехом можно поместить в сопутствующий объект `CrustSize` или `CrustType`. + +## Модули + +Второй способ организации поведения — использование “модульного” подхода. +В книге _“Программирование на Scala”_ _модуль_ определяется как +“небольшая часть программы с четко определенным интерфейсом и скрытой реализацией”. +Давайте посмотрим, что это значит. + +### Создание интерфейса `PizzaService` + +Первое, о чем следует подумать, — это “поведение” `Pizza`. +Делая это, определяем `trait PizzaServiceInterface` следующим образом: + +{% tabs module_1 class=tabs-scala-version %} +{% tab 'Scala 2' for=module_1 %} + +```scala +trait PizzaServiceInterface { + + def price(p: Pizza): Double + + def addTopping(p: Pizza, t: Topping): Pizza + def removeAllToppings(p: Pizza): Pizza + + def updateCrustSize(p: Pizza, cs: CrustSize): Pizza + def updateCrustType(p: Pizza, ct: CrustType): Pizza +} +``` + +{% endtab %} + +{% tab 'Scala 3' for=module_1 %} + +```scala +trait PizzaServiceInterface: + + def price(p: Pizza): Double + + def addTopping(p: Pizza, t: Topping): Pizza + def removeAllToppings(p: Pizza): Pizza + + def updateCrustSize(p: Pizza, cs: CrustSize): Pizza + def updateCrustType(p: Pizza, ct: CrustType): Pizza +``` + +{% endtab %} +{% endtabs %} + +Как показано, каждый метод принимает `Pizza` в качестве входного параметра вместе с другими параметрами, +а затем возвращает экземпляр `Pizza` в качестве результата. + +Когда пишется такой чистый интерфейс, можно думать о нем как о контракте, +в котором говорится: “Все неабстрактные классы, расширяющие этот trait, должны предоставлять реализацию этих сервисов”. + +На этом этапе также можно представить, что вы являетесь потребителем этого API. +Когда вы это сделаете, будет полезно набросать некоторый пример “потребительского” кода, +чтобы убедиться, что API выглядит так, как хотелось: + +{% tabs module_2 %} +{% tab 'Scala 2 и 3' for=module_2 %} + +```scala +val p = Pizza(Small, Thin, Seq(Cheese)) + +// как вы хотите использовать методы в PizzaServiceInterface +val p1 = addTopping(p, Pepperoni) +val p2 = addTopping(p1, Onions) +val p3 = updateCrustType(p2, Thick) +val p4 = updateCrustSize(p3, Large) +``` + +{% endtab %} +{% endtabs %} + +Если с этим кодом все в порядке, как правило, можно начать набрасывать другой API, например API для заказов, +но, поскольку сейчас рассматривается только `Pizza`, перейдем к созданию конкретной реализации этого интерфейса. + +> Обратите внимание, что обычно это двухэтапный процесс. +> На первом шаге набрасывается контракт API в качестве _интерфейса_. +> На втором шаге создается конкретная _реализация_ этого интерфейса. +> В некоторых случаях в конечном итоге создается несколько конкретных реализаций базового интерфейса. + +### Создание конкретной реализации + +Теперь, когда известно, как выглядит `PizzaServiceInterface`, можно создать конкретную реализацию, +написав тело для всех методов, определенных в интерфейсе: + +{% tabs module_3 class=tabs-scala-version %} +{% tab 'Scala 2' for=module_3 %} + +```scala +object PizzaService extends PizzaServiceInterface { + + def price(p: Pizza): Double = + ... // реализация была дана выше + + def addTopping(p: Pizza, t: Topping): Pizza = + p.copy(toppings = p.toppings :+ t) + + def removeAllToppings(p: Pizza): Pizza = + p.copy(toppings = Seq.empty) + + def updateCrustSize(p: Pizza, cs: CrustSize): Pizza = + p.copy(crustSize = cs) + + def updateCrustType(p: Pizza, ct: CrustType): Pizza = + p.copy(crustType = ct) +} +``` + +{% endtab %} + +{% tab 'Scala 3' for=module_3 %} + +```scala +object PizzaService extends PizzaServiceInterface: + + def price(p: Pizza): Double = + ... // реализация была дана выше + + def addTopping(p: Pizza, t: Topping): Pizza = + p.copy(toppings = p.toppings :+ t) + + def removeAllToppings(p: Pizza): Pizza = + p.copy(toppings = Seq.empty) + + def updateCrustSize(p: Pizza, cs: CrustSize): Pizza = + p.copy(crustSize = cs) + + def updateCrustType(p: Pizza, ct: CrustType): Pizza = + p.copy(crustType = ct) + +end PizzaService +``` + +{% endtab %} +{% endtabs %} + +Хотя двухэтапный процесс создания интерфейса с последующей реализацией не всегда необходим, +явное продумывание API и его использования — хороший подход. + +Когда все готово, можно использовать `Pizza` и `PizzaService`: + +{% tabs module_4 class=tabs-scala-version %} +{% tab 'Scala 2' for=module_4 %} + +```scala +import PizzaService._ + +val p = Pizza(Small, Thin, Seq(Cheese)) + +// использование методов PizzaService +val p1 = addTopping(p, Pepperoni) +val p2 = addTopping(p1, Onions) +val p3 = updateCrustType(p2, Thick) +val p4 = updateCrustSize(p3, Large) + +println(price(p4)) // печатает 8.75 +``` + +{% endtab %} + +{% tab 'Scala 3' for=module_4 %} + +```scala +import PizzaService.* + +val p = Pizza(Small, Thin, Seq(Cheese)) + +// использование методов PizzaService +val p1 = addTopping(p, Pepperoni) +val p2 = addTopping(p1, Onions) +val p3 = updateCrustType(p2, Thick) +val p4 = updateCrustSize(p3, Large) + +println(price(p4)) // печатает 8.75 +``` + +{% endtab %} +{% endtabs %} + +### Функциональные объекты + +В книге _“Программирование на Scala”_ авторы определяют термин “Функциональные объекты” как +“объекты, которые не имеют никакого изменяемого состояния”. +Это также относится к типам в `scala.collection.immutable`. +Например, методы в `List` не изменяют внутреннего состояния, а вместо этого в результате создают копию `List`. + +Об этом подходе можно думать, как о “гибридном дизайне ФП/ООП”, потому что: + +- данные моделируются, используя неизменяемые `case` классы. +- определяется поведение (методы) _того же типа_, что и данные. +- поведение реализуется как чистые функции: они не изменяют никакого внутреннего состояния; скорее - возвращают копию. + +> Это действительно гибридный подход: как и в **дизайне ООП**, методы инкапсулированы в класс с данными, +> но, как это обычно бывает **в дизайне ФП**, методы реализованы как чистые функции, которые данные не изменяют. + +#### Пример + +Используя этот подход, можно напрямую реализовать функциональность пиццы в `case class`: + +{% tabs module_5 class=tabs-scala-version %} +{% tab 'Scala 2' for=module_5 %} + +```scala +case class Pizza( + crustSize: CrustSize, + crustType: CrustType, + toppings: Seq[Topping] +) { + + // операции этой модели данных + def price: Double = + pizzaPrice(this) // такая же имплементация, как и выше + + def addTopping(t: Topping): Pizza = + this.copy(toppings = this.toppings :+ t) + + def removeAllToppings: Pizza = + this.copy(toppings = Seq.empty) + + def updateCrustSize(cs: CrustSize): Pizza = + this.copy(crustSize = cs) + + def updateCrustType(ct: CrustType): Pizza = + this.copy(crustType = ct) +} +``` + +{% endtab %} + +{% tab 'Scala 3' for=module_5 %} + +```scala +case class Pizza( + crustSize: CrustSize, + crustType: CrustType, + toppings: Seq[Topping] +): + + // операции этой модели данных + def price: Double = + pizzaPrice(this) // такая же имплементация, как и выше + + def addTopping(t: Topping): Pizza = + this.copy(toppings = this.toppings :+ t) + + def removeAllToppings: Pizza = + this.copy(toppings = Seq.empty) + + def updateCrustSize(cs: CrustSize): Pizza = + this.copy(crustSize = cs) + + def updateCrustType(ct: CrustType): Pizza = + this.copy(crustType = ct) +``` + +{% endtab %} +{% endtabs %} + +Обратите внимание, что в отличие от предыдущих подходов, поскольку это методы класса `Pizza`, +они не принимают ссылку `Pizza` в качестве входного параметра. +Вместо этого у них есть собственная ссылка на текущий экземпляр пиццы - `this`. + +Теперь можно использовать этот новый дизайн следующим образом: + +{% tabs module_6 %} +{% tab 'Scala 2 и 3' for=module_6 %} + +```scala +Pizza(Small, Thin, Seq(Cheese)) + .addTopping(Pepperoni) + .updateCrustType(Thick) + .price +``` + +{% endtab %} +{% endtabs %} + +### Методы расширения + +Методы расширения - подход, который находится где-то между первым (определение функций в сопутствующем объекте) +и последним (определение функций как методов самого типа). + +Методы расширения позволяют создавать API, похожий на API функционального объекта, +без необходимости определять функции как методы самого типа. +Это может иметь несколько преимуществ: + +- модель данных снова _очень лаконична_ и не упоминает никакого поведения. +- можно _задним числом_ развить функциональность типов дополнительными методами, не изменяя исходного определения. +- помимо сопутствующих объектов или прямых методов типов, методы расширения могут быть определены _извне_ в другом файле. + +Вернемся к примеру: + +{% tabs module_7 class=tabs-scala-version %} +{% tab 'Scala 2' for=module_7 %} + +```scala +case class Pizza( + crustSize: CrustSize, + crustType: CrustType, + toppings: Seq[Topping] +) + +implicit class PizzaOps(p: Pizza) { + def price: Double = + pizzaPrice(p) // такая же имплементация, как и выше + + def addTopping(t: Topping): Pizza = + p.copy(toppings = p.toppings :+ t) + + def removeAllToppings: Pizza = + p.copy(toppings = Seq.empty) + + def updateCrustSize(cs: CrustSize): Pizza = + p.copy(crustSize = cs) + + def updateCrustType(ct: CrustType): Pizza = + p.copy(crustType = ct) +} +``` +В приведенном выше коде мы определяем различные методы для пиццы как методы в _неявном классе_ (_implicit class_). +С `implicit class PizzaOps(p: Pizza)` тогда, где бы `PizzaOps` ни был импортирован, +его методы будут доступны в экземплярах `Pizza`. +Получатель в этом случае `p`. + +{% endtab %} +{% tab 'Scala 3' for=module_7 %} + +```scala +case class Pizza( + crustSize: CrustSize, + crustType: CrustType, + toppings: Seq[Topping] +) + +extension (p: Pizza) + def price: Double = + pizzaPrice(p) // такая же имплементация, как и выше + + def addTopping(t: Topping): Pizza = + p.copy(toppings = p.toppings :+ t) + + def removeAllToppings: Pizza = + p.copy(toppings = Seq.empty) + + def updateCrustSize(cs: CrustSize): Pizza = + p.copy(crustSize = cs) + + def updateCrustType(ct: CrustType): Pizza = + p.copy(crustType = ct) +``` +В приведенном выше коде мы определяем различные методы для пиццы как _методы расширения_ (_extension methods_). +С помощью `extension (p: Pizza)` мы говорим, что хотим сделать методы доступными для экземпляров `Pizza`. +Получатель в этом случае `p`. + +{% endtab %} +{% endtabs %} + +Используя наши методы расширения, мы можем получить тот же API, что и раньше: + +{% tabs module_8 %} +{% tab 'Scala 2 и 3' for=module_8 %} + +```scala +Pizza(Small, Thin, Seq(Cheese)) + .addTopping(Pepperoni) + .updateCrustType(Thick) + .price +``` + +{% endtab %} +{% endtabs %} + +При этом методы расширения можно определить в любом другом модуле. +Как правило, если вы являетесь разработчиком модели данных, то определяете свои методы расширения в сопутствующем объекте. +Таким образом, они уже доступны всем пользователям. +В противном случае методы расширения должны быть импортированы явно, чтобы их можно было использовать. + +## Резюме функционального подхода + +Определение модели данных в Scala/ФП, как правило, простое: +моделируются варианты данных с помощью перечислений и составных данных с помощью `case` классов. +Затем, чтобы смоделировать поведение, определяются функции, которые работают со значениями модели данных. +Были рассмотрены разные способы организации функций: + +- можно поместить методы в сопутствующие объекты +- можно использовать модульный стиль программирования, разделяющий интерфейс и реализацию +- можно использовать подход “функциональных объектов” и хранить методы в определенном типе данных +- можно использовать методы расширения, чтобы снабдить модель данных функциональностью + +[adts]: {% link _overviews/scala3-book/types-adts-gadts.md %} +[modeling-tools]: {% link _overviews/scala3-book/domain-modeling-tools.md %} diff --git a/_ru/scala3/book/domain-modeling-intro.md b/_ru/scala3/book/domain-modeling-intro.md new file mode 100644 index 0000000000..6202dec08d --- /dev/null +++ b/_ru/scala3/book/domain-modeling-intro.md @@ -0,0 +1,19 @@ +--- +layout: multipage-overview +title: Моделирование предметной области +scala3: true +partof: scala3-book +overview-name: "Scala 3 — Book" +type: chapter +description: В этой главе показано, как можно моделировать предметную область с помощью Scala 3. +language: ru +num: 19 +previous-page: control-structures +next-page: domain-modeling-tools +--- + +В этой главе показано, как можно смоделировать предметную область с помощью Scala 3: + +- В разделе "Инструменты" представлены доступные вам инструменты, включая классы, трейты, перечисления и многое другое. +- В разделе "Моделирование ООП" рассматриваются атрибуты и поведение моделирования в стиле объектно-ориентированного программирования (ООП). +- В разделе "Моделирование ФП" рассматривается моделирование предметной области в стиле функционального программирования (ФП). diff --git a/_ru/scala3/book/domain-modeling-oop.md b/_ru/scala3/book/domain-modeling-oop.md new file mode 100644 index 0000000000..df09cdbdd2 --- /dev/null +++ b/_ru/scala3/book/domain-modeling-oop.md @@ -0,0 +1,602 @@ +--- +layout: multipage-overview +title: Моделирование ООП +scala3: true +partof: scala3-book +overview-name: "Scala 3 — Book" +type: section +description: В этой главе представлено введение в моделирование предметной области с использованием ООП в Scala 3. +language: ru +num: 21 +previous-page: domain-modeling-tools +next-page: domain-modeling-fp +--- + +В этой главе представлено введение в моделирование предметной области с использованием +объектно-ориентированного программирования (ООП) в Scala 3. + +## Введение + +Scala предоставляет все необходимые инструменты для объектно-ориентированного проектирования: + +- **Traits** позволяют указывать (абстрактные) интерфейсы, а также конкретные реализации. +- **Mixin Composition** предоставляет инструменты для создания компонентов из более мелких деталей. +- **Классы** могут реализовывать интерфейсы, заданные трейтами. +- **Экземпляры** классов могут иметь свое собственное приватное состояние. +- **Subtyping** позволяет использовать экземпляр одного класса там, где ожидается экземпляр его суперкласса. +- **Модификаторы доступа** позволяют управлять, к каким членам класса можно получить доступ с помощью какой части кода. + +## Трейты + +В отличие от других языков с поддержкой ООП, таких как Java, возможно, +основным инструментом декомпозиции в Scala являются не классы, а трейты. +Они могут служить для описания абстрактных интерфейсов, таких как: + +{% tabs traits_1 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +trait Showable { + def show: String +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +trait Showable: + def show: String +``` +{% endtab %} +{% endtabs %} + +а также могут содержать конкретные реализации: + +{% tabs traits_2 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +trait Showable { + def show: String + def showHtml = "

" + show + "

" +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +trait Showable: + def show: String + def showHtml = "

" + show + "

" +``` +{% endtab %} +{% endtabs %} + +На примере видно, что метод `showHtml` определяется в терминах абстрактного метода `show`. + +[Odersky и Zenger][scalable] представляют _сервис-ориентированную компонентную модель_ и рассматривают: + +- **абстрактные члены** как _требуемые_ службы: их все еще необходимо реализовать в подклассе. +- **конкретные члены** как _предоставляемые_ услуги: они предоставляются подклассу. + +Это видно на примере со `Showable`: определяя класс `Document`, который расширяет `Showable`, +все еще нужно определить `show`, но `showHtml` уже предоставляется: + +{% tabs traits_3 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +class Document(text: String) extends Showable { + def show = text +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +class Document(text: String) extends Showable: + def show = text +``` + +{% endtab %} +{% endtabs %} + +#### Абстрактные члены + +Абстрактными в `trait` могут оставаться не только методы. +`trait` может содержать: + +- абстрактные методы (`def m(): T`) +- абстрактные переменные (`val x: T`) +- абстрактные типы (`type T`), потенциально с ограничениями (`type T <: S`) +- абстрактные given (`given t: T`) только в Scala 3 + +Каждая из вышеперечисленных функций может быть использована для определения той или иной формы требований к реализатору `trait`. + +## Смешанная композиция + +Кроме того, что `trait`-ы могут содержать абстрактные и конкретные определения, +Scala также предоставляет мощный способ создания нескольких `trait`: +структура, которую часто называют _смешанной композицией_. + +Предположим, что существуют следующие два (потенциально независимо определенные) `trait`-а: + +{% tabs traits_4 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +trait GreetingService { + def translate(text: String): String + def sayHello = translate("Hello") +} + +trait TranslationService { + def translate(text: String): String = "..." +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +trait GreetingService: + def translate(text: String): String + def sayHello = translate("Hello") + +trait TranslationService: + def translate(text: String): String = "..." +``` + +{% endtab %} +{% endtabs %} + +Чтобы скомпоновать два сервиса, можно просто создать новый `trait`, расширяющий их: + +{% tabs traits_5 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +trait ComposedService extends GreetingService with TranslationService +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +trait ComposedService extends GreetingService, TranslationService +``` + +{% endtab %} +{% endtabs %} + +Абстрактные элементы в одном `trait`-е (например, `translate` в `GreetingService`) +автоматически сопоставляются с конкретными элементами в другом `trait`-е. +Это работает не только с методами, как в этом примере, но и со всеми другими абстрактными членами, +упомянутыми выше (то есть типами, переменными и т.д.). + +## Классы + +`trait`-ы отлично подходят для модуляции компонентов и описания интерфейсов (обязательных и предоставляемых). +Но в какой-то момент возникнет необходимость создавать их экземпляры. +При разработке программного обеспечения в Scala часто бывает полезно рассмотреть возможность +использования классов только на начальных этапах модели наследования: + +{% tabs table-traits-cls-summary class=tabs-scala-version %} +{% tab 'Scala 2' %} +| Трейты | `T1`, `T2`, `T3` +| Составные трейты | `S1 extends T1 with T2`, `S2 extends T2 with T3` +| Классы | `C extends S1 with T3` +| Экземпляры | `new C()` +{% endtab %} +{% tab 'Scala 3' %} +| Трейты | `T1`, `T2`, `T3` +| Составные трейты | `S1 extends T1, T2`, `S2 extends T2, T3` +| Классы | `C extends S1, T3` +| Экземпляры | `C()` +{% endtab %} +{% endtabs %} + +Это еще более актуально в Scala 3, где трейты теперь также могут принимать параметры конструктора, +что еще больше устраняет необходимость в классах. + +#### Определение класса + +Подобно `trait`-ам, классы могут расширять несколько `trait`-ов (но только один суперкласс): + +{% tabs class_1 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +class MyService(name: String) extends ComposedService with Showable { + def show = s"$name says $sayHello" +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +class MyService(name: String) extends ComposedService, Showable: + def show = s"$name says $sayHello" +``` + +{% endtab %} +{% endtabs %} + +#### Подтипы + +Экземпляр `MyService` создается следующим образом: + +{% tabs class_2 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +val s1: MyService = new MyService("Service 1") +``` + +{% endtab %} +{% tab 'Scala 3' %} + +```scala +val s1: MyService = MyService("Service 1") +``` + +{% endtab %} +{% endtabs %} + +С помощью подтипов экземпляр `s1` можно использовать везде, где ожидается любое из расширенных свойств: + +{% tabs class_3 %} +{% tab 'Scala 2 и 3' %} + +```scala +val s2: GreetingService = s1 +val s3: TranslationService = s1 +val s4: Showable = s1 +// ... и так далее ... +``` +{% endtab %} +{% endtabs %} + +#### Планирование расширения + +Как упоминалось ранее, можно расширить еще один класс: + +{% tabs class_4 %} +{% tab 'Scala 2 и 3' %} + +```scala +class Person(name: String) +class SoftwareDeveloper(name: String, favoriteLang: String) + extends Person(name) +``` + +{% endtab %} +{% endtabs %} + +Однако, поскольку `trait`-ы разработаны как основное средство декомпозиции, +то не рекомендуется расширять класс, определенный в одном файле, из другого файла. + +
Открытые классы только в Scala 3
+ +В Scala 3 расширение неабстрактных классов в других файлах ограничено. +Чтобы разрешить это, базовый класс должен быть помечен как `open`: + +{% tabs class_5 %} +{% tab 'Только в Scala 3' %} + +```scala +open class Person(name: String) +``` +{% endtab %} +{% endtabs %} + +Маркировка классов с помощью [`open`][open] - это новая функция Scala 3. +Необходимость явно помечать классы как открытые позволяет избежать многих распространенных ошибок в ООП. +В частности, это требует, чтобы разработчики библиотек явно планировали расширение +и, например, документировали классы, помеченные как открытые. + +## Экземпляры и приватное изменяемое состояние + +Как и в других языках с поддержкой ООП, трейты и классы в Scala могут определять изменяемые поля: + +{% tabs instance_6 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +class Counter { + // получить значение можно только с помощью метода `count` + private var currentCount = 0 + + def tick(): Unit = currentCount += 1 + def count: Int = currentCount +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +class Counter: + // получить значение можно только с помощью метода `count` + private var currentCount = 0 + + def tick(): Unit = currentCount += 1 + def count: Int = currentCount +``` + +{% endtab %} +{% endtabs %} + +Каждый экземпляр класса `Counter` имеет собственное приватное состояние, +которое можно получить только через метод `count`, как показано в следующем взаимодействии: + +{% tabs instance_7 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +val c1 = new Counter() +c1.count // 0 +c1.tick() +c1.tick() +c1.count // 2 +``` + +{% endtab %} +{% tab 'Scala 3' %} + +```scala +val c1 = Counter() +c1.count // 0 +c1.tick() +c1.tick() +c1.count // 2 +``` + +{% endtab %} +{% endtabs %} + +#### Модификаторы доступа + +По умолчанию все определения элементов в Scala общедоступны. +Чтобы скрыть детали реализации, можно определить элементы (методы, поля, типы и т.д.) в качестве `private` или `protected`. +Таким образом, вы можете управлять доступом к ним или их переопределением. +Закрытые (`private`) элементы видны только самому классу/трейту и его сопутствующему объекту. +Защищенные (`protected`) элементы также видны для подклассов класса. + +## Дополнительный пример: сервис-ориентированный дизайн + +Далее будут проиллюстрированы некоторые расширенные возможности Scala и показано, +как их можно использовать для структурирования более крупных программных компонентов. +Примеры взяты из статьи Мартина Одерски и Маттиаса Зенгера ["Scalable Component Abstractions"][scalable]. +Пример в первую очередь предназначен для демонстрации того, +как использовать несколько функций типа для создания более крупных компонентов. + +Цель состоит в том, чтобы определить программный компонент с семейством типов, +которые могут быть уточнены позже при реализации компонента. +Конкретно, следующий код определяет компонент `SubjectObserver` как `trait` с двумя членами абстрактного типа, +`S` (для субъектов) и `O` (для наблюдателей): + +{% tabs example_1 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +trait SubjectObserver { + + type S <: Subject + type O <: Observer + + trait Subject { self: S => + private var observers: List[O] = List() + def subscribe(obs: O): Unit = { + observers = obs :: observers + } + def publish() = { + for ( obs <- observers ) obs.notify(this) + } + } + + trait Observer { + def notify(sub: S): Unit + } +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +trait SubjectObserver: + + type S <: Subject + type O <: Observer + + trait Subject: + self: S => + private var observers: List[O] = List() + def subscribe(obs: O): Unit = + observers = obs :: observers + def publish() = + for obs <- observers do obs.notify(this) + + trait Observer: + def notify(sub: S): Unit +``` + +{% endtab %} +{% endtabs %} + +Есть несколько вещей, которые нуждаются в объяснении. + +#### Члены абстрактного типа + +Тип объявления `S <: Subject` говорит, что внутри trait `SubjectObserver` можно ссылаться на +некоторый _неизвестный_ (то есть абстрактный) тип, который называется `S`. +Однако этот тип не является полностью неизвестным: мы знаем, по крайней мере, что это какой-то подтип `Subject`. +Все trait-ы и классы, расширяющие `SubjectObserver`, могут свободно выбирать любой тип для `S`, +если выбранный тип является подтипом `Subject`. +Часть `<: Subject` декларации также упоминается как верхняя граница на `S`. + +#### Вложенные trait-ы + +_В рамках_ trait-а `SubjectObserver` определяются два других trait-а. +trait `Observer`, который определяет только один абстрактный метод `notify` с одним аргументом типа `S`. +Как будет видно, важно, чтобы аргумент имел тип `S`, а не тип `Subject`. + +Второй trait, `Subject`, определяет одно приватное поле `observers` для хранения всех наблюдателей, +подписавшихся на этот конкретный объект. Подписка на объект просто сохраняет объект в списке. +Опять же, тип параметра `obs` - это `O`, а не `Observer`. + +#### Аннотации собственного типа + +Наконец, что означает `self: S =>` в trait-е `Subject`? Это называется аннотацией собственного типа. +И требует, чтобы подтипы `Subject` также были подтипами `S`. +Это необходимо, чтобы иметь возможность вызывать `obs.notify` с `this` в качестве аргумента, +поскольку для этого требуется значение типа `S`. +Если бы `S` был конкретным типом, аннотацию собственного типа можно было бы заменить на `trait Subject extends S`. + +### Реализация компонента + +Теперь можно реализовать вышеуказанный компонент и определить члены абстрактного типа как конкретные типы: + +{% tabs example_2 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +object SensorReader extends SubjectObserver { + type S = Sensor + type O = Display + + class Sensor(val label: String) extends Subject { + private var currentValue = 0.0 + def value = currentValue + def changeValue(v: Double) = { + currentValue = v + publish() + } + } + + class Display extends Observer { + def notify(sub: Sensor) = + println(s"${sub.label} has value ${sub.value}") + } +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +object SensorReader extends SubjectObserver: + type S = Sensor + type O = Display + + class Sensor(val label: String) extends Subject: + private var currentValue = 0.0 + def value = currentValue + def changeValue(v: Double) = + currentValue = v + publish() + + class Display extends Observer: + def notify(sub: Sensor) = + println(s"${sub.label} has value ${sub.value}") +``` + +{% endtab %} +{% endtabs %} + +В частности, мы определяем _singleton_ `object SensorReader`, который расширяет `SubjectObserver`. +В реализации `SensorReader` говорится, что тип `S` теперь определяется как тип `Sensor`, +а тип `O` определяется как тип `Display`. +И `Sensor`, и `Display` определяются как вложенные классы в `SensorReader`, +реализующие trait-ы `Subject` и `Observer` соответственно. + +Помимо того, что этот код является примером сервис-ориентированного дизайна, +он также освещает многие аспекты объектно-ориентированного программирования: + +- Класс `Sensor` вводит свое собственное частное состояние (`currentValue`) + и инкапсулирует изменение состояния за методом `changeValue`. +- Реализация `changeValue` использует метод `publish`, определенный в родительском trait-е. +- Класс `Display` расширяет trait `Observer` и реализует отсутствующий метод `notify`. + +Важно отметить, что реализация `notify` может безопасно получить доступ только к `label` и значению `sub`, +поскольку мы изначально объявили параметр типа `S`. + +### Использование компонента + +Наконец, следующий код иллюстрирует, как использовать компонент `SensorReader`: + +{% tabs example_3 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +import SensorReader._ + +// настройка сети +val s1 = new Sensor("sensor1") +val s2 = new Sensor("sensor2") +val d1 = new Display() +val d2 = new Display() +s1.subscribe(d1) +s1.subscribe(d2) +s2.subscribe(d1) + +// распространение обновлений по сети +s1.changeValue(2) +s2.changeValue(3) + +// печатает: +// sensor1 has value 2.0 +// sensor1 has value 2.0 +// sensor2 has value 3.0 + +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +import SensorReader.* + +// настройка сети +val s1 = Sensor("sensor1") +val s2 = Sensor("sensor2") +val d1 = Display() +val d2 = Display() +s1.subscribe(d1) +s1.subscribe(d2) +s2.subscribe(d1) + +// распространение обновлений по сети +s1.changeValue(2) +s2.changeValue(3) + +// печатает: +// sensor1 has value 2.0 +// sensor1 has value 2.0 +// sensor2 has value 3.0 +``` + +{% endtab %} +{% endtabs %} + +Имея под рукой все утилиты объектно-ориентированного программирования, в следующем разделе будет продемонстрировано, +как разрабатывать программы в функциональном стиле. + +[scalable]: https://doi.org/10.1145/1094811.1094815 +[open]: {{ site.scala3ref }}/other-new-features/open-classes.html +[trait-params]: {{ site.scala3ref }}/other-new-features/trait-parameters.html diff --git a/_ru/scala3/book/domain-modeling-tools.md b/_ru/scala3/book/domain-modeling-tools.md new file mode 100644 index 0000000000..36f19bc3ad --- /dev/null +++ b/_ru/scala3/book/domain-modeling-tools.md @@ -0,0 +1,1175 @@ +--- +layout: multipage-overview +title: Инструменты +scala3: true +partof: scala3-book +overview-name: "Scala 3 — Book" +type: section +description: В этой главе представлено введение в доступные инструменты моделирования предметной области в Scala 3, включая классы, трейты, перечисления и многое другое. +language: ru +num: 20 +previous-page: domain-modeling-intro +next-page: domain-modeling-oop +--- + + +Scala предоставляет множество различных конструкций для моделирования предметной области: + +- Классы +- Объекты +- Сопутствующие объекты +- Трейты +- Абстрактные классы +- Перечисления только в Scala 3 +- Case классы +- Case объекты + +В этом разделе кратко представлена каждая из этих языковых конструкций. + + +## Классы + +Как и в других языках, _класс_ в Scala — это шаблон для создания экземпляров объекта. +Вот несколько примеров классов: + +{% tabs class_1 %} +{% tab 'Scala 2 и 3' %} + +```scala +class Person(var name: String, var vocation: String) +class Book(var title: String, var author: String, var year: Int) +class Movie(var name: String, var director: String, var year: Int) +``` + +{% endtab %} +{% endtabs %} + +Эти примеры показывают, что в Scala есть очень легкий способ объявления классов. + +Все параметры в примерах наших классов определены как `var` поля, а значит, они изменяемы: их можно читать, а также изменять. +Если вы хотите, чтобы они были неизменяемыми — только для чтения — создайте их как `val` поля или используйте case класс. + +До Scala 3 для создания нового экземпляра класса использовалось ключевое слово `new`: + +{% tabs class_2 %} +{% tab 'Scala 2 и 3' %} + +```scala +val p = new Person("Robert Allen Zimmerman", "Harmonica Player") +// --- +``` + +{% endtab %} +{% endtabs %} + +Однако с [универсальными apply методами][creator] в Scala 3 этого больше не требуется: только в Scala 3. + +{% tabs class_3 %} +{% tab 'Только в Scala 3' %} + +```scala +val p = Person("Robert Allen Zimmerman", "Harmonica Player") +``` + +{% endtab %} +{% endtabs %} + +Если у вас есть экземпляр класса, такой как `p`, то вы можете получить доступ к полям экземпляра, +которые в этом примере являются параметрами конструктора: + +{% tabs class_4 %} +{% tab 'Scala 2 и 3' %} + +```scala +p.name // "Robert Allen Zimmerman" +p.vocation // "Harmonica Player" +``` + +{% endtab %} +{% endtabs %} + +Как уже упоминалось, все эти параметры были созданы как `var` поля, поэтому они изменяемые: + +{% tabs class_5 %} +{% tab 'Scala 2 и 3' %} + +```scala +p.name = "Bob Dylan" +p.vocation = "Musician" +``` + +{% endtab %} +{% endtabs %} + +### Поля и методы + +Классы также могут содержать методы и дополнительные поля, не являющиеся частью конструкторов. +Они определены в теле класса. +Тело инициализируется как часть конструктора по умолчанию: + +{% tabs method class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +class Person(var firstName: String, var lastName: String) { + + println("initialization begins") + val fullName = firstName + " " + lastName + + // метод класса + def printFullName: Unit = + // обращение к полю `fullName`, определенному выше + println(fullName) + + printFullName + println("initialization ends") +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +class Person(var firstName: String, var lastName: String): + + println("initialization begins") + val fullName = firstName + " " + lastName + + // метод класса + def printFullName: Unit = + // обращение к полю `fullName`, определенному выше + println(fullName) + + printFullName + println("initialization ends") +``` + +{% endtab %} +{% endtabs %} + +Следующая сессия REPL показывает, как создать новый экземпляр `Person` с этим классом: + +{% tabs demo-person class=tabs-scala-version %} +{% tab 'Scala 2' %} +````scala +scala> val john = new Person("John", "Doe") +initialization begins +John Doe +initialization ends +val john: Person = Person@55d8f6bb + +scala> john.printFullName +John Doe +```` +{% endtab %} +{% tab 'Scala 3' %} +````scala +scala> val john = Person("John", "Doe") +initialization begins +John Doe +initialization ends +val john: Person = Person@55d8f6bb + +scala> john.printFullName +John Doe +```` +{% endtab %} +{% endtabs %} + +Классы также могут расширять трейты и абстрактные классы, которые мы рассмотрим в специальных разделах ниже. + +### Значения параметров по умолчанию + +В качестве беглого взгляда на некоторые другие функции, +параметры конструктора класса также могут иметь значения по умолчанию: + +{% tabs default-values_1 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +class Socket(val timeout: Int = 5_000, val linger: Int = 5_000) { + override def toString = s"timeout: $timeout, linger: $linger" +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +class Socket(val timeout: Int = 5_000, val linger: Int = 5_000): + override def toString = s"timeout: $timeout, linger: $linger" +``` + +{% endtab %} +{% endtabs %} + +Отличительной особенностью этой функции является то, что она позволяет потребителям вашего кода +создавать классы различными способами, как если бы у класса были альтернативные конструкторы: + +{% tabs default-values_2 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +val s = new Socket() // timeout: 5000, linger: 5000 +val s = new Socket(2_500) // timeout: 2500, linger: 5000 +val s = new Socket(10_000, 10_000) // timeout: 10000, linger: 10000 +val s = new Socket(timeout = 10_000) // timeout: 10000, linger: 5000 +val s = new Socket(linger = 10_000) // timeout: 5000, linger: 10000 +``` + +{% endtab %} +{% tab 'Scala 3' %} + +```scala +val s = Socket() // timeout: 5000, linger: 5000 +val s = Socket(2_500) // timeout: 2500, linger: 5000 +val s = Socket(10_000, 10_000) // timeout: 10000, linger: 10000 +val s = Socket(timeout = 10_000) // timeout: 10000, linger: 5000 +val s = Socket(linger = 10_000) // timeout: 5000, linger: 10000 +``` + +{% endtab %} +{% endtabs %} + +При создании нового экземпляра класса вы также можете использовать именованные параметры. +Это особенно полезно, когда несколько параметров имеют одинаковый тип, как показано в этом сравнении: + +{% tabs default-values_3 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +// пример 1 +val s = new Socket(10_000, 10_000) + +// пример 2 +val s = new Socket( + timeout = 10_000, + linger = 10_000 +) +``` + +{% endtab %} +{% tab 'Scala 3' %} + +```scala +// пример 1 +val s = Socket(10_000, 10_000) + +// пример 2 +val s = Socket( + timeout = 10_000, + linger = 10_000 +) +``` + +{% endtab %} +{% endtabs %} + +### Вспомогательные конструкторы + +Вы можете определить класс с несколькими конструкторами, +чтобы клиенты вашего класса могли создавать его различными способами. +Например, предположим, что вам нужно написать код для моделирования студентов в системе приема в колледж. +При анализе требований вы увидели, что необходимо создавать экземпляр `Student` тремя способами: + +- С именем и государственным удостоверением личности, когда они впервые начинают процесс приема +- С именем, государственным удостоверением личности и дополнительной датой подачи заявки, когда они подают заявку +- С именем, государственным удостоверением личности и студенческим билетом после того, как они будут приняты + +Один из способов справиться с этой ситуацией в стиле ООП - с помощью нижеследующего кода: + +{% tabs structor_1 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +import java.time._ + +// [1] основной конструктор +class Student( + var name: String, + var govtId: String +) { + private var _applicationDate: Option[LocalDate] = None + private var _studentId: Int = 0 + + // [2] конструктор для студента, подавшего заявку + def this( + name: String, + govtId: String, + applicationDate: LocalDate + ) = { + this(name, govtId) + _applicationDate = Some(applicationDate) + } + + // [3] конструктор, когда учащийся принят и теперь имеет студенческий билет + def this( + name: String, + govtId: String, + studentId: Int + ) = { + this(name, govtId) + _studentId = studentId + } +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +import java.time.* + +// [1] основной конструктор +class Student( + var name: String, + var govtId: String +): + private var _applicationDate: Option[LocalDate] = None + private var _studentId: Int = 0 + + // [2] конструктор для студента, подавшего заявку + def this( + name: String, + govtId: String, + applicationDate: LocalDate + ) = + this(name, govtId) + _applicationDate = Some(applicationDate) + + // [3] конструктор, когда учащийся принят и теперь имеет студенческий билет + def this( + name: String, + govtId: String, + studentId: Int + ) = + this(name, govtId) + _studentId = studentId +``` + +{% endtab %} +{% endtabs %} + +Класс содержит три конструктора, обозначенных комментариями в коде: + +1. Первичный конструктор, заданный `name` и `govtId` в определении класса +2. Вспомогательный конструктор с параметрами `name`, `govtId` и `applicationDate` +3. Другой вспомогательный конструктор с параметрами `name`, `govtId` и `studentId` + +Эти конструкторы можно вызывать следующим образом: + +{% tabs structor_2 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +val s1 = new Student("Mary", "123") +val s2 = new Student("Mary", "123", LocalDate.now) +val s3 = new Student("Mary", "123", 456) +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +val s1 = Student("Mary", "123") +val s2 = Student("Mary", "123", LocalDate.now) +val s3 = Student("Mary", "123", 456) +``` + +{% endtab %} +{% endtabs %} + +Хотя этот метод можно использовать, имейте в виду, что параметры конструктора также могут иметь значения по умолчанию, +из-за чего создается впечатление, что класс содержит несколько конструкторов. +Это показано в предыдущем примере `Socket`. + +## Объекты + +Объект — это класс, который имеет ровно один экземпляр. +Инициализируется он лениво, тогда, когда на его элементы ссылаются, подобно `lazy val`. +Объекты в Scala позволяют группировать методы и поля в одном пространстве имен, аналогично тому, +как вы используете `static` члены в классе в Java, Javascript (ES6) или `@staticmethod` в Python. + +Объявление `object` аналогично объявлению `class`. +Вот пример объекта “строковые утилиты”, который содержит набор методов для работы со строками: + +{% tabs object_1 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +object StringUtils { + def truncate(s: String, length: Int): String = s.take(length) + def containsWhitespace(s: String): Boolean = s.object_1es(".*\\s.*") + def isNullOrEmpty(s: String): Boolean = s == null || s.trim.isEmpty +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +object StringUtils: + def truncate(s: String, length: Int): String = s.take(length) + def containsWhitespace(s: String): Boolean = s.object_1es(".*\\s.*") + def isNullOrEmpty(s: String): Boolean = s == null || s.trim.isEmpty +``` + +{% endtab %} +{% endtabs %} + +Мы можем использовать объект следующим образом: + +{% tabs object_2 %} +{% tab 'Scala 2 и 3' %} + +```scala +StringUtils.truncate("Chuck Bartowski", 5) // "Chuck" +``` + +{% endtab %} +{% endtabs %} + +Импорт в Scala очень гибкий и позволяет импортировать _все_ члены объекта: + +{% tabs object_3 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +import StringUtils._ +truncate("Chuck Bartowski", 5) // "Chuck" +containsWhitespace("Sarah Walker") // true +isNullOrEmpty("John Casey") // false +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +import StringUtils.* +truncate("Chuck Bartowski", 5) // "Chuck" +containsWhitespace("Sarah Walker") // true +isNullOrEmpty("John Casey") // false +``` + +{% endtab %} +{% endtabs %} + +или только _некоторые_: + +{% tabs object_4 %} +{% tab 'Scala 2 и 3' %} + +```scala +import StringUtils.{truncate, containsWhitespace} +truncate("Charles Carmichael", 7) // "Charles" +containsWhitespace("Captain Awesome") // true +isNullOrEmpty("Morgan Grimes") // Not found: isNullOrEmpty (error) +``` + +{% endtab %} +{% endtabs %} + +Объекты также могут содержать поля, доступ к которым также осуществляется как к статическим элементам: + +{% tabs object_5 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +object MathConstants { + val PI = 3.14159 + val E = 2.71828 +} + +println(MathConstants.PI) // 3.14159 +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +object MathConstants: + val PI = 3.14159 + val E = 2.71828 + +println(MathConstants.PI) // 3.14159 +``` + +{% endtab %} +{% endtabs %} + +## Сопутствующие объекты + +Объект `object`, имеющий то же имя, что и класс, и объявленный в том же файле, что и класс, +называется _"сопутствующим объектом"_. Точно так же соответствующий класс называется сопутствующим классом объекта. +Сопутствующие класс или объект могут получить доступ к закрытым членам своего “соседа”. + +Сопутствующие объекты используются для методов и значений, не относящихся к экземплярам сопутствующего класса. +Например, в следующем примере у класса `Circle` есть элемент с именем `area`, специфичный для каждого экземпляра, +а у его сопутствующего объекта есть метод с именем `calculateArea`, +который (а) не специфичен для экземпляра и (б) доступен для каждого экземпляра: + +{% tabs companion class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +import scala.math._ + +class Circle(val radius: Double) { + def area: Double = Circle.calculateArea(radius) +} + +object Circle { + private def calculateArea(radius: Double): Double = Pi * pow(radius, 2.0) +} + +val circle1 = new Circle(5.0) +circle1.area +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +import scala.math.* + +class Circle(val radius: Double): + def area: Double = Circle.calculateArea(radius) + +object Circle: + private def calculateArea(radius: Double): Double = Pi * pow(radius, 2.0) + +val circle1 = Circle(5.0) +circle1.area +``` + +{% endtab %} +{% endtabs %} + +В этом примере метод `area`, доступный для каждого экземпляра `Circle`, +использует метод `calculateArea`, определенный в сопутствующем объекте. +Кроме того, поскольку `calculateArea` является приватным, к нему нельзя получить доступ с помощью другого кода, +но, как показано, его могут видеть экземпляры класса `Circle`. + +### Другие виды использования сопутствующих объектов + +Сопутствующие объекты могут использоваться для нескольких целей: + +- их можно использовать для группировки “статических” методов в пространстве имен, как в примере выше + - эти методы могут быть `public` или `private` + - если бы `calculateArea` был `public`, к нему можно было бы получить доступ из любого места как `Circle.calculateArea` +- они могут содержать методы `apply`, которые — благодаря некоторому синтаксическому сахару — + работают как фабричные методы для создания новых экземпляров +- они могут содержать методы `unapply`, которые используются для деконструкции объектов, например, с помощью сопоставления с шаблоном + +Вот краткий обзор того, как методы `apply` можно использовать в качестве фабричных методов для создания новых объектов: + +{% tabs companion-use class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +class Person { + var name = "" + var age = 0 + override def toString = s"$name is $age years old" +} + +object Person { + // фабричный метод с одним аргументом + def apply(name: String): Person = { + var p = new Person + p.name = name + p + } + + // фабричный метод с двумя аргументами + def apply(name: String, age: Int): Person = { + var p = new Person + p.name = name + p.age = age + p + } +} + +val joe = Person("Joe") +val fred = Person("Fred", 29) + +//val joe: Person = Joe is 0 years old +//val fred: Person = Fred is 29 years old +``` + +Метод `unapply` здесь не рассматривается, но описан в [Спецификации языка](https://scala-lang.org/files/archive/spec/2.13/08-pattern-matching.html#extractor-patterns). + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +class Person: + var name = "" + var age = 0 + override def toString = s"$name is $age years old" + +object Person: + + // фабричный метод с одним аргументом + def apply(name: String): Person = + var p = new Person + p.name = name + p + + // фабричный метод с двумя аргументами + def apply(name: String, age: Int): Person = + var p = new Person + p.name = name + p.age = age + p + +end Person + +val joe = Person("Joe") +val fred = Person("Fred", 29) + +//val joe: Person = Joe is 0 years old +//val fred: Person = Fred is 29 years old +``` + +Метод `unapply` здесь не рассматривается, но описан в [справочной документации]({{ site.scala3ref }}/changed-features/pattern-matching.html). + +{% endtab %} +{% endtabs %} + +## Трейты + +Если провести аналогию с Java, то Scala `trait` похож на интерфейс в Java 8+. +Trait-ы могут содержать: + +- абстрактные методы и поля +- конкретные методы и поля + +В базовом использовании `trait` может использоваться как интерфейс, определяющий только абстрактные члены, +которые будут реализованы другими классами: + +{% tabs traits_1 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +trait Employee { + def id: Int + def firstName: String + def lastName: String +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +trait Employee: + def id: Int + def firstName: String + def lastName: String +``` + +{% endtab %} +{% endtabs %} + +Однако трейты также могут содержать конкретные члены. +Например, следующий трейт определяет два абстрактных члена — `numLegs` и `walk()` — +а также имеет конкретную реализацию метода `stop()`: + +{% tabs traits_2 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +trait HasLegs { + def numLegs: Int + def walk(): Unit + def stop() = println("Stopped walking") +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +trait HasLegs: + def numLegs: Int + def walk(): Unit + def stop() = println("Stopped walking") +``` + +{% endtab %} +{% endtabs %} + +Вот еще один трейт с абстрактным членом и двумя конкретными реализациями: + +{% tabs traits_3 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +trait HasTail { + def tailColor: String + def wagTail() = println("Tail is wagging") + def stopTail() = println("Tail is stopped") +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +trait HasTail: + def tailColor: String + def wagTail() = println("Tail is wagging") + def stopTail() = println("Tail is stopped") +``` + +{% endtab %} +{% endtabs %} + +Обратите внимание, что каждый трейт обрабатывает только очень специфичные атрибуты и поведение: +`HasLegs` имеет дело только с "лапами", а `HasTail` имеет дело только с функциональностью, связанной с хвостом. +Трейты позволяют создавать такие небольшие модули. + +Позже в вашем коде классы могут смешивать несколько трейтов для создания более крупных компонентов: + +{% tabs traits_4 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +class IrishSetter(name: String) extends HasLegs with HasTail { + val numLegs = 4 + val tailColor = "Red" + def walk() = println("I’m walking") + override def toString = s"$name is a Dog" +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +class IrishSetter(name: String) extends HasLegs, HasTail: + val numLegs = 4 + val tailColor = "Red" + def walk() = println("I’m walking") + override def toString = s"$name is a Dog" +``` + +{% endtab %} +{% endtabs %} + +Обратите внимание, что класс `IrishSetter` реализует абстрактные члены, определенные в `HasLegs` и `HasTail`. +Теперь вы можете создавать новые экземпляры `IrishSetter`: + +{% tabs traits_5 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +val d = new IrishSetter("Big Red") // "Big Red is a Dog" +``` + +{% endtab %} +{% tab 'Scala 3' %} + +```scala +val d = IrishSetter("Big Red") // "Big Red is a Dog" +``` + +{% endtab %} +{% endtabs %} + +Это всего лишь пример того, чего можно добиться с помощью trait-ов. +Дополнительные сведения см. в остальных уроках по моделированию. + +## Абстрактные классы + +Когда необходимо написать класс, но известно, что в нем будут абстрактные члены, можно создать либо `trait`, либо абстрактный класс. +В большинстве случаев желательно использовать `trait`, но исторически сложилось так, что было две ситуации, +когда предпочтительнее использование абстрактного класса: + +- необходимо создать базовый класс, который принимает аргументы конструктора +- код будет вызван из Java-кода + +### Базовый класс, который принимает аргументы конструктора + +До Scala 3, когда базовому классу нужно было принимать аргументы конструктора, он объявлялся как `abstract class`: + +{% tabs abstract_1 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +abstract class Pet(name: String) { + def greeting: String + def age: Int + override def toString = s"My name is $name, I say $greeting, and I’m $age" +} + +class Dog(name: String, var age: Int) extends Pet(name) { + val greeting = "Woof" +} + +val d = new Dog("Fido", 1) +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +abstract class Pet(name: String): + def greeting: String + def age: Int + override def toString = s"My name is $name, I say $greeting, and I’m $age" + +class Dog(name: String, var age: Int) extends Pet(name): + val greeting = "Woof" + +val d = Dog("Fido", 1) +``` + +{% endtab %} +{% endtabs %} + +

Параметры в trait только в Scala 3

+ +Однако в Scala 3 трейты теперь могут иметь [параметры][trait-params], +так что теперь вы можете использовать трейты в той же ситуации: + +{% tabs abstract_2 %} + +{% tab 'Только в Scala 3' %} + +```scala +trait Pet(name: String): + def greeting: String + def age: Int + override def toString = s"My name is $name, I say $greeting, and I’m $age" + +class Dog(name: String, var age: Int) extends Pet(name): + val greeting = "Woof" + +val d = Dog("Fido", 1) +``` + +{% endtab %} +{% endtabs %} + +Trait-ы более гибки в составлении, потому что можно смешивать (наследовать) несколько trait-ов, но только один класс. +В большинстве случаев trait-ы следует предпочитать классам и абстрактным классам. +Правило выбора состоит в том, чтобы использовать классы всякий раз, когда необходимо создавать экземпляры определенного типа, +и trait-ы, когда желательно разложить и повторно использовать поведение. + +

Перечисления только в Scala 3

+ +Перечисление (_an enumeration_) может быть использовано для определения типа, +состоящего из конечного набора именованных значений (в разделе, посвященном [моделированию ФП][fp-modeling], +будут показаны дополнительные возможности перечислений). +Базовые перечисления используются для определения наборов констант, +таких как месяцы в году, дни в неделе, направления, такие как север/юг/восток/запад, и многое другое. + +В качестве примера, рассмотрим перечисления, определяющие наборы атрибутов, связанных с пиццами: + +{% tabs enum_1 %} +{% tab 'Только в Scala 3' %} + +```scala +enum CrustSize: + case Small, Medium, Large + +enum CrustType: + case Thin, Thick, Regular + +enum Topping: + case Cheese, Pepperoni, BlackOlives, GreenOlives, Onions +``` + +{% endtab %} +{% endtabs %} + +Для использования в коде в первую очередь перечисление нужно импортировать, а затем - использовать: + +{% tabs enum_2 %} +{% tab 'Только в Scala 3' %} + +```scala +import CrustSize.* +val currentCrustSize = Small +``` + +{% endtab %} +{% endtabs %} + +Значения перечислений можно сравнивать (`==`) и использовать в сопоставлении: + +{% tabs enum_3 %} +{% tab 'Только в Scala 3' %} + +```scala +// if/then +if currentCrustSize == Large then + println("You get a prize!") + +// match +currentCrustSize match + case Small => println("small") + case Medium => println("medium") + case Large => println("large") +``` + +{% endtab %} +{% endtabs %} + +### Дополнительные функции перечисления + +Перечисления также могут быть параметризованы: + +{% tabs enum_4 %} +{% tab 'Только в Scala 3' %} + +```scala +enum Color(val rgb: Int): + case Red extends Color(0xFF0000) + case Green extends Color(0x00FF00) + case Blue extends Color(0x0000FF) +``` + +{% endtab %} +{% endtabs %} + +И они также могут содержать элементы (например, поля и методы): + +{% tabs enum_5 %} +{% tab 'Только в Scala 3' %} + +```scala +enum Planet(mass: Double, radius: Double): + private final val G = 6.67300E-11 + def surfaceGravity = G * mass / (radius * radius) + def surfaceWeight(otherMass: Double) = + otherMass * surfaceGravity + + case Mercury extends Planet(3.303e+23, 2.4397e6) + case Earth extends Planet(5.976e+24, 6.37814e6) + // далее идут остальные планеты ... +``` + +{% endtab %} +{% endtabs %} + +### Совместимость с перечислениями Java + +Если вы хотите использовать перечисления, определенные в Scala, как перечисления Java, +то можете сделать это, расширив класс `java.lang.Enum` (импортированный по умолчанию) следующим образом: + +{% tabs enum_6 %} +{% tab 'Только в Scala 3' %} + +```scala +enum Color extends Enum[Color] { case Red, Green, Blue } +``` + +{% endtab %} +{% endtabs %} + +Параметр типа берется из определения Java `enum` и должен совпадать с типом перечисления. +Нет необходимости предоставлять аргументы конструктора (как определено в документации Java API) для `java.lang.Enum` +при его расширении — компилятор генерирует их автоматически. + +После такого определения `Color` вы можете использовать его так же, как перечисление Java: + +```` +scala> Color.Red.compareTo(Color.Green) +val res0: Int = -1 +```` + +В разделе об [алгебраических типах данных][adts] и [справочной документации][ref-enums] перечисления рассматриваются более подробно. + +## Case class-ы + +Case class используются для моделирования неизменяемых структур данных. +Возьмем следующий пример: + +{% tabs case-classes_1 %} +{% tab 'Scala 2 и 3' %} + +```scala: +case class Person(name: String, relation: String) +``` + +{% endtab %} +{% endtabs %} + +Поскольку мы объявляем `Person` как `case class`, поля `name` и `relation` по умолчанию общедоступны и неизменяемы. +Мы можем создавать экземпляры case классов следующим образом: + +{% tabs case-classes_2 %} +{% tab 'Scala 2 и 3' %} + +```scala +val christina = Person("Christina", "niece") +``` + +{% endtab %} +{% endtabs %} + +Обратите внимание, что поля не могут быть изменены: + +{% tabs case-classes_3 %} +{% tab 'Scala 2 и 3' %} + +```scala +christina.name = "Fred" // ошибка: reassignment to val +``` + +{% endtab %} +{% endtabs %} + +Поскольку предполагается, что поля case класса неизменяемы, +компилятор Scala может сгенерировать для вас множество полезных методов: + +- Генерируется метод `unapply`, позволяющий выполнять сопоставление с образцом case класса (то есть `case Person(n, r) => ...`). +- В классе генерируется метод `copy`, полезный для создания модифицированных копий экземпляра. +- Генерируются методы `equals` и `hashCode`, использующие структурное равенство, + что позволяет использовать экземпляры case классов в `Map`-ах. +- Генерируется дефолтный метод `toString`, полезный для отладки. + +Эти дополнительные функции показаны в следующем примере: + +{% tabs case-classes_4 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +// Case class-ы можно использовать в качестве шаблонов +christina match { + case Person(n, r) => println("name is " + n) +} + +// для вас генерируются методы `equals` и `hashCode` +val hannah = Person("Hannah", "niece") +christina == hannah // false + +// метод `toString` +println(christina) // Person(Christina,niece) + +// встроенный метод `copy` +case class BaseballTeam(name: String, lastWorldSeriesWin: Int) +val cubs1908 = BaseballTeam("Chicago Cubs", 1908) +val cubs2016 = cubs1908.copy(lastWorldSeriesWin = 2016) +// в результате: +// cubs2016: BaseballTeam = BaseballTeam(Chicago Cubs,2016) + +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +// Case class-ы можно использовать в качестве шаблонов +christina match + case Person(n, r) => println("name is " + n) + +// для вас генерируются методы `equals` и `hashCode` +val hannah = Person("Hannah", "niece") +christina == hannah // false + +// метод `toString` +println(christina) // Person(Christina,niece) + +// встроенный метод `copy` +case class BaseballTeam(name: String, lastWorldSeriesWin: Int) +val cubs1908 = BaseballTeam("Chicago Cubs", 1908) +val cubs2016 = cubs1908.copy(lastWorldSeriesWin = 2016) +// в результате: +// cubs2016: BaseballTeam = BaseballTeam(Chicago Cubs,2016) +``` + +{% endtab %} +{% endtabs %} + +### Поддержка функционального программирования + +Как уже упоминалось ранее, case class-ы поддерживают функциональное программирование (ФП): + +- ФП избегает изменения структур данных. + Поэтому поля конструктора по умолчанию имеют значение `val`. + Поскольку экземпляры case class не могут быть изменены, ими можно легко делиться, не опасаясь мутаций или условий гонки. +- вместо изменения экземпляра можно использовать метод `copy` в качестве шаблона для создания нового (потенциально измененного) экземпляра. + Этот процесс можно назвать “обновлением по мере копирования”. +- наличие автоматически сгенерированного метода `unapply` позволяет использовать case class в сопоставлении шаблонов. + +## Case object-ы + +Case object-ы относятся к объектам так же, как case class-ы относятся к классам: +они предоставляют ряд автоматически генерируемых методов, чтобы сделать их более мощными. +Case object-ы особенно полезны тогда, когда необходим одноэлементный объект, +который нуждается в небольшой дополнительной функциональности, +например, для использования с сопоставлением шаблонов в выражениях `match`. + +Case object-ы полезны, когда необходимо передавать неизменяемые сообщения. +Например, представим проект музыкального проигрывателя, и создадим набор команд или сообщений: + +{% tabs case-objects_1 %} +{% tab 'Scala 2 и 3' %} + +```scala +sealed trait Message +case class PlaySong(name: String) extends Message +case class IncreaseVolume(amount: Int) extends Message +case class DecreaseVolume(amount: Int) extends Message +case object StopPlaying extends Message +``` + +{% endtab %} +{% endtabs %} + +Затем в других частях кода можно написать методы, которые используют сопоставление с образцом +для обработки входящего сообщения +(при условии, что методы `playSong`, `changeVolume` и `stopPlayingSong` определены где-то еще): + +{% tabs case-objects_2 class=tabs-scala-version %} +{% tab 'Scala 2' %} + +```scala +def handleMessages(message: Message): Unit = message match { + case PlaySong(name) => playSong(name) + case IncreaseVolume(amount) => changeVolume(amount) + case DecreaseVolume(amount) => changeVolume(-amount) + case StopPlaying => stopPlayingSong() +} +``` + +{% endtab %} + +{% tab 'Scala 3' %} + +```scala +def handleMessages(message: Message): Unit = message match + case PlaySong(name) => playSong(name) + case IncreaseVolume(amount) => changeVolume(amount) + case DecreaseVolume(amount) => changeVolume(-amount) + case StopPlaying => stopPlayingSong() +``` + +{% endtab %} +{% endtabs %} + +[ref-enums]: {{ site.scala3ref }}/enums/enums.html +[adts]: {% link _overviews/scala3-book/types-adts-gadts.md %} +[fp-modeling]: {% link _overviews/scala3-book/domain-modeling-fp.md %} +[creator]: {{ site.scala3ref }}/other-new-features/creator-applications.html +[unapply]: {{ site.scala3ref }}/changed-features/pattern-matching.html +[trait-params]: {{ site.scala3ref }}/other-new-features/trait-parameters.html diff --git a/_ru/scala3/book/first-look-at-types.md b/_ru/scala3/book/first-look-at-types.md index 603157c6d1..c1bbe20d4b 100644 --- a/_ru/scala3/book/first-look-at-types.md +++ b/_ru/scala3/book/first-look-at-types.md @@ -9,7 +9,7 @@ description: На этой странице представлено кратк language: ru num: 17 previous-page: taste-summary -next-page: +next-page: control-structures --- @@ -47,7 +47,7 @@ next-page: такие как следующий метод, для той же цели используется `Unit`: {% tabs unit %} -{% tab 'Scala 2 and 3' for=unit %} +{% tab 'Scala 2 и 3' for=unit %} ```scala def printIt(a: Any): Unit = println(a) ``` @@ -58,7 +58,7 @@ def printIt(a: Any): Unit = println(a) и могут обрабатываться так же, как и любой другой объект: {% tabs any %} -{% tab 'Scala 2 and 3' for=any %} +{% tab 'Scala 2 и 3' for=any %} ```scala val list: List[Any] = List( "a string", @@ -93,7 +93,7 @@ true В этих примерах показано, как объявлять переменные этих числовых типов: {% tabs anyval %} -{% tab 'Scala 2 and 3' for=anyval %} +{% tab 'Scala 2 и 3' for=anyval %} ```scala val b: Byte = 1 val i: Int = 1 @@ -113,7 +113,7 @@ val f: Float = 3.0 Поскольку `Int` и `Double` являются числовыми типами по умолчанию, их можно создавать без явного объявления типа данных: {% tabs anynum %} -{% tab 'Scala 2 and 3' for=anynum %} +{% tab 'Scala 2 и 3' for=anynum %} ```scala val i = 123 // по умолчанию Int val x = 1.0 // по умолчанию Double @@ -125,7 +125,7 @@ val x = 1.0 // по умолчанию Double для того, чтобы задать `Long`, `Double` или `Float` значения: {% tabs type-post %} -{% tab 'Scala 2 and 3' for=type-post %} +{% tab 'Scala 2 и 3' for=type-post %} ```scala val x = 1_000L // val x: Long = 1000 val y = 2.2D // val y: Double = 2.2 @@ -137,7 +137,7 @@ val z = 3.3F // val z: Float = 3.3 В Scala также есть типы `String` и `Char`, которые обычно можно объявить в неявной форме: {% tabs type-string %} -{% tab 'Scala 2 and 3' for=type-string %} +{% tab 'Scala 2 и 3' for=type-string %} ```scala val s = "Bill" val c = 'a' @@ -168,7 +168,7 @@ val c = 'a' Для действительно больших чисел можно использовать типы `BigInt` и `BigDecimal`: {% tabs type-bigint %} -{% tab 'Scala 2 and 3' for=type-bigint %} +{% tab 'Scala 2 и 3' for=type-bigint %} ```scala val a = BigInt(1_234_567_890_987_654_321L) val b = BigDecimal(123_456.789) @@ -182,7 +182,7 @@ val b = BigDecimal(123_456.789) `BigInt` и `BigDecimal` поддерживают все привычные числовые операторы: {% tabs type-bigint2 %} -{% tab 'Scala 2 and 3' for=type-bigint2 %} +{% tab 'Scala 2 и 3' for=type-bigint2 %} ```scala val b = BigInt(1234567890) // scala.math.BigInt = 1234567890 val c = b + b // scala.math.BigInt = 2469135780 @@ -204,7 +204,7 @@ val d = b * b // scala.math.BigInt = 1524157875019052100 Например, учитывая эти три переменные: {% tabs string-inside1 %} -{% tab 'Scala 2 and 3' for=string-inside1 %} +{% tab 'Scala 2 и 3' for=string-inside1 %} ```scala val firstName = "John" val mi = 'C' @@ -216,7 +216,7 @@ val lastName = "Doe" их комбинацию можно получить так: {% tabs string-inside2 %} -{% tab 'Scala 2 and 3' for=string-inside2 %} +{% tab 'Scala 2 и 3' for=string-inside2 %} ```scala println(s"Name: $firstName $mi $lastName") // "Name: John C Doe" ``` @@ -228,7 +228,7 @@ println(s"Name: $firstName $mi $lastName") // "Name: John C Doe" Чтобы вставить произвольные выражения в строку, они заключаются в фигурные скобки: {% tabs string-inside3 %} -{% tab 'Scala 2 and 3' for=string-inside3 %} +{% tab 'Scala 2 и 3' for=string-inside3 %} ```scala println(s"2 + 2 = ${2 + 2}") // печатает "2 + 2 = 4" val x = -1 @@ -249,7 +249,7 @@ println(s"x.abs = ${x.abs}") // печатает "x.abs = 1" Многострочные строки создаются путем включения строки в три двойные кавычки: {% tabs string-mlines1 %} -{% tab 'Scala 2 and 3' for=string-mlines1 %} +{% tab 'Scala 2 и 3' for=string-mlines1 %} ```scala val quote = """The essence of Scala: Fusion of functional and object-oriented @@ -261,7 +261,7 @@ val quote = """The essence of Scala: Одним из недостатков базового подхода является то, что строки после первой имеют отступ. {% tabs string-mlines2 %} -{% tab 'Scala 2 and 3' for=string-mlines2 %} +{% tab 'Scala 2 и 3' for=string-mlines2 %} ```scala "The essence of Scala: Fusion of functional and object-oriented @@ -273,7 +273,7 @@ val quote = """The essence of Scala: Если важно исключить отступ, можно поставить символ `|` перед всеми строками после первой и вызвать метод `stripMargin` после строки: {% tabs string-mlines3 %} -{% tab 'Scala 2 and 3' for=string-mlines3 %} +{% tab 'Scala 2 и 3' for=string-mlines3 %} ```scala val quote = """The essence of Scala: |Fusion of functional and object-oriented @@ -285,7 +285,7 @@ val quote = """The essence of Scala: Теперь все строки выравниваются по левому краю: {% tabs string-mlines4 %} -{% tab 'Scala 2 and 3' for=string-mlines4 %} +{% tab 'Scala 2 и 3' for=string-mlines4 %} ```scala "The essence of Scala: Fusion of functional and object-oriented @@ -303,7 +303,7 @@ programming in a typed setting." Например: {% tabs cast1 %} -{% tab 'Scala 2 and 3' for=cast1 %} +{% tab 'Scala 2 и 3' for=cast1 %} ```scala val b: Byte = 127 val i: Int = b // 127 @@ -318,7 +318,7 @@ val number: Int = face // 9786 В противном случае вам нужно четко указать приведение типов: {% tabs cast2 %} -{% tab 'Scala 2 and 3' for=cast2 %} +{% tab 'Scala 2 и 3' for=cast2 %} ```scala val x: Long = 987654321 val y: Float = x.toFloat // 9.8765434E8 (обратите внимание, что требуется `.toFloat`, потому что приведение приводит к потере точности) diff --git a/_ru/scala3/book/fun-anonymous-functions.md b/_ru/scala3/book/fun-anonymous-functions.md new file mode 100644 index 0000000000..d5d0046917 --- /dev/null +++ b/_ru/scala3/book/fun-anonymous-functions.md @@ -0,0 +1,214 @@ +--- +layout: multipage-overview +title: Анонимные функции +scala3: true +partof: scala3-book +overview-name: "Scala 3 — Book" +type: section +description: На этой странице показано, как использовать анонимные функции в Scala, включая примеры с функциями map и filter класса List. +language: ru +num: 28 +previous-page: fun-intro +next-page: fun-function-variables +--- + +Анонимная функция, также известная как _лямбда_, представляет собой блок кода, +который передается в качестве аргумента функции высшего порядка. +Википедия определяет [анонимную функцию](https://en.wikipedia.org/wiki/Anonymous_function) +как “определение функции, не привязанное к идентификатору”. + +Например, возьмем коллекцию: + +{% tabs fun-anonymous-1 %} +{% tab 'Scala 2 и 3' %} +```scala +val ints = List(1, 2, 3) +``` +{% endtab %} +{% endtabs %} + +Можно создать новый список, удвоив каждый элемент в целых числах, используя метод `map` класса `List` +и свою пользовательскую анонимную функцию: + +{% tabs fun-anonymous-2 %} +{% tab 'Scala 2 и 3' %} +```scala +val doubledInts = ints.map(_ * 2) // List(2, 4, 6) +``` +{% endtab %} +{% endtabs %} + +Как видно из комментария, `doubleInts` содержит список `List(2, 4, 6)`. +В этом примере анонимной функцией является часть кода: + +{% tabs fun-anonymous-3 %} +{% tab 'Scala 2 и 3' %} +```scala +_ * 2 +``` +{% endtab %} +{% endtabs %} + +Это сокращенный способ сказать: “Умножить данный элемент на 2”. + +## Более длинные формы + +Когда вы освоитесь со Scala, то будете постоянно использовать эту форму для написания анонимных функций, +использующих одну переменную в одном месте функции. +Но при желании можете также написать их, используя более длинные формы, +поэтому в дополнение к написанию этого кода: + +{% tabs fun-anonymous-4 %} +{% tab 'Scala 2 и 3' %} +```scala +val doubledInts = ints.map(_ * 2) +``` +{% endtab %} +{% endtabs %} + +вы также можете написать его, используя такие формы: + +{% tabs fun-anonymous-5 %} +{% tab 'Scala 2 и 3' %} +```scala +val doubledInts = ints.map((i: Int) => i * 2) +val doubledInts = ints.map((i) => i * 2) +val doubledInts = ints.map(i => i * 2) +``` +{% endtab %} +{% endtabs %} + +Все эти строки имеют одно и то же значение: удваивайте каждый элемент `ints`, чтобы создать новый список, `doubledInts` +(синтаксис каждой формы объясняется ниже). + +Если вы знакомы с Java, вам будет полезно узнать, что эти примеры `map` эквивалентны следующему Java коду: + +{% tabs fun-anonymous-5-b %} +{% tab 'Java' %} +```java +List ints = List.of(1, 2, 3); +List doubledInts = ints.stream() + .map(i -> i * 2) + .collect(Collectors.toList()); +``` +{% endtab %} +{% endtabs %} + +## Сокращение анонимных функций + +Если необходимо явно указать анонимную функцию, можно использовать следующую длинную форму: + +{% tabs fun-anonymous-6 %} +{% tab 'Scala 2 и 3' %} +```scala +val doubledInts = ints.map((i: Int) => i * 2) +``` +{% endtab %} +{% endtabs %} + +Анонимная функция в этом выражении такова: + +{% tabs fun-anonymous-7 %} +{% tab 'Scala 2 и 3' %} +```scala +(i: Int) => i * 2 +``` +{% endtab %} +{% endtabs %} + +Если незнаком данный синтаксис, то можно воспринимать символ `=>` как преобразователь, +потому что выражение _преобразует_ список параметров в левой части символа (переменная `Int` с именем `i`) +в новый результат, используя алгоритм справа от символа `=>` +(в данном случае выражение, которое удваивает значение `Int`). + + +### Сокращение выражения + +Эту длинную форму можно сократить, как будет показано в следующих шагах. +Во-первых, вот снова самая длинная и явная форма: + +{% tabs fun-anonymous-8 %} +{% tab 'Scala 2 и 3' %} +```scala +val doubledInts = ints.map((i: Int) => i * 2) +``` +{% endtab %} +{% endtabs %} + +Поскольку компилятор Scala может сделать вывод из данных в `ints` о том, что `i` - это `Int`, +`Int` объявление можно удалить: + +{% tabs fun-anonymous-9 %} +{% tab 'Scala 2 и 3' %} +```scala +val doubledInts = ints.map((i) => i * 2) +``` +{% endtab %} +{% endtabs %} + +Поскольку есть только один аргумент, круглые скобки вокруг параметра `i` не нужны: + +{% tabs fun-anonymous-10 %} +{% tab 'Scala 2 и 3' %} +```scala +val doubledInts = ints.map(i => i * 2) +``` +{% endtab %} +{% endtabs %} + +Поскольку Scala позволяет использовать символ `_` вместо имени переменной, +когда параметр появляется в функции только один раз, код можно упростить еще больше: + +{% tabs fun-anonymous-11 %} +{% tab 'Scala 2 и 3' %} +```scala +val doubledInts = ints.map(_ * 2) +``` +{% endtab %} +{% endtabs %} + +### Ещё короче + +В других примерах можно еще больше упростить анонимные функции. +Например, начиная с самой явной формы, можно распечатать каждый элемент в `ints`, +используя эту анонимную функцию с методом `foreach` класса `List`: + +{% tabs fun-anonymous-12 %} +{% tab 'Scala 2 и 3' %} +```scala +ints.foreach((i: Int) => println(i)) +``` +{% endtab %} +{% endtabs %} + +Как и раньше, объявление `Int` не требуется, а поскольку аргумент всего один, скобки вокруг `i` не нужны: + +{% tabs fun-anonymous-13 %} +{% tab 'Scala 2 и 3' %} +```scala +ints.foreach(i => println(i)) +``` +{% endtab %} +{% endtabs %} + +Поскольку `i` используется в теле функции только один раз, выражение можно еще больше упростить с помощью символа `_`: + +{% tabs fun-anonymous-14 %} +{% tab 'Scala 2 и 3' %} +```scala +ints.foreach(println(_)) +``` +{% endtab %} +{% endtabs %} + +Наконец, если анонимная функция состоит из одного вызова метода с одним аргументом, +нет необходимости явно называть и указывать аргумент, +можно написать только имя метода (здесь, `println`): + +{% tabs fun-anonymous-15 %} +{% tab 'Scala 2 и 3' %} +```scala +ints.foreach(println) +``` +{% endtab %} +{% endtabs %} diff --git a/_ru/scala3/book/fun-eta-expansion.md b/_ru/scala3/book/fun-eta-expansion.md new file mode 100644 index 0000000000..9e2baf3810 --- /dev/null +++ b/_ru/scala3/book/fun-eta-expansion.md @@ -0,0 +1,92 @@ +--- +layout: multipage-overview +title: Eta расширение +scala3: true +partof: scala3-book +overview-name: "Scala 3 — Book" +type: section +description: На этой странице обсуждается Eta Expansion, технология Scala, которая автоматически и прозрачно преобразует методы в функции. +language: ru +num: 30 +previous-page: fun-function-variables +next-page: fun-hofs +--- + + +Если посмотреть на Scaladoc для метода `map` в классах коллекций Scala, +то можно увидеть, что метод определен для приема _функции_: + +```scala +def map[B](f: (A) => B): List[B] + ----------- +``` + +Действительно, в Scaladoc сказано: “`f` — это _функция_, применяемая к каждому элементу”. +Но, несмотря на это, каким-то образом в `map` можно передать _метод_, и он все еще работает: + +```scala +def times10(i: Int) = i * 10 // метод +List(1, 2, 3).map(times10) // List(10,20,30) +``` + +Как это работает? Как можно передать _метод_ в `map`, который ожидает _функцию_? + +Технология, стоящая за этим, известна как _Eta Expansion_. +Она преобразует выражение _типа метода_ в эквивалентное выражение _типа функции_, и делает это легко и незаметно. + + +## Различия между методами и функциями + +Исторически _методы_ были частью определения класса, хотя в Scala 3 методы могут быть вне классов, +такие как [определения верхнего уровня][toplevel] и [методы расширения][extension]. + +В отличие от методов, _функции_ сами по себе являются полноценными объектами, что делает их объектами первого класса. + +Их синтаксис также отличается. +В этом примере показано, как задать метод и функцию, которые выполняют одну и ту же задачу, +определяя, является ли заданное целое число четным: + +```scala +def isEvenMethod(i: Int) = i % 2 == 0 // метод +val isEvenFunction = (i: Int) => i % 2 == 0 // функция +``` + +Функция действительно является объектом, поэтому ее можно использовать так же, +как и любую другую переменную, например, помещая в список: + +```scala +val functions = List(isEvenFunction) +``` + +И наоборот, технически метод не является объектом, поэтому в Scala 2 метод нельзя было поместить в `List`, +по крайней мере, напрямую, как показано в этом примере: + +```scala +// В этом примере показано сообщение об ошибке в Scala 2 +val methods = List(isEvenMethod) + ^ +error: missing argument list for method isEvenMethod +Unapplied methods are only converted to functions when a function type is expected. +You can make this conversion explicit by writing `isEvenMethod _` or `isEvenMethod(_)` instead of `isEvenMethod`. +``` + +Как показано в этом сообщении об ошибке, в Scala 2 существует ручной способ преобразования метода в функцию, +но важной частью для Scala 3 является то, что технология Eta Expansion улучшена, +поэтому теперь, когда попытаться использовать метод в качестве переменной, +он просто работает — не нужно самостоятельно выполнять ручное преобразование: + +```scala +val functions = List(isEvenFunction) // работает +val methods = List(isEvenMethod) // работает +``` + +Для целей этой вводной книги важно знать следующее: + +- Eta Expansion — технология Scala, позволяющая использовать методы так же, как и функции +- Технология была улучшена в Scala 3, чтобы быть почти полностью бесшовной + +Дополнительные сведения о том, как это работает, см. на [странице Eta Expansion][eta_expansion] в справочной документации. + +[eta_expansion]: {{ site.scala3ref }}/changed-features/eta-expansion.html +[extension]: {% link _overviews/scala3-book/ca-extension-methods.md %} +[toplevel]: {% link _overviews/scala3-book/taste-toplevel-definitions.md %} diff --git a/_ru/scala3/book/fun-function-variables.md b/_ru/scala3/book/fun-function-variables.md new file mode 100644 index 0000000000..bd41113ac1 --- /dev/null +++ b/_ru/scala3/book/fun-function-variables.md @@ -0,0 +1,172 @@ +--- +layout: multipage-overview +title: Параметры функции +scala3: true +partof: scala3-book +overview-name: "Scala 3 — Book" +type: section +description: На этой странице показано, как использовать параметры функции в Scala. +language: ru +num: 29 +previous-page: fun-anonymous-functions +next-page: fun-eta-expansion +--- + + +Вернемся к примеру из предыдущего раздела: + +{% tabs fun-function-variables-1 %} +{% tab 'Scala 2 и 3' %} +```scala +val doubledInts = ints.map((i: Int) => i * 2) +``` +{% endtab %} +{% endtabs %} + +Анонимной функцией является следующая часть: + +{% tabs fun-function-variables-2 %} +{% tab 'Scala 2 и 3' %} +```scala +(i: Int) => i * 2 +``` +{% endtab %} +{% endtabs %} + +Причина, по которой она называется _анонимной_ (_anonymous_), заключается в том, +что она не присваивается переменной и, следовательно, не имеет имени. + +Однако анонимная функция, также известная как _функциональный литерал_ (_function literal_), +может быть назначена переменной для создания _функциональной переменной_ (_function variable_): + +{% tabs fun-function-variables-3 %} +{% tab 'Scala 2 и 3' %} +```scala +val double = (i: Int) => i * 2 +``` +{% endtab %} +{% endtabs %} + +Код выше создает функциональную переменную с именем `double`. +В этом выражении исходный литерал функции находится справа от символа `=`: + +{% tabs fun-function-variables-4 %} +{% tab 'Scala 2 и 3' %} +```scala +val double = (i: Int) => i * 2 + ----------------- +``` +{% endtab %} +{% endtabs %} + +, а новое имя переменной - слева: + +{% tabs fun-function-variables-5 %} +{% tab 'Scala 2 и 3' %} +```scala +val double = (i: Int) => i * 2 + ------ +``` +{% endtab %} +{% endtabs %} + +список параметров функции подчеркнут: + +{% tabs fun-function-variables-6 %} +{% tab 'Scala 2 и 3' %} +```scala +val double = (i: Int) => i * 2 + -------- +``` +{% endtab %} +{% endtabs %} + +Как и список параметров для метода, список параметров функции означает, +что функция `double` принимает один параметр с типом `Int` и именем `i`. +Как можно видеть ниже, `double` имеет тип `Int => Int`, +что означает, что он принимает один параметр `Int` и возвращает `Int`: + +{% tabs fun-function-variables-7 %} +{% tab 'Scala 2 и 3' %} +```scala +scala> val double = (i: Int) => i * 2 +val double: Int => Int = ... +``` +{% endtab %} +{% endtabs %} + + +### Вызов функции + +Функция `double` может быть вызвана так: + +{% tabs fun-function-variables-8 %} +{% tab 'Scala 2 и 3' %} +```scala +val x = double(2) // 4 +``` +{% endtab %} +{% endtabs %} + +`double` также можно передать в вызов `map`: + +{% tabs fun-function-variables-9 %} +{% tab 'Scala 2 и 3' %} +```scala +List(1, 2, 3).map(double) // List(2, 4, 6) +``` +{% endtab %} +{% endtabs %} + +Кроме того, когда есть другие функции типа `Int => Int`: + +{% tabs fun-function-variables-10 %} +{% tab 'Scala 2 и 3' %} +```scala +val triple = (i: Int) => i * 3 +``` +{% endtab %} +{% endtabs %} + +можно сохранить их в `List` или `Map`: + +{% tabs fun-function-variables-11 %} +{% tab 'Scala 2 и 3' %} +```scala +val functionList = List(double, triple) + +val functionMap = Map( + "2x" -> double, + "3x" -> triple +) +``` +{% endtab %} +{% endtabs %} + +Если вы вставите эти выражения в REPL, то увидите, что они имеют следующие типы: + +{% tabs fun-function-variables-12 %} +{% tab 'Scala 2 и 3' %} +```` +// список, содержащий функции типа `Int => Int` +functionList: List[Int => Int] + +// Map, ключи которой имеют тип `String`, +// а значения имеют тип `Int => Int` +functionMap: Map[String, Int => Int] +```` +{% endtab %} +{% endtabs %} + + + +## Ключевые моменты + +Ключевыми моментами здесь являются: + +- чтобы создать функциональную переменную, достаточно присвоить имя переменной функциональному литералу +- когда есть функция, с ней можно обращаться как с любой другой переменной, то есть как со `String` или `Int` переменной + +А благодаря улучшенной функциональности [Eta Expansion][eta_expansion] в Scala 3 с _методами_ можно обращаться точно так же. + +[eta_expansion]: {% link _overviews/scala3-book/fun-eta-expansion.md %} diff --git a/_ru/scala3/book/fun-hofs.md b/_ru/scala3/book/fun-hofs.md new file mode 100644 index 0000000000..ae8445463c --- /dev/null +++ b/_ru/scala3/book/fun-hofs.md @@ -0,0 +1,389 @@ +--- +layout: multipage-overview +title: Функции высшего порядка +scala3: true +partof: scala3-book +overview-name: "Scala 3 — Book" +type: section +description: На этой странице показано, как создавать и использовать функции высшего порядка в Scala. +language: ru +num: 31 +previous-page: fun-eta-expansion +next-page: fun-write-map-function +--- + + +Функция высшего порядка (HOF - higher-order function) часто определяется как функция, которая + +- принимает другие функции в качестве входных параметров или +- возвращает функцию в качестве результата. + +В Scala HOF возможны, потому что функции являются объектами первого класса. + +В качестве важного примечания: хотя в этом документе используется общепринятый термин “функция высшего порядка”, +в Scala эта фраза применима как к методам, так и к функциям. +Благодаря [технологии Eta Expansion][eta_expansion] их, как правило, можно использовать в одних и тех же местах. + + +## От потребителя к разработчику + +В примерах, приведенных ранее в документации, было видно, как _пользоваться_ методами, +которые принимают другие функции в качестве входных параметров, например, `map` и `filter`. + +В следующих разделах будет показано, как _создавать_ HOF, в том числе: + +- как писать методы, принимающие функции в качестве входных параметров +- как возвращать функции из методов + +В процессе будет видно: + +- синтаксис, который используется для определения входных параметров функции +- как вызвать функцию, если есть на нее ссылка + +В качестве полезного побочного эффекта, как только синтаксис станет привычным, +его можно начать использовать для определения параметров функций, анонимных функций и функциональных переменных, +а также станет легче читать Scaladoc для функций высшего порядка. + + +## Понимание Scaladoc метода filter + +Чтобы понять, как работают функции высшего порядка, рассмотрим пример: +определим, какой тип функций принимает `filter`, взглянув на его Scaladoc. +Вот определение `filter` в классе `List[A]`: + +{% tabs filter-definition %} +{% tab 'Scala 2 и 3' %} +```scala +def filter(p: A => Boolean): List[A] +``` +{% endtab %} +{% endtabs %} + +Это определение указывает на то, что `filter` - метод, который принимает параметр функции с именем `p`. +По соглашению, `p` обозначает _предикат_, который представляет собой просто функцию, возвращающую `Boolean`. +Таким образом, `filter` принимает предикат `p` в качестве входного параметра и возвращает `List[A]`, +где `A` - тип, содержащийся в списке; если `filter` вызывается для `List[Int]`, то `A` - это тип `Int`. + +На данный момент, если не учитывать назначение метода `filter`, +все, что известно, так это то, что алгоритм каким-то образом использует предикат `p` для создания и возврата `List[A]`. + +Если посмотреть конкретно на параметр функции `p`: + +```scala +p: A => Boolean +``` + +, то эта часть описания `filter` означает, что любая передаваемая функция +должна принимать тип `A` в качестве входного параметра и возвращать `Boolean`. +Итак, если список представляет собой список `List[Int]`, +то можно заменить универсальный тип `A` на `Int` и прочитать эту подпись следующим образом: + +```scala +p: Int => Boolean +``` + +Поскольку `isEven` имеет такой же тип — преобразует входное значение `Int` в результирующее `Boolean` — +его можно использовать с `filter`. + + +## Написание методов, которые принимают параметры функции + +Рассмотрим пример написания методов, которые принимают функции в качестве входных параметров. + +**Примечание:** для определенности, будем называть код, который пишется, _методом_, +а код, принимаемый в качестве входного параметра, — _функцией_. + +### Пример + +Чтобы создать метод, который принимает функцию в качестве параметра, необходимо: + +1. в списке параметров метода определить сигнатуру принимаемой функции +2. использовать эту функцию внутри метода + +Чтобы продемонстрировать это, вот метод, который принимает входной параметр с именем `f`, где `f` — функция: + + +{% tabs sayHello-definition %} +{% tab 'Scala 2 и 3' %} +```scala +def sayHello(f: () => Unit): Unit = f() +``` +{% endtab %} +{% endtabs %} + +Эта часть кода — _сигнатура типа (type signature)_ — утверждает, что `f` является функцией, +и определяет типы функций, которые будет принимать метод `sayHello`: + +```scala +f: () => Unit +``` + +Как это работает: + +- `f` — имя входного параметра функции. + Аналогично тому, как параметр `String` обычно называется `s` или параметр `Int` - `i` +- сигнатура типа `f` определяет _тип_ функций, которые будет принимать метод +- часть `()` подписи `f` (слева от символа `=>`) указывает на то, что `f` не принимает входных параметров +- часть сигнатуры `Unit` (справа от символа `=>`) указывает на то, что функция `f` не должна возвращать осмысленный результат +- в теле метода `sayHello` (справа от символа `=`) оператор `f()` вызывает переданную функцию + +Теперь, когда `sayHello` определен, создадим функцию, соответствующую сигнатуре `f`, чтобы ее можно было проверить. +Следующая функция не принимает входных параметров и ничего не возвращает, поэтому она соответствует сигнатуре типа `f`: + +{% tabs helloJoe-definition %} +{% tab 'Scala 2 и 3' %} +```scala +def helloJoe(): Unit = println("Hello, Joe") +``` +{% endtab %} +{% endtabs %} + + +Поскольку сигнатуры типов совпадают, можно передать `helloJoe` в `sayHello`: + +{% tabs sayHello-usage %} +{% tab 'Scala 2 и 3' %} +```scala +sayHello(helloJoe) // печатает "Hello, Joe" +``` +{% endtab %} +{% endtabs %} + +Если вы никогда этого не делали раньше, поздравляем: +был определен метод с именем `sayHello`, который принимает функцию в качестве входного параметра, +а затем вызывает эту функцию в теле своего метода. + + +### sayHello может принимать разные функции + +Важно знать, что преимущество этого подхода заключается не в том, +что `sayHello` может принимать одну функцию в качестве входного параметра; +преимущество в том, что `sayHello` может принимать любую функцию, соответствующую сигнатуре `f`. +Например, поскольку следующая функция не принимает входных параметров и ничего не возвращает, она также работает с `sayHello`: + +{% tabs bonjourJulien-definition %} +{% tab 'Scala 2 и 3' %} +```scala +def bonjourJulien(): Unit = println("Bonjour, Julien") +``` +{% endtab %} +{% endtabs %} + +Вот что выводится в REPL: + +{% tabs bonjourJulien-usage %} +{% tab 'Scala 2 и 3' %} +```` +scala> sayHello(bonjourJulien) +Bonjour, Julien +```` +{% endtab %} +{% endtabs %} + +Это отличный старт. +Рассмотрим ещё несколько примеров того, как определять сигнатуры различных типов для параметров функции. + + +## Общий синтаксис для определения входных параметров функции + +В методе: + +{% tabs sayHello-definition-2 %} +{% tab 'Scala 2 и 3' %} +```scala +def sayHello(f: () => Unit): Unit +``` +{% endtab %} +{% endtabs %} + +сигнатурой типа для `f` является: + +```scala +() => Unit +``` + +Это сигнатура означает “функцию, которая не принимает входных параметров и не возвращает ничего значимого (`Unit`)”. + +Вот сигнатура функции, которая принимает параметр `String` и возвращает `Int`: + +```scala +f: String => Int +``` + +Какие функции принимают строку и возвращают целое число? +Например, такие, как “длина строки” и контрольная сумма. + +Эта функция принимает два параметра `Int` и возвращает `Int`: + +```scala +f: (Int, Int) => Int +``` + +Какие функции соответствуют данной сигнатуре? + +Любая функция, которая принимает два входных параметра `Int` и возвращает `Int`, +соответствует этой сигнатуре, поэтому все “функции” ниже (точнее, методы) подходят: + +{% tabs add-sub-mul-definitions %} +{% tab 'Scala 2 и 3' %} +```scala +def add(a: Int, b: Int): Int = a + b +def subtract(a: Int, b: Int): Int = a - b +def multiply(a: Int, b: Int): Int = a * b +``` +{% endtab %} +{% endtabs %} + +Из примеров выше можно сделать вывод, что общий синтаксис сигнатуры функций такой: + +```scala +variableName: (parameterTypes ...) => returnType +``` + +> Поскольку функциональное программирование похоже на создание и объединение ряда алгебраических уравнений, +> обычно _много думают_ о типах при разработке функций и приложений. +> Можно сказать, что “думают типами”. + + +## Параметр функции вместе с другими параметрами + +Чтобы HOFs стали действительно полезными, им также нужны некоторые данные для работы. +Для класса, подобного `List`, в его методе `map` уже есть данные для работы: элементы в `List`. +Но для автономного приложения, у которого нет собственных данных, +метод также должен принимать в качестве других входных параметров данные. + +Рассмотрим пример метода с именем `executeNTimes`, который имеет два входных параметра: функцию и `Int`: + +{% tabs executeNTimes-definition class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala +def executeNTimes(f: () => Unit, n: Int): Unit = + for (i <- 1 to n) f() +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +def executeNTimes(f: () => Unit, n: Int): Unit = + for i <- 1 to n do f() +``` +{% endtab %} +{% endtabs %} + +Как видно из кода, `executeNTimes` выполняет функцию `f` `n` раз. +Поскольку простой цикл `for`, подобный этому, не имеет возвращаемого значения, `executeNTimes` возвращает `Unit`. + +Чтобы протестировать `executeNTimes`, определим метод, соответствующий сигнатуре `f`: + +{% tabs helloWorld-definition %} +{% tab 'Scala 2 и 3' %} +```scala +// тип метода - `() => Unit` +def helloWorld(): Unit = println("Hello, world") +``` +{% endtab %} +{% endtabs %} + +Затем передадим этот метод в `executeNTimes` вместе с `Int`: + +{% tabs helloWorld-usage %} +{% tab 'Scala 2 и 3' %} +``` +scala> executeNTimes(helloWorld, 3) +Hello, world +Hello, world +Hello, world +``` +{% endtab %} +{% endtabs %} + + +Великолепно. +Метод `executeNTimes` трижды выполняет функцию `helloWorld`. + + +### Столько параметров, сколько необходимо + +Методы могут усложняться по мере необходимости. +Например, этот метод принимает функцию типа `(Int, Int) => Int` вместе с двумя входными параметрами: + +{% tabs executeAndPrint-definition %} +{% tab 'Scala 2 и 3' %} +```scala +def executeAndPrint(f: (Int, Int) => Int, i: Int, j: Int): Unit = + println(f(i, j)) +``` +{% endtab %} +{% endtabs %} + + +Поскольку методы `sum` и `multiply` соответствуют сигнатуре `f`, +их можно передать в `executeAndPrint` вместе с двумя значениями `Int`: + +{% tabs executeAndPrint-usage %} +{% tab 'Scala 2 и 3' %} +```scala +def sum(x: Int, y: Int) = x + y +def multiply(x: Int, y: Int) = x * y + +executeAndPrint(sum, 3, 11) // печатает 14 +executeAndPrint(multiply, 3, 9) // печатает 27 +``` +{% endtab %} +{% endtabs %} + + +## Согласованность подписи типа функции + +Самое замечательное в изучении сигнатур типов функций Scala заключается в том, +что синтаксис, используемый для определения входных параметров функции, — +это тот же синтаксис, что используется для написания литералов функций. + +Например, если необходимо написать функцию, вычисляющую сумму двух целых чисел, её можно было бы написать так: + +{% tabs f-val-definition %} +{% tab 'Scala 2 и 3' %} +```scala +val f: (Int, Int) => Int = (a, b) => a + b +``` +{% endtab %} +{% endtabs %} + +Этот код состоит из сигнатуры типа: + +```` +val f: (Int, Int) => Int = (a, b) => a + b + ----------------- +```` + +входных параметров: + +```` +val f: (Int, Int) => Int = (a, b) => a + b + ------ +```` + +и тела функции: + +```` +val f: (Int, Int) => Int = (a, b) => a + b + ----- +```` + +Согласованность Scala состоит в том, что тип функции: + +```` +val f: (Int, Int) => Int = (a, b) => a + b + ----------------- +```` + +совпадает с сигнатурой типа, используемого для определения входного параметра функции: + +```` +def executeAndPrint(f: (Int, Int) => Int, ... + ----------------- +```` + +По мере освоения этого синтаксиса, становится привычным его использование для определения параметров функций, +анонимных функций и функциональных переменных, а также становится легче читать Scaladoc для функций высшего порядка. + +[eta_expansion]: {% link _overviews/scala3-book/fun-eta-expansion.md %} diff --git a/_ru/scala3/book/fun-intro.md b/_ru/scala3/book/fun-intro.md new file mode 100644 index 0000000000..e28840f949 --- /dev/null +++ b/_ru/scala3/book/fun-intro.md @@ -0,0 +1,17 @@ +--- +layout: multipage-overview +title: Функции +scala3: true +partof: scala3-book +overview-name: "Scala 3 — Book" +type: chapter +description: В этой главе рассматриваются темы, связанные с функциями в Scala 3. +language: ru +num: 27 +previous-page: methods-summary +next-page: fun-anonymous-functions +--- + +Если в предыдущей главе были представлены Scala _методы_, то в этой главе мы углубимся в _функции_. +Рассматриваемые темы включают анонимные функции, функциональные переменные и функции высшего порядка (HOF), +в том числе способы создания собственных HOF. diff --git a/_ru/scala3/book/fun-summary.md b/_ru/scala3/book/fun-summary.md new file mode 100644 index 0000000000..497e4d4c5e --- /dev/null +++ b/_ru/scala3/book/fun-summary.md @@ -0,0 +1,41 @@ +--- +layout: multipage-overview +title: Обзор +scala3: true +partof: scala3-book +overview-name: "Scala 3 — Book" +type: section +description: На этой странице представлен обзор предыдущего раздела 'Функции'. +language: ru +num: 34 +previous-page: fun-write-method-returns-function +next-page: +--- + +Это была длинная глава, поэтому давайте рассмотрим ключевые моменты, которые мы прошли. + +Функция высшего порядка (HOF) часто определяется как функция, +которая принимает другие функции в качестве входных параметров или возвращает функцию в качестве своего значения. +В Scala это возможно, потому что функции являются объектами первого класса. + +Двигаясь по разделам, сначала вы узнали: + +- Как писать анонимные функции в виде небольших фрагментов кода. +- Как передать их десяткам HOF (методов) в классах коллекций, т.е. таким методам, как `filter`, `map` и т.д. +- Как с помощью этих небольших фрагментов кода и мощных HOF создавать множество функций с помощью небольшого кода. + +Изучив анонимные функции и HOF, вы узнали: + +- Функциональные переменные — это просто анонимные функции, привязанные к переменной. + +Увидев, как быть потребителем HOF, вы увидели, как стать создателем HOF. +В частности, вы узнали: + +- Как писать методы, принимающие функции в качестве входных параметров +- Как вернуть функцию из метода + +Полезным побочным эффектом этой главы является то, +что вы увидели много примеров того, как объявлять сигнатуры типов для функций. +Преимущество этого заключается в том, что вы используете один и тот же синтаксис +для определения параметров функций, анонимных функций и функциональных переменных, +а также становится легче читать Scaladoc для функций высшего порядка, таких как `map`, `filter` и другие. diff --git a/_ru/scala3/book/fun-write-map-function.md b/_ru/scala3/book/fun-write-map-function.md new file mode 100644 index 0000000000..020a2e61a8 --- /dev/null +++ b/_ru/scala3/book/fun-write-map-function.md @@ -0,0 +1,141 @@ +--- +layout: multipage-overview +title: Собственный map +scala3: true +partof: scala3-book +overview-name: "Scala 3 — Book" +type: section +description: На этой странице описано, как создать свой собственный метод map +language: ru +num: 32 +previous-page: fun-hofs +next-page: fun-write-method-returns-function +--- + + +Теперь, когда известно, как писать собственные функции высшего порядка, рассмотрим более реальный пример. + +Представим, что у класса `List` нет метода `map`, и есть необходимость его написать. +Первым шагом при создании функций является точное определение проблемы. +Сосредоточившись только на `List[Int]`, получаем: + +> Необходимо написать метод `map`, который можно использовать для применения функции к каждому элементу в `List[Int]`, +> возвращая преобразованные элементы в виде нового списка. + +Учитывая это утверждение, начнем писать сигнатуру метода. +Во-первых, известно, что функция должна приниматься в качестве параметра, +и эта функция должна преобразовать `Int` в какой-то общий тип `A`, поэтому получаем: + +{% tabs map-accept-func-definition %} +{% tab 'Scala 2 и 3' %} +```scala +def map(f: (Int) => A) +``` +{% endtab %} +{% endtabs %} + +Синтаксис использования универсального типа требует объявления этого символа типа перед списком параметров, +поэтому добавляем объявление типа: + +{% tabs map-type-symbol-definition %} +{% tab 'Scala 2 и 3' %} +```scala +def map[A](f: (Int) => A) +``` +{% endtab %} +{% endtabs %} + +Далее известно, что `map` также должен принимать `List[Int]`: + +{% tabs map-list-int-param-definition %} +{% tab 'Scala 2 и 3' %} +```scala +def map[A](f: (Int) => A, xs: List[Int]) +``` +{% endtab %} +{% endtabs %} + +Наконец, также известно, что `map` возвращает преобразованный список, содержащий элементы универсального типа `A`: + +{% tabs map-with-return-type-definition %} +{% tab 'Scala 2 и 3' %} +```scala +def map[A](f: (Int) => A, xs: List[Int]): List[A] = ??? +``` +{% endtab %} +{% endtabs %} + +Теперь все, что нужно сделать, это написать тело метода. +Метод `map` применяет заданную им функцию к каждому элементу в заданном списке для создания нового преобразованного списка. +Один из способов сделать это - использовать выражение `for`: + +{% tabs for-definition class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala +for (x <- xs) yield f(x) +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +for x <- xs yield f(x) +``` +{% endtab %} +{% endtabs %} + +`for` выражения зачастую делают код удивительно простым, и в данном случае - это все тело метода. + +Объединив `for` с сигнатурой метода, получим автономный метод `map`, который работает с `List[Int]`: + +{% tabs map-function class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala +def map[A](f: (Int) => A, xs: List[Int]): List[A] = + for (x <- xs) yield f(x) +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +def map[A](f: (Int) => A, xs: List[Int]): List[A] = + for x <- xs yield f(x) +``` +{% endtab %} +{% endtabs %} + + +### Обобщим метод map + +Обратим внимание, что выражение `for` не делает ничего, что зависит от типа `Int` внутри списка. +Следовательно, можно заменить `Int` в сигнатуре типа параметром универсального типа `B`: + +{% tabs map-function-full-generic class=tabs-scala-version %} +{% tab 'Scala 2' %} +```scala +def map[A, B](f: (B) => A, xs: List[B]): List[A] = + for (x <- xs) yield f(x) +``` +{% endtab %} +{% tab 'Scala 3' %} +```scala +def map[A, B](f: (B) => A, xs: List[B]): List[A] = + for x <- xs yield f(x) +``` +{% endtab %} +{% endtabs %} + +Получился метод `map`, который работает с любым списком. + +Демонстрация работы получившегося `map`: + +{% tabs map-use-example %} +{% tab 'Scala 2 и 3' %} +```scala +def double(i : Int): Int = i * 2 +map(double, List(1, 2, 3)) // List(2, 4, 6) + +def strlen(s: String): Int = s.length +map(strlen, List("a", "bb", "ccc")) // List(1, 2, 3) +``` +{% endtab %} +{% endtabs %} + +Теперь, когда рассмотрены методы, принимающие функции в качестве входных параметров, перейдем к методам, возвращающим функции. diff --git a/_ru/scala3/book/fun-write-method-returns-function.md b/_ru/scala3/book/fun-write-method-returns-function.md new file mode 100644 index 0000000000..a322162e58 --- /dev/null +++ b/_ru/scala3/book/fun-write-method-returns-function.md @@ -0,0 +1,176 @@ +--- +layout: multipage-overview +title: Создание метода, возвращающего функцию +scala3: true +partof: scala3-book +overview-name: "Scala 3 — Book" +type: section +description: На этой странице показано, как создавать методы, возвращающие функции, в Scala. +language: ru +num: 33 +previous-page: fun-write-map-function +next-page: fun-summary +--- + + +Благодаря согласованности Scala написание метода, возвращающего функцию, похоже на то, что было описано в предыдущих разделах. +Например, представьте, что вы хотите написать метод `greet`, возвращающий функцию. +Еще раз начнем с постановки проблемы: + +> Необходимо создать метод greet, возвращающий функцию. +> Эта функция должна принимать строковый параметр и печатать его с помощью `println`. +> Начнем с простого шага: `greet` не принимает никаких входных параметров, а просто создает функцию и возвращает её. + +Учитывая это утверждение, можно начать создавать `greet`. +Известно, что это будет метод: + +```scala +def greet() +``` + +Также известно, что этот метод должен возвращать функцию, которая (a) принимает параметр `String` и +(b) печатает эту строку с помощью `println`. + +Следовательно, эта функция имеет тип `String => Unit`: + +```scala +def greet(): String => Unit = ??? + ---------------- +``` + +Теперь нужно просто создать тело метода. +Известно, что метод должен возвращать функцию, и эта функция принимает `String` и печатает ее. +Эта анонимная функция соответствует следующему описанию: + +```scala +(name: String) => println(s"Hello, $name") +``` + +Теперь вы просто возвращаете эту функцию из метода: + +```scala +// метод, который возвращает функцию +def greet(): String => Unit = + (name: String) => println(s"Hello, $name") +``` + +Поскольку этот метод возвращает функцию, вы получаете функцию, вызывая `greet()`. +Это хороший шаг для проверки в REPL, потому что он проверяет тип новой функции: + +```` +scala> val greetFunction = greet() +val greetFunction: String => Unit = Lambda.... + ----------------------------- +```` + +Теперь можно вызвать `greetFunction`: + +```scala +greetFunction("Joe") // печатает "Hello, Joe" +``` + +Поздравляем, вы только что создали метод, возвращающий функцию, а затем запустили её. + + +## Доработка метода + +Метод `greet` был бы более полезным, если бы была возможность задавать приветствие. +Например, передать его в качестве параметра методу `greet` и использовать внутри `println`: + +```scala +def greet(theGreeting: String): String => Unit = + (name: String) => println(s"$theGreeting, $name") +``` + +Теперь, при вызове этого метода, процесс становится более гибким, потому что приветствие можно изменить. +Вот как это выглядит, когда создается функция из этого метода: + +```` +scala> val sayHello = greet("Hello") +val sayHello: String => Unit = Lambda..... + ------------------------ +```` + +Выходные данные подписи типа показывают, что `sayHello` — это функция, +которая принимает входной параметр `String` и возвращает `Unit` (ничего). +Так что теперь, при передаче `sayHello` строки, печатается приветствие: + +```scala +sayHello("Joe") // печатает "Hello, Joe" +``` + +Приветствие можно менять для создания новых функций: + +```scala +val sayCiao = greet("Ciao") +val sayHola = greet("Hola") + +sayCiao("Isabella") // печатает "Ciao, Isabella" +sayHola("Carlos") // печатает "Hola, Carlos" +``` + + +## Более реалистичный пример + +Этот метод может быть еще более полезным, когда возвращает одну из многих возможных функций, +например, фабрику пользовательских функций. + +Например, представим, что необходимо написать метод, который возвращает функции, приветствующие людей на разных языках. +Ограничим это функциями, которые приветствуют на английском или французском языках, +в зависимости от параметра, переданного в метод. + +Созданный метод должен: (a) принимать “желаемый язык” в качестве входных данных +и (b) возвращать функцию в качестве результата. +Кроме того, поскольку эта функция печатает заданную строку, известно, что она имеет тип `String => Unit`. +С помощью этой информации сигнатура метода должна выглядеть так: + +```scala +def createGreetingFunction(desiredLanguage: String): String => Unit = ??? +``` + +Далее, поскольку возвращаемые функции, берут строку и печатают ее, +можно прикинуть две анонимные функции для английского и французского языков: + +```scala +(name: String) => println(s"Hello, $name") +(name: String) => println(s"Bonjour, $name") +``` + +Для большей читабельности дадим этим анонимным функциям имена и назначим двум переменным: + +```scala +val englishGreeting = (name: String) => println(s"Hello, $name") +val frenchGreeting = (name: String) => println(s"Bonjour, $name") +``` + +Теперь все, что осталось, это (a) вернуть `englishGreeting`, если `desiredLanguage` — английский, +и (b) вернуть `frenchGreeting`, если `desiredLanguage` — французский. +Один из способов сделать это - выражение `match`: + +```scala +def createGreetingFunction(desiredLanguage: String): String => Unit = + val englishGreeting = (name: String) => println(s"Hello, $name") + val frenchGreeting = (name: String) => println(s"Bonjour, $name") + desiredLanguage match + case "english" => englishGreeting + case "french" => frenchGreeting +``` + +И это последний метод. +Обратите внимание, что возврат значения функции из метода ничем не отличается от возврата строкового или целочисленного значения. + +Вот как `createGreetingFunction` создает функцию приветствия на французском языке: + +```scala +val greetInFrench = createGreetingFunction("french") +greetInFrench("Jonathan") // печатает "Bonjour, Jonathan" +``` + +И вот как - на английском: + +```scala +val greetInEnglish = createGreetingFunction("english") +greetInEnglish("Joe") // печатает "Hello, Joe" +``` + +Если вам понятен этот код — поздравляю — теперь вы знаете, как писать методы, возвращающие функции. diff --git a/_ru/scala3/book/methods-intro.md b/_ru/scala3/book/methods-intro.md new file mode 100644 index 0000000000..b5192db1cb --- /dev/null +++ b/_ru/scala3/book/methods-intro.md @@ -0,0 +1,22 @@ +--- +layout: multipage-overview +title: Методы +scala3: true +partof: scala3-book +overview-name: "Scala 3 — Book" +type: chapter +description: В этой главе представлены методы в Scala 3. +language: ru +num: 23 +previous-page: domain-modeling-fp +next-page: methods-most +--- + + +В Scala 2 _методы_ могут быть определены внутри классов, трейтов, объектов, `case` классов и `case` объектов. +Но стало еще лучше: в Scala 3 они также могут быть определены вне любой из этих конструкций; +мы говорим, что это определения "верхнего уровня", поскольку они не вложены в другое определение. +Короче говоря, теперь методы можно определять где угодно. + +Многие особенности методов демонстрируются в следующем разделе. +Поскольку `main` методы требуют немного больше пояснений, они описаны в одном из следующих разделов отдельно. diff --git a/_ru/scala3/book/methods-main-methods.md b/_ru/scala3/book/methods-main-methods.md new file mode 100644 index 0000000000..3faca33023 --- /dev/null +++ b/_ru/scala3/book/methods-main-methods.md @@ -0,0 +1,178 @@ +--- +layout: multipage-overview +title: Main методы в Scala 3 +scala3: true +partof: scala3-book +overview-name: "Scala 3 — Book" +type: section +description: На этой странице описывается, как основные методы и аннотация @main работают в Scala 3. +language: ru +num: 25 +previous-page: methods-most +next-page: methods-summary +--- + +
Написание однострочных программ только в Scala 3
+ +Scala 3 предлагает следующий способ определения программ, которые можно вызывать из командной строки: +добавление аннотации `@main` к методу превращает его в точку входа исполняемой программы: + +{% tabs method_1 %} +{% tab 'Только в Scala 3' for=method_1 %} + +```scala +@main def hello() = println("Hello, World") +``` + +{% endtab %} +{% endtabs %} + +Для запуска программы достаточно сохранить эту строку кода в файле с именем, например, _Hello.scala_ +(имя файла необязательно должно совпадать с именем метода) и запустить с помощью `scala`: + +```bash +$ scala Hello.scala +Hello, World +``` + +Аннотированный метод `@main` может быть написан либо на верхнем уровне (как показано), +либо внутри статически доступного объекта. +В любом случае имя программы - это имя метода без каких-либо префиксов объектов. + +Узнайте больше об аннотации `@main`, прочитав следующие разделы или посмотрев это видео: + +
+ +
+ +### Аргументы командной строки + +Метод `@main` может обрабатывать аргументы командной строки с различными типами. +Например, данный метод `@main`, который принимает параметры `Int`, `String` и дополнительные строковые параметры: + +{% tabs method_2 %} +{% tab 'Только в Scala 3' for=method_2 %} + +```scala +@main def happyBirthday(age: Int, name: String, others: String*) = + val suffix = (age % 100) match + case 11 | 12 | 13 => "th" + case _ => (age % 10) match + case 1 => "st" + case 2 => "nd" + case 3 => "rd" + case _ => "th" + + val sb = StringBuilder(s"Happy $age$suffix birthday, $name") + for other <- others do sb.append(" and ").append(other) + println(sb.toString) +``` + +{% endtab %} +{% endtabs %} + +После компиляции кода создается основная программа с именем `happyBirthday`, которая вызывается следующим образом: + +``` +$ scala happyBirthday 23 Lisa Peter +Happy 23rd Birthday, Lisa and Peter! +``` + +Как показано, метод `@main` может иметь произвольное количество параметров. +Для каждого типа параметра должен существовать [given экземпляр][given] +класса типа `scala.util.CommandLineParser.FromString`, который преобразует аргумент из `String` в требуемый тип параметра. +Также, как показано, список параметров основного метода может заканчиваться повторяющимся параметром типа `String*`, +который принимает все оставшиеся аргументы, указанные в командной строке. + +Программа, реализованная с помощью метода `@main`, проверяет, +что в командной строке достаточно аргументов для заполнения всех параметров, +и что строки аргументов могут быть преобразованы в требуемые типы. +Если проверка завершается неудачей, программа завершается с сообщением об ошибке: + +``` +$ scala happyBirthday 22 +Illegal command line after first argument: more arguments expected + +$ scala happyBirthday sixty Fred +Illegal command line: java.lang.NumberFormatException: For input string: "sixty" +``` + +## Детали + +Компилятор Scala генерирует программу из `@main` метода `f` следующим образом: + +- он создает класс с именем `f` в пакете, где был найден метод `@main`. +- класс имеет статический метод `main` с обычной сигнатурой Java `main` метода: + принимает `Array[String]` в качестве аргумента и возвращает `Unit`. +- сгенерированный `main` метод вызывает метод `f` с аргументами, + преобразованными с помощью методов в объекте `scala.util.CommandLineParser.FromString`. + +Например, приведенный выше метод `happyBirthday` генерирует дополнительный код, эквивалентный следующему классу: + +{% tabs method_3 %} +{% tab 'Только в Scala 3' for=method_3 %} + +```scala +final class happyBirthday { + import scala.util.{CommandLineParser as CLP} + def main(args: Array[String]): Unit = + try + happyBirthday( + CLP.parseArgument[Int](args, 0), + CLP.parseArgument[String](args, 1), + CLP.parseRemainingArguments[String](args, 2)*) + catch { + case error: CLP.ParseError => CLP.showError(error) + } +} +``` + +> Примечание: В этом сгенерированном коде модификатор `` выражает, +> что `main` метод генерируется как статический метод класса `happyBirthday`. +> Эта функция недоступна для пользовательских программ в Scala. +> Вместо неё обычные “статические” члены генерируются в Scala с использованием `object`. + +{% endtab %} +{% endtabs %} + +## Обратная совместимость со Scala 2 + +`@main` методы — это рекомендуемый способ создания программ, вызываемых из командной строки в Scala 3. +Они заменяют предыдущий подход, который заключался в создании `object`, расширяющего класс `App`: + +Прежняя функциональность `App`, основанная на "волшебном" `DelayedInit trait`, больше недоступна. +`App` все еще существует в ограниченной форме, но не поддерживает аргументы командной строки и будет объявлен устаревшим в будущем. + +Если программам необходимо выполнять перекрестную сборку между Scala 2 и Scala 3, +вместо этого рекомендуется использовать `object` с явным методом `main` и одним аргументом `Array[String]`: + +{% tabs method_4 %} +{% tab 'Scala 2 и 3' %} + +```scala +object happyBirthday { + private def happyBirthday(age: Int, name: String, others: String*) = { + ... // тоже, что и раньше + } + def main(args: Array[String]): Unit = + happyBirthday(args(0).toInt, args(1), args.drop(2).toIndexedSeq:_*) +} +``` + +> обратите внимание, что здесь мы используем `:_*` для передачи переменного числа аргументов, +> который остается в Scala 3 для обратной совместимости. + +{% endtab %} +{% endtabs %} + +Если вы поместите этот код в файл с именем _happyBirthday.scala_, то сможете скомпилировать его с `scalac` +и запустить с помощью `scala`, как показывалось ранее: + +```bash +$ scalac happyBirthday.scala + +$ scala happyBirthday 23 Lisa Peter +Happy 23rd Birthday, Lisa and Peter! +``` + +[given]: {% link _overviews/scala3-book/ca-given-using-clauses.md %} diff --git a/_ru/scala3/book/methods-most.md b/_ru/scala3/book/methods-most.md new file mode 100644 index 0000000000..0554fa9464 --- /dev/null +++ b/_ru/scala3/book/methods-most.md @@ -0,0 +1,712 @@ +--- +layout: multipage-overview +title: Особенности методов +scala3: true +partof: scala3-book +overview-name: "Scala 3 — Book" +type: section +description: В этом разделе представлены методы Scala 3, включая main методы, методы расширения и многое другое. +language: ru +num: 24 +previous-page: methods-intro +next-page: methods-main-methods +--- + +В этом разделе представлены различные аспекты определения и вызова методов в Scala 3. + +## Определение методов + +В Scala методы обладают множеством особенностей, в том числе: + +- Generic (типовые) параметры +- Значения параметров по умолчанию +- Несколько групп параметров +- Контекстные параметры +- Параметры по имени +- и другие… + +Некоторые из этих функций демонстрируются в этом разделе, но когда вы определяете “простой” метод, +который не использует эти функции, синтаксис выглядит следующим образом: + +{% tabs method_1 class=tabs-scala-version %} +{% tab 'Scala 2' for=method_1 %} + +```scala +def methodName(param1: Type1, param2: Type2): ReturnType = { + // тело метода + // находится здесь +} +``` + +{% endtab %} + +{% tab 'Scala 3' for=method_1 %} + +```scala +def methodName(param1: Type1, param2: Type2): ReturnType = + // тело метода + // находится здесь +end methodName // опционально +``` + +{% endtab %} +{% endtabs %} + +В этом синтаксисе: + +- ключевое слово `def` используется для определения метода +- для наименования методов согласно стандартам Scala используется camel case convention +- у параметров метода необходимо всегда указывать тип +- возвращаемый тип метода указывать необязательно +- методы могут состоять как только из одной строки, так и из нескольких строк +- метку окончания метода `end methodName` указывать необязательно, её рекомендуется указывать только для длинных методов + +Вот два примера однострочного метода с именем `add`, который принимает два входных параметра `Int`. +Первая версия явно показывает возвращаемый тип метода - `Int`, а вторая - нет: + +{% tabs method_2 %} +{% tab 'Scala 2 и 3' for=method_2 %} + +```scala +def add(a: Int, b: Int): Int = a + b +def add(a: Int, b: Int) = a + b +``` + +{% endtab %} +{% endtabs %} + +У публичных методов рекомендуется всегда указывать тип возвращаемого значения. +Объявление возвращаемого типа может упростить его понимание при просмотре кода другого человека +или своего кода спустя некоторое время. + +## Вызов методов + +Вызов методов прост: + +{% tabs method_3 %} +{% tab 'Scala 2 и 3' for=method_3 %} + +```scala +val x = add(1, 2) // 3 +``` + +{% endtab %} +{% endtabs %} + +Коллекции Scala имеют десятки встроенных методов. +Эти примеры показывают, как их вызывать: + +{% tabs method_4 %} +{% tab 'Scala 2 и 3' for=method_4 %} + +```scala +val x = List(1, 2, 3) + +x.size // 3 +x.contains(1) // true +x.map(_ * 10) // List(10, 20, 30) +``` + +{% endtab %} +{% endtabs %} + +Внимание: + +- `size` не принимает аргументов и возвращает количество элементов в списке +- метод `contains` принимает один аргумент — значение для поиска +- `map` принимает один аргумент - функцию; в данном случае в него передается анонимная функция + +## Многострочные методы + +Если метод длиннее одной строки, начинайте тело метода со второй строки с отступом вправо: + +{% tabs method_5 class=tabs-scala-version %} +{% tab 'Scala 2' for=method_5 %} + +```scala +def addThenDouble(a: Int, b: Int): Int = { + // представим, что это тело метода требует несколько строк + val sum = a + b + sum * 2 +} +``` + +{% endtab %} + +{% tab 'Scala 3' for=method_5 %} + +```scala +def addThenDouble(a: Int, b: Int): Int = + // представим, что это тело метода требует несколько строк + val sum = a + b + sum * 2 +``` + +{% endtab %} +{% endtabs %} + +В этом методе: + +- `sum` — неизменяемая локальная переменная; к ней нельзя получить доступ вне метода +- последняя строка удваивает значение `sum` - именно это значение возвращается из метода + +Когда вы вставите этот код в REPL, то увидите, что он работает как требовалось: + +{% tabs method_6 %} +{% tab 'Scala 2 и 3' for=method_6 %} + +```scala +scala> addThenDouble(1, 1) +res0: Int = 4 +``` + +{% endtab %} +{% endtabs %} + +Обратите внимание, что нет необходимости в операторе `return` в конце метода. +Поскольку почти все в Scala является _выражением_ — то это означает, +что каждая строка кода возвращает (или _вычисляет_) значение — нет необходимости использовать `return`. + +Это видно на примере того же метода, но в более сжатой форме: + +{% tabs method_7 %} +{% tab 'Scala 2 и 3' for=method_7 %} + +```scala +def addThenDouble(a: Int, b: Int): Int = (a + b) * 2 +``` + +{% endtab %} +{% endtabs %} + +В теле метода можно использовать все возможности Scala: + +- `if`/`else` выражения +- `match` выражения +- циклы `while` +- циклы `for` и `for` выражения +- присвоение переменных +- вызовы других методов +- определения других методов + +В качестве ещё одного примера многострочного метода, +`getStackTraceAsString` преобразует свой входной параметр `Throwable` в правильно отформатированную строку: + +{% tabs method_8 class=tabs-scala-version %} +{% tab 'Scala 2' for=method_8 %} + +```scala +def getStackTraceAsString(t: Throwable): String = { + val sw = new StringWriter() + t.printStackTrace(new PrintWriter(sw)) + sw.toString +} +``` + +{% endtab %} + +{% tab 'Scala 3' for=method_8 %} + +```scala +def getStackTraceAsString(t: Throwable): String = + val sw = StringWriter() + t.printStackTrace(PrintWriter(sw)) + sw.toString +``` + +{% endtab %} +{% endtabs %} + +В этом методе: + +- в первой строке переменная `sw` принимает значение нового экземпляра `StringWriter` +- вторая строка сохраняет содержимое трассировки стека в `StringWriter` +- третья строка возвращает строковое представление трассировки стека + +## Значения параметров по умолчанию + +Параметры метода могут иметь значения по умолчанию. +В этом примере для параметров `timeout` и `protocol` заданы значения по умолчанию: + +{% tabs method_9 class=tabs-scala-version %} +{% tab 'Scala 2' for=method_9 %} + +```scala +def makeConnection(timeout: Int = 5_000, protocol: String = "http") = { + println(f"timeout = ${timeout}%d, protocol = ${protocol}%s") + // здесь ещё какой-то код ... +} +``` + +{% endtab %} + +{% tab 'Scala 3' for=method_9 %} + +```scala +def makeConnection(timeout: Int = 5_000, protocol: String = "http") = + println(f"timeout = ${timeout}%d, protocol = ${protocol}%s") + // здесь ещё какой-то код ... +``` + +{% endtab %} +{% endtabs %} + +Поскольку параметры имеют значения по умолчанию, метод можно вызвать следующими способами: + +{% tabs method_10 %} +{% tab 'Scala 2 и 3' for=method_10 %} + +```scala +makeConnection() // timeout = 5000, protocol = http +makeConnection(2_000) // timeout = 2000, protocol = http +makeConnection(3_000, "https") // timeout = 3000, protocol = https +``` + +{% endtab %} +{% endtabs %} + +Вот несколько ключевых моментов об этих примерах: + +- В первом примере аргументы не предоставляются, поэтому метод использует значения параметров по умолчанию: `5_000` и `http` +- Во втором примере для параметра `timeout` указывается значение `2_000`, + поэтому оно используется вместе со значением по умолчанию для `protocol` +- В третьем примере значения указаны для обоих параметров, поэтому используются они. + +Обратите внимание, что при использовании значений параметров по умолчанию потребителю кажется, +что он может работать с тремя разными переопределенными методами. + +## Именованные параметры + +При желании вы также можете использовать имена параметров метода при его вызове. +Например, `makeConnection` может также вызываться следующими способами: + +{% tabs method_11 %} +{% tab 'Scala 2 и 3' for=method_11 %} + +```scala +makeConnection(timeout=10_000) +makeConnection(protocol="https") +makeConnection(timeout=10_000, protocol="https") +makeConnection(protocol="https", timeout=10_000) +``` + +{% endtab %} +{% endtabs %} + +В некоторых фреймворках именованные параметры используются постоянно. +Они также очень полезны, когда несколько параметров метода имеют один и тот же тип: + +{% tabs method_12 %} +{% tab 'Scala 2 и 3' for=method_12 %} + +```scala +engage(true, true, true, false) +``` + +{% endtab %} +{% endtabs %} + +Без помощи IDE этот код может быть трудночитаемым, но так он становится намного понятнее и очевиднее: + +{% tabs method_13 %} +{% tab 'Scala 2 и 3' for=method_13 %} + +```scala +engage( + speedIsSet = true, + directionIsSet = true, + picardSaidMakeItSo = true, + turnedOffParkingBrake = false +) +``` + +{% endtab %} +{% endtabs %} + +## Рекомендации о методах, которые не принимают параметров + +Когда метод не принимает параметров, говорят, что он имеет _arity_ уровень 0 (_arity-0_). +Аналогично, если метод принимает один параметр - это метод с _arity-1_. + +Когда создаются методы _arity-0_: + +- если метод выполняет побочные эффекты, такие как вызов `println`, метод объявляется с пустыми скобками. +- если метод не выполняет побочных эффектов, например, получение размера коллекции, + что аналогично доступу к полю в коллекции, круглые скобки опускаются. + +Например, этот метод выполняет побочный эффект, поэтому он объявлен с пустыми скобками: + +{% tabs method_14 %} +{% tab 'Scala 2 и 3' for=method_14 %} + +```scala +def speak() = println("hi") +``` + +{% endtab %} +{% endtabs %} + +При вызове метода нужно обязательно указывать круглые скобки, если он был объявлен с ними: + +{% tabs method_15 %} +{% tab 'Scala 2 и 3' for=method_15 %} + +```scala +speak // ошибка: "method speak must be called with () argument" +speak() // печатает "hi" +``` + +{% endtab %} +{% endtabs %} + +Хотя это всего лишь соглашение, его соблюдение значительно улучшает читаемость кода: +с первого взгляда становится понятно, что метод с arity-0 имеет побочные эффекты. + +## Использование `if` в качестве тела метода + +Поскольку выражения `if`/`else` возвращают значение, их можно использовать в качестве тела метода. +Вот метод с именем `isTruthy`, реализующий Perl-определения `true` и `false`: + +{% tabs method_16 class=tabs-scala-version %} +{% tab 'Scala 2' for=method_16 %} + +```scala +def isTruthy(a: Any) = { + if (a == 0 || a == "" || a == false) + false + else + true +} +``` + +{% endtab %} + +{% tab 'Scala 3' for=method_16 %} + +```scala +def isTruthy(a: Any) = + if a == 0 || a == "" || a == false then + false + else + true +``` + +{% endtab %} +{% endtabs %} + +Примеры показывают, как работает метод: + +{% tabs method_17 %} +{% tab 'Scala 2 и 3' for=method_17 %} + +```scala +isTruthy(0) // false +isTruthy("") // false +isTruthy("hi") // true +isTruthy(1.0) // true +``` + +{% endtab %} +{% endtabs %} + +## Использование `match` в качестве тела метода + +Довольно часто в качестве тела метода используются `match`-выражения. +Вот еще одна версия `isTruthy`, написанная с `match` выражением: + +{% tabs method_18 class=tabs-scala-version %} +{% tab 'Scala 2' for=method_18 %} + +```scala +def isTruthy(a: Any) = a match { + case 0 | "" | false => false + case _ => true +} +``` + +{% endtab %} + +{% tab 'Scala 3' for=method_18 %} + +```scala +def isTruthy(a: Matchable) = a match + case 0 | "" | false => false + case _ => true +``` + +> Этот метод работает точно так же, как и предыдущий, в котором использовалось выражение `if`/`else`. +> Вместо `Any` в качестве типа параметра используется `Matchable`, чтобы принять любое значение, +> поддерживающее сопоставление с образцом (pattern matching). + +> См. дополнительную информацию о trait `Matchable` в [Справочной документации][reference_matchable]. + +[reference_matchable]: {{ site.scala3ref }}/other-new-features/matchable.html +{% endtab %} +{% endtabs %} + +## Контроль видимости методов в классах + +В классах, объектах, trait-ах и enum-ах методы Scala по умолчанию общедоступны, +поэтому созданный здесь экземпляр `Dog` может получить доступ к методу `speak`: + +{% tabs method_19 class=tabs-scala-version %} +{% tab 'Scala 2' for=method_19 %} + +```scala +class Dog { + def speak() = println("Woof") +} + +val d = new Dog +d.speak() // печатает "Woof" +``` + +{% endtab %} + +{% tab 'Scala 3' for=method_19 %} + +```scala +class Dog: + def speak() = println("Woof") + +val d = new Dog +d.speak() // печатает "Woof" +``` + +{% endtab %} +{% endtabs %} + +Также методы можно помечать как `private`. +Это делает их закрытыми в текущем классе, поэтому их нельзя вызвать или переопределить в подклассах: + +{% tabs method_20 class=tabs-scala-version %} +{% tab 'Scala 2' for=method_20 %} +```scala +class Animal { + private def breathe() = println("I’m breathing") +} + +class Cat extends Animal { + // этот метод не скомпилируется + override def breathe() = println("Yo, I’m totally breathing") +} +``` + +{% endtab %} + +{% tab 'Scala 3' for=method_20 %} + +```scala +class Animal: + private def breathe() = println("I’m breathing") + +class Cat extends Animal: + // этот метод не скомпилируется + override def breathe() = println("Yo, I’m totally breathing") +``` + +{% endtab %} +{% endtabs %} + +Если необходимо сделать метод закрытым в текущем классе, но разрешить подклассам вызывать или переопределять его, +метод помечается как `protected`, как показано в примере с методом `speak`: + +{% tabs method_21 class=tabs-scala-version %} +{% tab 'Scala 2' for=method_21 %} + +```scala +class Animal { + private def breathe() = println("I’m breathing") + def walk() = { + breathe() + println("I’m walking") + } + protected def speak() = println("Hello?") +} + +class Cat extends Animal { + override def speak() = println("Meow") +} + +val cat = new Cat +cat.walk() +cat.speak() +cat.breathe() // не скомпилируется, потому что private +``` + +{% endtab %} + +{% tab 'Scala 3' for=method_21 %} + +```scala +class Animal: + private def breathe() = println("I’m breathing") + def walk() = + breathe() + println("I’m walking") + protected def speak() = println("Hello?") + +class Cat extends Animal: + override def speak() = println("Meow") + +val cat = new Cat +cat.walk() +cat.speak() +cat.breathe() // не скомпилируется, потому что private +``` + +{% endtab %} +{% endtabs %} + +Настройка `protected` означает: + +- к методу (или полю) могут обращаться другие экземпляры того же класса +- метод (или поле) не виден в текущем пакете +- он доступен для подклассов + +## Методы в объектах + +Ранее было показано, что trait-ы и классы могут иметь методы. +Ключевое слово `object` используется для создания одноэлементного класса, и объект также может содержать методы. +Это хороший способ сгруппировать набор “служебных” методов. +Например, этот объект содержит набор методов, которые работают со строками: + +{% tabs method_22 class=tabs-scala-version %} +{% tab 'Scala 2' for=method_22 %} + +```scala +object StringUtils { + + /** + * Returns a string that is the same as the input string, but + * truncated to the specified length. + */ + def truncate(s: String, length: Int): String = s.take(length) + + /** + * Returns true if the string contains only letters and numbers. + */ + def lettersAndNumbersOnly_?(s: String): Boolean = + s.matches("[a-zA-Z0-9]+") + + /** + * Returns true if the given string contains any whitespace + * at all. Assumes that `s` is not null. + */ + def containsWhitespace(s: String): Boolean = + s.matches(".*\\s.*") + +} +``` + +{% endtab %} + +{% tab 'Scala 3' for=method_22 %} + +```scala +object StringUtils: + + /** + * Returns a string that is the same as the input string, but + * truncated to the specified length. + */ + def truncate(s: String, length: Int): String = s.take(length) + + /** + * Returns true if the string contains only letters and numbers. + */ + def lettersAndNumbersOnly_?(s: String): Boolean = + s.matches("[a-zA-Z0-9]+") + + /** + * Returns true if the given string contains any whitespace + * at all. Assumes that `s` is not null. + */ + def containsWhitespace(s: String): Boolean = + s.matches(".*\\s.*") + +end StringUtils +``` + +{% endtab %} +{% endtabs %} + +## Методы расширения + +Есть много ситуаций, когда необходимо добавить функциональность к закрытым классам. +Например, представьте, что у вас есть класс `Circle`, но вы не можете изменить его исходный код. +Это может быть определено в сторонней библиотеке так: + +{% tabs method_23 %} +{% tab 'Scala 2 и 3' for=method_23 %} + +```scala +case class Circle(x: Double, y: Double, radius: Double) +``` + +{% endtab %} +{% endtabs %} + +Если вы хотите добавить методы в этот класс, то можете определить их как методы расширения, например: + +{% tabs method_24 class=tabs-scala-version %} +{% tab 'Scala 2' for=method_24 %} + +```scala +implicit class CircleOps(c: Circle) { + def circumference: Double = c.radius * math.Pi * 2 + def diameter: Double = c.radius * 2 + def area: Double = math.Pi * c.radius * c.radius +} +``` +В Scala 2 используйте `implicit class`, подробности [здесь](/overviews/core/implicit-classes.html). + +{% endtab %} +{% tab 'Scala 3' for=method_24 %} + +```scala +extension (c: Circle) + def circumference: Double = c.radius * math.Pi * 2 + def diameter: Double = c.radius * 2 + def area: Double = math.Pi * c.radius * c.radius +``` +В Scala 3 используйте новую конструкцию `extension`. +Дополнительные сведения см. [в главах этой книги][extension] или [в справочнике по Scala 3][reference-ext]. + +[reference-ext]: {{ site.scala3ref }}/contextual/extension-methods.html +[extension]: {% link _overviews/scala3-book/ca-extension-methods.md %} +{% endtab %} +{% endtabs %} + +Теперь, когда у вас есть экземпляр `Circle` с именем `aCircle`, +то вы можете вызывать эти методы следующим образом: + +{% tabs method_25 %} +{% tab 'Scala 2 и 3' for=method_25 %} + +```scala +aCircle.circumference +aCircle.diameter +aCircle.area +``` + +{% endtab %} +{% endtabs %} + +## Дальнейшее изучение + +Есть много чего, что можно узнать о методах, в том числе: + +- Вызов методов в суперклассах +- Определение и использование параметров по имени +- Написание метода, который принимает параметр функции +- Создание встроенных методов +- Обработка исключений +- Использование входных параметров vararg +- Написание методов с несколькими группами параметров (частично применяемые функции). +- Создание методов с параметрами универсального типа. + +Дополнительные сведения об этих функциях см. в других главах этой книги. + +[reference_extension_methods]: {{ site.scala3ref }}/contextual/extension-methods.html +[reference_matchable]: {{ site.scala3ref }}/other-new-features/matchable.html diff --git a/_ru/scala3/book/methods-summary.md b/_ru/scala3/book/methods-summary.md new file mode 100644 index 0000000000..e19748a8fc --- /dev/null +++ b/_ru/scala3/book/methods-summary.md @@ -0,0 +1,30 @@ +--- +layout: multipage-overview +title: Обзор +scala3: true +partof: scala3-book +overview-name: "Scala 3 — Book" +type: section +description: Эта страница подводит итог предыдущим разделам о методах в Scala 3. +language: ru +num: 26 +previous-page: methods-main-methods +next-page: fun-intro +--- + + +Есть ещё много чего, что можно узнать о методах, в том числе: + +- Вызов методов в суперклассах +- Определение и использование параметров по имени +- Создание метода, который принимает функцию в качестве параметра +- Создание встроенных (_inline_) методов +- Обработка исключений +- Использование переменного числа входных параметров +- Создание методов с несколькими группами параметров (частично применяемые функции) +- Создание методов с параметрами универсального типа + +Дополнительные сведения об этих функциях доступны в [справочной документации][reference]. + + +[reference]: {{ site.scala3ref }}/overview.html diff --git a/_ru/scala3/book/scala-features.md b/_ru/scala3/book/scala-features.md index f1eab830aa..84dedb717f 100644 --- a/_ru/scala3/book/scala-features.md +++ b/_ru/scala3/book/scala-features.md @@ -91,7 +91,7 @@ val newNumbers = double(oldNumbers) подобные этому, для вычисления того же результата: {% tabs scala-features-2 %} -{% tab 'Scala 2 and 3' for=scala-features-2 %} +{% tab 'Scala 2 и 3' for=scala-features-2 %} ```scala val newNumbers = oldNumbers.map(_ * 2) ``` @@ -108,7 +108,7 @@ Scala имеет краткий, удобочитаемый синтаксис. Например, переменные создаются лаконично, а их типы понятны: {% tabs scala-features-3 %} -{% tab 'Scala 2 and 3' for=scala-features-3 %} +{% tab 'Scala 2 и 3' for=scala-features-3 %} ```scala val nums = List(1,2,3) val p = Person("Martin", "Odersky") @@ -120,7 +120,7 @@ val p = Person("Martin", "Odersky") Функции высшего порядка и лямбда-выражения делают код кратким и удобочитаемым: {% tabs scala-features-4 %} -{% tab 'Scala 2 and 3' for=scala-features-4 %} +{% tab 'Scala 2 и 3' for=scala-features-4 %} ```scala nums.map(i => i * 2) // длинная форма nums.map(_ * 2) // краткая форма @@ -318,7 +318,7 @@ Scala 3 делает этот процесс более понятным, чем Например, в Scala вы можете читать файлы с помощью `BufferedReader` и `FileReader` из Java: {% tabs scala-features-7 %} -{% tab 'Scala 2 and 3' for=scala-features-7 %} +{% tab 'Scala 2 и 3' for=scala-features-7 %} ```scala import java.io.* val br = BufferedReader(FileReader(filename)) @@ -333,7 +333,7 @@ val br = BufferedReader(FileReader(filename)) то можете преобразовать их с помощью всего нескольких строк кода: {% tabs scala-features-8 %} -{% tab 'Scala 2 and 3' for=scala-features-8 %} +{% tab 'Scala 2 и 3' for=scala-features-8 %} ```scala import scala.jdk.CollectionConverters.* val scalaList: Seq[Integer] = JavaClass.getJavaList().asScala.toSeq diff --git a/_ru/scala3/book/taste-collections.md b/_ru/scala3/book/taste-collections.md index 2585f24d20..672e5158af 100644 --- a/_ru/scala3/book/taste-collections.md +++ b/_ru/scala3/book/taste-collections.md @@ -23,7 +23,7 @@ next-page: taste-contextual-abstractions В этих примерах показаны различные способы создания заполненного `List`: {% tabs collection_1 %} -{% tab 'Scala 2 and 3' for=collection_1 %} +{% tab 'Scala 2 и 3' for=collection_1 %} ```scala val a = List(1, 2, 3) // a: List[Int] = List(1, 2, 3) @@ -48,7 +48,7 @@ val g = List.range(1, 10, 3) // g: List[Int] = List(1, 4, 7) Результат, возвращаемый каждым выражением, отображается в комментарии к каждой строке: {% tabs collection_2 %} -{% tab 'Scala 2 and 3' for=collection_2 %} +{% tab 'Scala 2 и 3' for=collection_2 %} ```scala // a sample list @@ -79,7 +79,7 @@ nums.flatMap(_.toUpperCase) // List('O', 'N', 'E', 'T', 'W', 'O') для суммирования значений в последовательности целых чисел: {% tabs collection_3 %} -{% tab 'Scala 2 and 3' for=collection_3 %} +{% tab 'Scala 2 и 3' for=collection_3 %} ```scala val firstTen = (1 to 10).toList // List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10) @@ -100,7 +100,7 @@ firstTen.foldLeft(100)(_ + _) // 155 (100 является “на Например, используя данный case класс `Person`: {% tabs collection_4 %} -{% tab 'Scala 2 and 3' for=collection_4 %} +{% tab 'Scala 2 и 3' for=collection_4 %} ```scala case class Person(name: String) @@ -112,7 +112,7 @@ case class Person(name: String) Вот как вы создаете кортеж, который содержит `Int`, `String` и пользовательское значение `Person`: {% tabs collection_5 %} -{% tab 'Scala 2 and 3' for=collection_5 %} +{% tab 'Scala 2 и 3' for=collection_5 %} ```scala val t = (11, "eleven", Person("Eleven")) @@ -125,7 +125,7 @@ val t = (11, "eleven", Person("Eleven")) или получить к ним доступ по номеру: {% tabs collection_6 %} -{% tab 'Scala 2 and 3' for=collection_6 %} +{% tab 'Scala 2 и 3' for=collection_6 %} ```scala t(0) // 11 @@ -139,7 +139,7 @@ t(2) // Person("Eleven") Вы также можете использовать этот метод _извлечения_, чтобы присвоить поля кортежа именам переменных: {% tabs collection_7 %} -{% tab 'Scala 2 and 3' for=collection_7 %} +{% tab 'Scala 2 и 3' for=collection_7 %} ```scala val (num, str, person) = t diff --git a/_ru/scala3/book/taste-contextual-abstractions.md b/_ru/scala3/book/taste-contextual-abstractions.md index 80d5e9e3a1..fea81bbb84 100644 --- a/_ru/scala3/book/taste-contextual-abstractions.md +++ b/_ru/scala3/book/taste-contextual-abstractions.md @@ -22,7 +22,7 @@ next-page: taste-toplevel-definitions название города, а затем название улицы. {% tabs contextual_1 %} -{% tab 'Scala 2 and 3' for=contextual_1 %} +{% tab 'Scala 2 и 3' for=contextual_1 %} ```scala val addresses: List[Address] = ... diff --git a/_ru/scala3/book/taste-functions.md b/_ru/scala3/book/taste-functions.md index 207e154931..f83da448e7 100644 --- a/_ru/scala3/book/taste-functions.md +++ b/_ru/scala3/book/taste-functions.md @@ -27,7 +27,7 @@ Scala обладает большинством возможностей, кот {% tabs function_1 %} -{% tab 'Scala 2 and 3' for=function_1 %} +{% tab 'Scala 2 и 3' for=function_1 %} ```scala val a = List(1, 2, 3).map(i => i * 2) // List(2,4,6) val b = List(1, 2, 3).map(_ * 2) // List(2,4,6) @@ -39,7 +39,7 @@ val b = List(1, 2, 3).map(_ * 2) // List(2,4,6) {% tabs function_2 %} -{% tab 'Scala 2 and 3' for=function_2 %} +{% tab 'Scala 2 и 3' for=function_2 %} ```scala def double(i: Int): Int = i * 2 @@ -69,7 +69,7 @@ val b = List(1, 2, 3).map(double) // List(2,4,6) {% tabs function_3 %} -{% tab 'Scala 2 and 3' for=function_3 %} +{% tab 'Scala 2 и 3' for=function_3 %} ```scala // пример списка val nums = (1 to 10).toList // List(1,2,3,4,5,6,7,8,9,10) diff --git a/_ru/scala3/book/taste-hello-world.md b/_ru/scala3/book/taste-hello-world.md index 29a59ae763..77442e43ca 100644 --- a/_ru/scala3/book/taste-hello-world.md +++ b/_ru/scala3/book/taste-hello-world.md @@ -109,7 +109,7 @@ Hello, World! Чтобы использовать этот метод, вам нужно сначала его импортировать, например: {% tabs import-readline %} -{% tab 'Scala 2 and 3' for=import-readline %} +{% tab 'Scala 2 и 3' for=import-readline %} ```scala import scala.io.StdIn.readLine ``` @@ -190,7 +190,7 @@ Hello, Alvin Alexander! если вы не используете подобное предложение `import`: {% tabs import-readline-2 %} -{% tab 'Scala 2 and 3' for=import-readline-2 %} +{% tab 'Scala 2 и 3' for=import-readline-2 %} ```scala import scala.io.StdIn.readLine ``` diff --git a/_ru/scala3/book/taste-methods.md b/_ru/scala3/book/taste-methods.md index 87169991f4..c881761826 100644 --- a/_ru/scala3/book/taste-methods.md +++ b/_ru/scala3/book/taste-methods.md @@ -19,7 +19,7 @@ next-page: taste-functions Синтаксис простого метода выглядит так: {% tabs method_1 %} -{% tab 'Scala 2 and 3' for=method_1 %} +{% tab 'Scala 2 и 3' for=method_1 %} ```scala def methodName(param1: Type1, param2: Type2): ReturnType = // тело метода @@ -31,7 +31,7 @@ def methodName(param1: Type1, param2: Type2): ReturnType = Вот несколько примеров: {% tabs method_2 %} -{% tab 'Scala 2 and 3' for=method_2 %} +{% tab 'Scala 2 и 3' for=method_2 %} ```scala def sum(a: Int, b: Int): Int = a + b def concatenate(s1: String, s2: String): String = s1 + s2 @@ -42,7 +42,7 @@ def concatenate(s1: String, s2: String): String = s1 + s2 Вам не нужно объявлять возвращаемый тип метода, поэтому можно написать эти методы следующим образом, если хотите: {% tabs method_3 %} -{% tab 'Scala 2 and 3' for=method_3 %} +{% tab 'Scala 2 и 3' for=method_3 %} ```scala def sum(a: Int, b: Int) = a + b def concatenate(s1: String, s2: String) = s1 + s2 @@ -53,7 +53,7 @@ def concatenate(s1: String, s2: String) = s1 + s2 Вот как эти методы вызываются: {% tabs method_4 %} -{% tab 'Scala 2 and 3' for=method_4 %} +{% tab 'Scala 2 и 3' for=method_4 %} ```scala val x = sum(1, 2) val y = concatenate("foo", "bar") @@ -88,7 +88,7 @@ def getStackTraceAsString(t: Throwable): String = В этом примере параметр `timeout` имеет значение по умолчанию `5000`: {% tabs method_6 %} -{% tab 'Scala 2 and 3' for=method_6 %} +{% tab 'Scala 2 и 3' for=method_6 %} ```scala def makeConnection(url: String, timeout: Int = 5000): Unit = println(s"url=$url, timeout=$timeout") @@ -99,7 +99,7 @@ def makeConnection(url: String, timeout: Int = 5000): Unit = Поскольку в объявлении метода указано значение по умолчанию для `timeout`, метод можно вызывать двумя способами: {% tabs method_7 %} -{% tab 'Scala 2 and 3' for=method_7 %} +{% tab 'Scala 2 и 3' for=method_7 %} ```scala makeConnection("https://localhost") // url=http://localhost, timeout=5000 makeConnection("https://localhost", 2500) // url=http://localhost, timeout=2500 @@ -111,7 +111,7 @@ Scala также поддерживает использование _имено поэтому вы можете вызвать этот метод, если хотите, вот так: {% tabs method_8 %} -{% tab 'Scala 2 and 3' for=method_8 %} +{% tab 'Scala 2 и 3' for=method_8 %} ```scala makeConnection( url = "https://localhost", @@ -126,7 +126,7 @@ makeConnection( какие параметры установлены в `true` или `false`: {% tabs method_9 %} -{% tab 'Scala 2 and 3' for=method_9 %} +{% tab 'Scala 2 и 3' for=method_9 %} ```scala engage(true, true, true, false) @@ -145,7 +145,7 @@ engage(true, true, true, false) но, опуская эту деталь, примеры показывают, как работают методы расширения: {% tabs extension %} -{% tab 'Scala 3 Only' %} +{% tab 'Только в Scala 3' %} ```scala extension (s: String) diff --git a/_ru/scala3/book/taste-modeling.md b/_ru/scala3/book/taste-modeling.md index b50ac31aaf..deede29e6a 100644 --- a/_ru/scala3/book/taste-modeling.md +++ b/_ru/scala3/book/taste-modeling.md @@ -388,7 +388,7 @@ enum Nat: Этот код демонстрирует несколько функций `case class`: {% tabs case-class %} -{% tab 'Scala 2 and 3' for=case-class %} +{% tab 'Scala 2 и 3' for=case-class %} ```scala // определение case class diff --git a/_ru/scala3/book/taste-objects.md b/_ru/scala3/book/taste-objects.md index 14c2d79229..a244c7cfa2 100644 --- a/_ru/scala3/book/taste-objects.md +++ b/_ru/scala3/book/taste-objects.md @@ -53,7 +53,7 @@ object StringUtils: Поскольку `StringUtils` - это "одиночка", его методы можно вызывать непосредственно для объекта: {% tabs object_2 %} -{% tab 'Scala 2 and 3' for=object_2 %} +{% tab 'Scala 2 и 3' for=object_2 %} ```scala val x = StringUtils.isNullOrEmpty("") // true val x = StringUtils.isNullOrEmpty("a") // false diff --git a/_ru/scala3/book/taste-repl.md b/_ru/scala3/book/taste-repl.md index 3d35bb9d55..e45dc0e8cc 100644 --- a/_ru/scala3/book/taste-repl.md +++ b/_ru/scala3/book/taste-repl.md @@ -45,7 +45,7 @@ REPL — это интерпретатор командной строки, по Теперь можно вводить выражения Scala, чтобы увидеть, как они работают: {% tabs expression-one %} -{% tab 'Scala 2 and 3' for=expression-one %} +{% tab 'Scala 2 и 3' for=expression-one %} ```` scala> 1 + 1 val res0: Int = 2 @@ -61,7 +61,7 @@ REPL автоматически создает для вас переменны Эти имена переменных можно использовать в последующих выражениях: {% tabs expression-two %} -{% tab 'Scala 2 and 3' for=expression-two %} +{% tab 'Scala 2 и 3' for=expression-two %} ```` scala> val x = res0 * 10 val x: Int = 20 @@ -75,7 +75,7 @@ val x: Int = 20 В этом примере показано, как создать, а затем вызвать метод `sum`: {% tabs expression-three %} -{% tab 'Scala 2 and 3' for=expression-three %} +{% tab 'Scala 2 и 3' for=expression-three %} ```` scala> def sum(a: Int, b: Int): Int = a + b def sum(a: Int, b: Int): Int diff --git a/_ru/scala3/book/taste-toplevel-definitions.md b/_ru/scala3/book/taste-toplevel-definitions.md index c0bac88150..3d15f774a8 100644 --- a/_ru/scala3/book/taste-toplevel-definitions.md +++ b/_ru/scala3/book/taste-toplevel-definitions.md @@ -17,7 +17,7 @@ next-page: taste-summary Например, вы можете создать файл с именем _MyCoolApp.scala_ и поместить в него следующее содержимое: {% tabs toplevel_1 %} -{% tab 'Scala 3 only' for=toplevel_1 %} +{% tab 'Только в Scala 3' for=toplevel_1 %} ```scala import scala.collection.mutable.ArrayBuffer @@ -57,7 +57,7 @@ type Money = BigDecimal как в этом примере: {% tabs toplevel_2 %} -{% tab 'Scala 3 only' for=toplevel_2 %} +{% tab 'Только в Scala 3' for=toplevel_2 %} ```scala package foo { def double(i: Int) = i * 2 diff --git a/_ru/scala3/book/taste-vars-data-types.md b/_ru/scala3/book/taste-vars-data-types.md index ab848966d0..7409db07b4 100644 --- a/_ru/scala3/book/taste-vars-data-types.md +++ b/_ru/scala3/book/taste-vars-data-types.md @@ -41,7 +41,7 @@ next-page: taste-control-structures Эти примеры показывают, как создавать `val` и `var` переменные: {% tabs var-express-1 %} -{% tab 'Scala 2 and 3' %} +{% tab 'Scala 2 и 3' %} ```scala // неизменяемая @@ -57,7 +57,7 @@ var b = 1 Появится ошибка компилятора, если попытаться её изменить: {% tabs var-express-2 %} -{% tab 'Scala 2 and 3' %} +{% tab 'Scala 2 и 3' %} ```scala val msg = "Hello, world" @@ -69,7 +69,7 @@ msg = "Aloha" // ошибка "reassignment to val"; этот код не ск И наоборот, `var` можно переназначить: {% tabs var-express-3 %} -{% tab 'Scala 2 and 3' %} +{% tab 'Scala 2 и 3' %} ```scala var msg = "Hello, world" @@ -83,7 +83,7 @@ msg = "Aloha" // этот код скомпилируется, потому ч Когда вы создаете переменную, то можете явно объявить ее тип или позволить компилятору его вывести: {% tabs var-express-4 %} -{% tab 'Scala 2 and 3' %} +{% tab 'Scala 2 и 3' %} ```scala val x: Int = 1 // явно @@ -96,7 +96,7 @@ val x = 1 // неявно; компилятор выводит тип Компилятор Scala обычно может определить тип данных за вас, как показано в выводе этих примеров REPL: {% tabs var-express-5 %} -{% tab 'Scala 2 and 3' %} +{% tab 'Scala 2 и 3' %} ```scala scala> val x = 1 @@ -115,7 +115,7 @@ val nums: List[Int] = List(1, 2, 3) но в простых присваиваниях, подобных нижеследующим, в этом нет необходимости: {% tabs var-express-6 %} -{% tab 'Scala 2 and 3' %} +{% tab 'Scala 2 и 3' %} ```scala val x: Int = 1 @@ -136,7 +136,7 @@ Scala поставляется со стандартными числовыми Эти примеры показывают, как объявлять переменные числовых типов: {% tabs var-express-7 %} -{% tab 'Scala 2 and 3' %} +{% tab 'Scala 2 и 3' %} ```scala val b: Byte = 1 @@ -152,7 +152,7 @@ val f: Float = 3.0 Поскольку `Int` и `Double` являются числовыми типами по умолчанию, то обычно они создаются без явного объявления типа: {% tabs var-express-8 %} -{% tab 'Scala 2 and 3' %} +{% tab 'Scala 2 и 3' %} ```scala val i = 123 // по умолчанию Int @@ -165,7 +165,7 @@ val j = 1.0 // по умолчанию Double чтобы указать, что они являются `Long`, `Double` или `Float` значениями: {% tabs var-express-9 %} -{% tab 'Scala 2 and 3' %} +{% tab 'Scala 2 и 3' %} ```scala val x = 1_000L // val x: Long = 1000 @@ -178,7 +178,7 @@ val z = 3.3F // val z: Float = 3.3 Когда вам нужны действительно большие числа, используйте типы `BigInt` и `BigDecimal`: {% tabs var-express-10 %} -{% tab 'Scala 2 and 3' %} +{% tab 'Scala 2 и 3' %} ```scala var a = BigInt(1_234_567_890_987_654_321L) @@ -192,7 +192,7 @@ var b = BigDecimal(123_456.789) В Scala также есть типы данных `String` и `Char`: {% tabs var-express-11 %} -{% tab 'Scala 2 and 3' %} +{% tab 'Scala 2 и 3' %} ```scala val name = "Bill" // String @@ -214,7 +214,7 @@ val c = 'a' // Char Например, учитывая эти три переменные: {% tabs var-express-12 %} -{% tab 'Scala 2 and 3' %} +{% tab 'Scala 2 и 3' %} ```scala val firstName = "John" @@ -227,7 +227,7 @@ val lastName = "Doe" Вы можете объединить эти переменные в строку следующим образом: {% tabs var-express-13 %} -{% tab 'Scala 2 and 3' %} +{% tab 'Scala 2 и 3' %} ```scala println(s"Name: $firstName $mi $lastName") // "Name: John C Doe" @@ -240,7 +240,7 @@ println(s"Name: $firstName $mi $lastName") // "Name: John C Doe" Чтобы вставить произвольные выражения в строку, заключите их в фигурные скобки: {% tabs var-express-14 %} -{% tab 'Scala 2 and 3' %} +{% tab 'Scala 2 и 3' %} ``` scala println(s"2 + 2 = ${2 + 2}") // prints "2 + 2 = 4" @@ -261,7 +261,7 @@ println(s"x.abs = ${x.abs}") // prints "x.abs = 1" Многострочные строки создаются путем включения строки в три двойные кавычки: {% tabs var-express-15 %} -{% tab 'Scala 2 and 3' %} +{% tab 'Scala 2 и 3' %} ```scala val quote = """The essence of Scala: diff --git a/_ru/scala3/book/why-scala-3.md b/_ru/scala3/book/why-scala-3.md index 3a8efd51f3..02e260d668 100644 --- a/_ru/scala3/book/why-scala-3.md +++ b/_ru/scala3/book/why-scala-3.md @@ -39,7 +39,7 @@ next-page: taste-intro Например, `List` определяется как класс---технически это абстрактный класс---и новый экземпляр создается следующим образом: {% tabs list %} -{% tab 'Scala 2 and 3' for=list %} +{% tab 'Scala 2 и 3' for=list %} ```scala val x = List(1, 2, 3) ``` @@ -54,7 +54,7 @@ val x = List(1, 2, 3) `List` API также состоит из десятков других методов, многие из которых являются функциями высшего порядка: {% tabs list-methods %} -{% tab 'Scala 2 and 3' for=list-methods %} +{% tab 'Scala 2 и 3' for=list-methods %} ```scala val xs = List(1, 2, 3, 4, 5) @@ -75,7 +75,7 @@ _Вывод типов_ (_type inference_) в Scala часто заставля Это верно для объявления переменной: {% tabs dynamic %} -{% tab 'Scala 2 and 3' for=dynamic %} +{% tab 'Scala 2 и 3' for=dynamic %} ```scala val a = 1 val b = "Hello, world" @@ -88,7 +88,7 @@ val stuff = ("fish", 42, 1_234.5) Это также верно при передаче анонимных функций функциям высшего порядка: {% tabs dynamic-hof %} -{% tab 'Scala 2 and 3' for=dynamic-hof %} +{% tab 'Scala 2 и 3' for=dynamic-hof %} ```scala list.filter(_ < 4) list.map(_ * 2) @@ -101,7 +101,7 @@ list.filter(_ < 4) и при определении методов: {% tabs dynamic-method %} -{% tab 'Scala 2 and 3' for=dynamic-method %} +{% tab 'Scala 2 и 3' for=dynamic-method %} ```scala def add(a: Int, b: Int) = a + b ``` @@ -111,7 +111,7 @@ def add(a: Int, b: Int) = a + b Это как никогда верно для Scala 3, например, при использовании [типов объединения][union-types]: {% tabs union %} -{% tab 'Scala 3 Only' for=union %} +{% tab 'Только в Scala 3' for=union %} ```scala // параметр типа объединения def help(id: Username | Password) = @@ -131,7 +131,7 @@ val b: Password | Username = if (true) name else password Scala — это неформальный, “краткий, но все же читабельный“ язык. Например, объявление переменной лаконично: {% tabs concise %} -{% tab 'Scala 2 and 3' for=concise %} +{% tab 'Scala 2 и 3' for=concise %} ```scala val a = 1 val b = "Hello, world" @@ -143,7 +143,7 @@ val c = List(1,2,3) Создание типов, таких как трейты, классы и перечисления, является кратким: {% tabs enum %} -{% tab 'Scala 3 Only' for=enum %} +{% tab 'Только в Scala 3' for=enum %} ```scala trait Tail: def wagTail(): Unit @@ -166,7 +166,7 @@ case class Person( Функции высшего порядка кратки: {% tabs list-hof %} -{% tab 'Scala 2 and 3' for=list-hof %} +{% tab 'Scala 2 и 3' for=list-hof %} ```scala list.filter(_ < 4) @@ -262,7 +262,7 @@ Scala также можно использовать в браузере с [п Вот некоторые примеры: {% tabs list-more %} -{% tab 'Scala 2 and 3' for=list-more %} +{% tab 'Scala 2 и 3' for=list-more %} ```scala List.range(1, 3) // List(1, 2) List.range(start = 1, end = 6, step = 2) // List(1, 3, 5) @@ -310,7 +310,7 @@ nums.sortWith(_ > _) // List(10, 8, 7, 5, 1) Для неизменяемости рекомендуется создавать неизменяемые val переменные: {% tabs val %} -{% tab 'Scala 2 and 3' for=val %} +{% tab 'Scala 2 и 3' for=val %} ```scala val a = 1 // неизменяемая переменная ``` @@ -320,7 +320,7 @@ val a = 1 // неизменяемая переменная Вам также рекомендуется использовать неизменяемые классы коллекций, такие как `List` и `Map`: {% tabs list-map %} -{% tab 'Scala 2 and 3' for=list-map %} +{% tab 'Scala 2 и 3' for=list-map %} ```scala val b = List(1,2,3) // List неизменяем val c = Map(1 -> "one") // Map неизменяема @@ -331,7 +331,7 @@ val c = Map(1 -> "one") // Map неизменяема Case классы в первую очередь предназначены для использования в [моделировании предметной области]({% link _overviews/scala3-book/domain-modeling-intro.md %}), и их параметры также неизменяемы: {% tabs case-class %} -{% tab 'Scala 2 and 3' for=case-class %} +{% tab 'Scala 2 и 3' for=case-class %} ```scala case class Person(name: String) val p = Person("Michael Scott") @@ -345,7 +345,7 @@ p.name = "Joe" // compiler error (переназначение val name) и вы можете передавать в них методы (не показаны) и анонимные функции: {% tabs higher-order %} -{% tab 'Scala 2 and 3' for=higher-order %} +{% tab 'Scala 2 и 3' for=higher-order %} ```scala a.dropWhile(_ < 25) a.filter(_ < 25) @@ -444,7 +444,7 @@ _Безопасность_ связана с несколькими новыми добавленные в Scala 3 довольно удобочитаемым образом: {% tabs extension %} -{% tab 'Scala 3 Only' for=extension %} +{% tab 'Только в Scala 3' for=extension %} ```scala // перечисления enum Color: