Научете 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)

Тук „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

Можем да добавяме елементи към Set, използвайки метода + (който взема 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

Можем да добавим запис към Map, използвайки +метода:

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.