Вивчіть Scala від 0–60: Основи

Scala - це мова програмування високого рівня загального призначення, яка пропонує баланс між розробкою функціональних та об’єктно-орієнтованих програм.

Що таке функціональне програмування? Простіше кажучи, функції - це першокласні громадяни у функціональному програмуванні. Для того, щоб розширити основний набір функціональних можливостей програми, ми схильні писати додаткові класи, що поширюються на певні керівні принципи / інтерфейси. У функціональному програмуванні функції допомагають нам досягти того самого.

Для всіх пояснень ми використовуватимемо Scala REPL. Це дуже зручний та інформативний інструмент для вивчення Scala. У ньому реєструються милі маленькі повідомлення про те, як інтерпретується та виконується наш код.

Почнемо спочатку з основ.

1. Змінні

Ми можемо визначити незмінні змінні, використовуючи val:

scala> val name = "King"name: String = King

Змінні змінні можна визначити та змінити, використовуючи var:

scala> var name = "King"name: String = King
scala> name = "Arthur"name: String = Arthur

Ми використовуємо defдля присвоєння мітки незмінному значенню, оцінка якого відкладається на пізніший час. Це означає, що вартість етикетки ліниво оцінюється кожного разу при використанні.

scala> var name = "King"name: String = King
scala> def alias = namealias: String
scala> aliasres2: String = King

Ви спостерігали щось цікаве?

При визначенні aliasне було присвоєно значення, alias: Stringоскільки воно ледаче пов’язане, коли ми його викликаємо. Що станеться, якщо ми змінимо значення name?

scala> aliasres5: String = King
scala> name = "Arthur, King Arthur"name: String = Arthur, King Arthur
scala> aliasres6: String = Arthur, King Arthur

2. Контроль потоку

Ми використовуємо оператори потоку управління, щоб виразити свою логіку прийняття рішення.

Ви можете написати if-elseзаяву, як показано нижче:

if(name.contains("Arthur")) { print("Entombed sword")} else { print("You're not entitled to this sword")}

Або ви можете використовувати while:

var attempts = 0while (attempts < 3) { drawSword() attempts += 1}

3. Колекції

Scala явно розрізняє незмінні та незмінні колекції - безпосередньо від простору імен пакунків ( scala.collection.immutableабо scala.collection.mutable).

На відміну від незмінних колекцій, мінливі колекції можуть бути оновлені або розширені на місці. Це дозволяє нам змінювати, додавати або видаляти елементи як побічний ефект.

Але виконання операцій додавання, видалення або оновлення незмінних колекцій натомість повертає нову колекцію.

Незмінні колекції завжди автоматично імпортуються через scala._ (що також містить псевдонім для scala.collection.immutable.List).

Однак, щоб використовувати мінливі колекції, потрібно явно імпортувати scala.collection.mutable.List.

У дусі функціонального програмування ми в першу чергу базуватимемо наші приклади на незмінних аспектах мови, з незначними об’їздами у змінні сторони.

Список

Ми можемо створити список різними способами:

scala> val names = List("Arthur", "Uther", "Mordred", "Vortigern")
names: List[String] = List(Arthur, Uther, Mordred, Vortigern)

Ще одним зручним підходом є визначення списку за допомогою ::оператора мінусів . Це поєднує елемент head з рештою хвоста списку.

scala> val name = "Arthur" :: "Uther" :: "Mordred" :: "Vortigern" :: Nil
name: List[String] = List(Arthur, Uther, Mordred, Vortigern)

Що еквівалентно:

scala> val name = "Arthur" :: ("Uther" :: ("Mordred" :: ("Vortigern" :: Nil)))
name: List[String] = List(Arthur, Uther, Mordred, Vortigern)

Ми можемо отримати доступ до елементів списку безпосередньо за їх індексом. Пам’ятайте, що Scala використовує нульове індексування:

scala> name(2)
res7: String = Mordred

Деякі загальнодоступні методи включають:

list.head, який повертає перший елемент:

scala> name.head
res8: String = Arthur

list.tail, який повертає хвіст списку (що включає все, крім голови):

scala> name.tail
res9: List[String] = List(Uther, Mordred, Vortigern)

Встановити

Setдозволяє нам створити не повторювану групу сутностей. Listне усуває дублікати за замовчуванням.

scala> val nameswithDuplicates = List("Arthur", "Uther", "Mordred", "Vortigern", "Arthur", "Uther")
nameswithDuplicates: List[String] = List(Arthur, Uther, Mordred, Vortigern, Arthur, Uther)

Тут "Артур" повторюється двічі, і "Утер" теж.

Давайте створимо Набір з однаковими іменами. Зверніть увагу, як він виключає дублікати.

scala> val uniqueNames = Set("Arthur", "Uther", "Mordred", "Vortigern", "Arthur", "Uther")
uniqueNames: scala.collection.immutable.Set[String] = Set(Arthur, Uther, Mordred, Vortigern)

Ми можемо перевірити наявність конкретного елемента в Set, використовуючи contains():

scala> uniqueNames.contains("Vortigern")res0: Boolean = true

Ми можемо додавати елементи до набору за допомогою методу + (який приймає varargsаргументи змінної довжини)

scala> uniqueNames + ("Igraine", "Elsa", "Guenevere")res0: scala.collection.immutable.Set[String] = Set(Arthur, Elsa, Vortigern, Guenevere, Mordred, Igraine, Uther)

Подібним чином ми можемо видаляти елементи, використовуючи -метод

scala> uniqueNames - "Elsa"
res1: scala.collection.immutable.Set[String] = Set(Arthur, Uther, Mordred, Vortigern)

Карта

Mapце ітераційна колекція, яка містить зіставлення від keyелементів до відповідних valueелементів, які можна створити як:

scala> val kingSpouses = Map( | "King Uther" -> "Igraine", | "Vortigern" -> "Elsa", | "King Arthur" -> "Guenevere" | )
kingSpouses: scala.collection.immutable.Map[String,String] = Map(King Uther -> Igraine, Vortigern -> Elsa, King Arthur -> Guenevere)

Значення для конкретного ключа на карті можна отримати як:

scala> kingSpouses("Vortigern")res0: String = Elsa

Ми можемо додати запис до Карти, використовуючи +метод:

scala> kingSpouses + ("Launcelot" -> "Elaine")res0: scala.collection.immutable.Map[String,String] = Map(King Uther -> Igraine, Vortigern -> Elsa, King Arthur -> Guenevere, Launcelot -> Elaine)

Щоб змінити існуюче зіставлення, ми просто повторно додаємо оновлений ключ-значення:

scala> kingSpouses + ("Launcelot" -> "Guenevere")res1: scala.collection.immutable.Map[String,String] = Map(King Uther -> Igraine, Vortigern -> Elsa, King Arthur -> Guenevere, Launcelot -> Guenevere)

Зауважте, що оскільки колекція незмінна, кожна операція редагування повертає нову колекцію ( res0, res1) із застосованими змінами. Оригінальна колекція kingSpousesзалишається незмінною.

4. Функціональні комбінатори

Тепер, коли ми дізналися, як групувати набір сутностей, давайте подивимося, як ми можемо використовувати функціональні комбінатори для створення значущих перетворень у таких колекціях.

Простими словами Джона Хьюза:

Комбінатор - це функція, яка створює фрагменти програми з фрагментів програми.

An in-depth look at how combinators work is outside of this article’s scope. But, we’ll try to touch upon a high-level understanding of the concept anyhow.

Let’s take an example.

Suppose we want to find names of all queens using the kingSpouses collection map that we created.

We’d want to do something along the lines of examining each entry in the map. If the key has the name of a king, then we’re interested in the name of it’s spouse (i.e. queen).

We shall use the filter combinator on map, which has a signature like:

collection.filter( /* a filter condition method which returns true on matching map entries */)

Overall we shall perform the following steps to find queens:

  • Find the (key, value) pairs with kings’ names as keys.
  • Extract the values (names of queen) only for such tuples.

The filter is a function which, when given a (key, value), returns true / false.

  1. Find the map entries pertaining to kings.

Let’s define our filtering predicate function. Since key_value is a tuple of (key, value), we extract the key using ._1 (and guess what ._2 returns?)

scala> def isKingly(key_value: (String, String)): Boolean = key_value._1.toLowerCase.contains("king")
isKingly: (key_value: (String, String))Boolean

Now we shall use the filter function defined above to filter kingly entries.

scala> val kingsAndQueens = kingSpouses.filter(isKingly)
kingsAndQueens: scala.collection.immutable.Map[String,String] = Map(King Uther -> Igraine, King Arthur -> Guenevere)

2. Extract the names of respective queens from the filtered tuples.

scala> kingsAndQueens.values
res10: Iterable[String] = MapLike.DefaultValuesIterable(Igraine, Guenevere)

Let’s print out the names of queens using the foreach combinator:

scala> kingsAndQueens.values.foreach(println)IgraineGuenevere

Some other useful combinators are foreach, filter, zip, partition, find.

We shall re-visit some of these after having learnt how to define functions and passing functions as arguments to other functions in higher-order functions.

Let’s recap on what we’ve learned:

  • Different ways of defining variables
  • Various control-flow statements
  • Some basics about various collections
  • Overview of using functional combinators on collections

I hope you found this article useful. It is first in a series of articles to follow on learning Scala.

In part two, we’ll learn about defining classes, traits, encapsulation and other object-oriented concepts.

Please feel free to let me know your feedback and suggestions on how I can improve the content. Until then, ❤ coding.