Scala tips and code snippets.
Scala is primarily a object language, all the values are objects. Different classes of objects are defined in Scala Standard Library, based on the following class hierarchy:
Any is Scala root class. Any value is somehow an instance of Any
and can be cast into a Any.AnyRef is the root class of all reference classes,
including all classes defined in Scala programs.AnyVal is the root class of all values that cannot be set to null.Null is the subtype of all reference types. It has exactly one instance,
null, that can be assigned to any AnyRef variable.Nothing is the subtype of every types, it has no instance.
Nothing is used to define functions that
never returns and empty objects (Nil, None).Values of a given type can be transformed into an equivalent values of an other type, both implicitly or explicitly.
Scala can cast a variable from a type to an other type using several hints, like the destination variable type.
// Converts implicitly a char to a Long
val codePoint: Long = '☻'
println(s"Smiley Unicode value is $codePoint")
// --> Smiley Unicode value is 9787
The following graph shows which number types can be implicitly cast to an other number type:
An explicit cast is done by calling a cast method to produce
an equivalent value with the desired type.
By convention the cast methods starts with to...
followed by the name of the new type. Example: toLong, toChar etc…
val longValue = 12L
val intValue = longValue.toInt // convert a `Long` value to a `Int` value
All Scala values can be cast to a
StringusingtoStringmethod.
In most programming languages the primary values are numbers (boolean, integer, rational), characters and strings.
These value types are defined in Scala standard library and here is how you can use them:
BooleanBoolean values are either true or false.
They are the result of comparison operations, for example a > b.
Boolean values supports the following operations:
&& and|| or! not^ xor& strict-and: evaluates all the parameters| strict-or: evaluates all the parameters== equal!= not equal> greater than< lower than<= lower than or equal>= greater than or equal// Example operations on Boolean values
val isFrontEngineRunning = true
val isRearEngineRunning = true
val isThereAPiloteInThePlane = false
val thePlaneWillLand =
isThereAPiloteInThePlane || (isFrontEngineRunning && isRearEngineRunning)
println(s"the plane will ${ if (thePlaneWillLand) "land" else "crash" }!")
// --> the plane will land!
Byte, Char, Short, Int, Long, Float, DoubleDifferent number types are defined for different usages:
Byte, 8-bit signed integer that represents a data byte.
Example: 0xFF.toByteChar, 16-bit unsigned integer that represents a character.
Example: 'Z'Short, 16-bit signed integer, for short integer values.
Example: 123.toShortInt, 32-bit signed integer, for medium integer values.
Example: -123456Long, 64-bit signed integer, for big integer values.
Example: 9876543210LFloat, 32-bit floating point number, to approximate real values.
Example: -36.15e-4FDouble, 64-bit floating point number, to approximate real values
with better precision. Example: 9.0123Number representations are explained in depth in the literals section.
Number values supports the following operations:
+ add- subtract* multiply/ divide% modulo== equal!= not equal> greater than< lower than<= lower than or equal>= greater than or equal// Operation on numbers
val sucessRate = 100 * (4294967296L - 1048576) / 4294967296D
println(f"sucess rate = $sucessRate%.2f percent")
// --> sucess rate = 99.98 percent
Integer types Byte, Char, Short, Int & Long also support:
& bit by bit and| bit by bit or^ bit by bit xor~ reverse all bits (bitwise not)<< left shift>> right shift that preserves the value sign>>> right shift that doesn’t preserves the value sign// Bitwise operations
// here is a filtering statement
val number = 19
val lowBits = number & ((1<<4) - 1)
println(s"number filtered to only keep the 4 lowest bits = $lowBits")
// --> number filtered to only keep the 4 lowest bits = 3
StringScala String are based on Java String.
String values supports the following operations:
+ concatenates two string values* repeats a string N-times== equal!= not equal> greater than< lower than<= lower than or equal>= greater than or equalString methodsExtracting data from a String:
// Select a character from a string
val text = "Here be dragons."
val firstChar = text(0)
println(s"first char is $firstChar")
// --> first char is H
// Split a string
val lastWord = text.split(raw"[ \.]").last
println(s"last word is $lastWord")
// --> last word is dragons.
// Extract a sub-strings
val verb = text.slice(5, 7)
println(s"extracted sub-string: $verb")
// --> extracted sub-string: be
// Remove white space characters before and after a text
val vegetable = " carrot "
val shortVegetable = vegetable.trim
println(s"vegetable: $shortVegetable")
// --> vegetable: carrot
// Get the position of a sub string
val vacationSchedule = "beach and sun"
val position = vacationSchedule.indexOf("and")
val firstActivity = vacationSchedule.slice(0, position)
println(s"vacation activity: $firstActivity")
// --> vacation activity: beach
Transforming a String:
// Replace patterns in a string
val magicWord = "Abracadabra"
val nearCityName = magicWord.replaceAll(raw".a.a", "u ")
println(s"word almost changed into a city name: $nearCityName")
// --> word almost changed into a city name: Abu dabra
// Change text case to lower case
val loud = "I DEMAND SILENCE"
val quiet = loud.toLowerCase
println(quiet)
// --> i demand silence
// Change text case to upper case
val missionCode = "Alpha delta Tango"
val formalCode = missionCode.toUpperCase
println(formalCode)
// --> ALPHA DELTA TANGO
// Capitalize: first letter in upper case
val words = "tomato is RED."
val cleanPhrase = words.capitalize
println(cleanPhrase)
// --> Tomato is RED.
Checking the content of a String:
// Check if a text matches a regular expression
val phoneNumber = "+226 43 00 00 00"
val isAPhoneNumber = phoneNumber.matches(raw"[\s\d\-\+]+")
println(s"the text ${ if (isAPhoneNumber) "is" else "is not" } a phone number.")
// --> the text is a phone number.
// Check if a text contains a given string
val cookingRecipe = "Mélangez bien le bouillon."
val isInFrench = cookingRecipe.contains("é")
println(s"the text ${ if (isInFrench) "is" else "is not" } in French.")
// --> the text is in French.
// Check if a text starts with a given string
val story = "Once upon a time, a rabbit and a turtle where arguing."
val isATale = story.startsWith("Once upon a time")
println(s"the text ${ if (isATale) "is" else "is not" } a tale.")
// --> the text is a tale.
// Check if a text ends with a given string
val shortText = "WAR FINALLY OVER STOP"
val isATelegram = shortText.endsWith("STOP")
println(s"the text ${ if (isATelegram) "is" else "is not" } a telegram.")
// --> the text is a telegram.
String Interpolation and formattingValues can be added to a String template using formatting and interpolation:
formatting: inject values into a String using C-printf-like syntax.
val message = "%s invested %.2f€".format("Bob", 405.123)
println(message)
// --> Bob invested 405.12€
s-interpolation: directly inject values and expressions into a String.
val user = "Bob"
val amount = 405.123
val message = s"$user invested ${ amount.round }€"
println(message)
// --> Bob invested 405€
f-interpolation: inject formatted expressions into the String,
a mix of s-interpolation and formatting.
val user = "Bob"
val amount = 405.123
val message = f"$user invested $amount%.2f€"
println(message)
// --> Bob invested 405.12€
raw-interpolation: represent all the characters as they are written. No escape character required.
val message = raw"the new line character is '\n'"
println(message)
// --> the new line character is '\n'
Scala collections types are containers that hold values:
List, Map, Set, etc…
There are two families of collections:
By default all available collections are immutable,
to use a mutable version of a collection, it is required to import it first:
// Seq type is immutable by default
val immutableCollection = Seq(1, 2, 3)
// the mutable Seq type is imported and used here
import collection.mutable.Seq
val mutableCollection = Seq(1, 2, 3)
mutableCollection(0) = 4 // updates the element at the position 0
There are many different collections types. The concepts behind each collection type are described in Scala documentation.
Here are some convenient collection types that can be used in Scala programs:
Stringtype is equivalent to a sequential collection.
ListScala List is an immutable sequence of elements.
List elementsMerge or exclude List elements:
// Merge two lists
val merged = List(1, 2, 3) ++ List(4, 5)
println(s"${ merged.mkString(", ") }")
// --> 1, 2, 3, 4, 5
val alsoMerged = List(1, 2, 3) ::: List(4, 5)
println(s"${ alsoMerged.mkString(", ") }")
// --> 1, 2, 3, 4, 5
val united = List(1, 2, 3).union(List(4, 5))
println(s"${ united.mkString(", ") }")
// --> 1, 2, 3, 4, 5
// Compute differences, Diff = List1 - List2
val diff = List(1, 2, 3, 4, 5).diff(List(0, 1, 3, 4, 6))
println(s"differences = ${ diff.mkString(", ") }")
// --> differences = 2, 5
Add elements to a List:
// Append an element to a list
val appended = List(1, 2, 3) :+ 4
println(s"${ appended.mkString(", ") }")
// --> 1, 2, 3, 4
// Prepend an element to a list
val prepended = 0 +: List(1, 2, 3)
println(s"${ prepended.mkString(", ") }")
// --> 0, 1, 2, 3
val alsoPrepended = 0 :: List(1, 2, 3)
println(s"${ alsoPrepended.mkString(", ") }")
// --> 0, 1, 2, 3
Zip two List instances:
// Zip, merge two lists item by item
val definitions = List("eucalyptus", "orange").zip(List("plant", "fruit"))
definitions.foreach { case (item, group) => println(s"$item is a $group") }
// --> eucalyptus is a plant
// --> orange is a fruit
Flatten nested List instances:
// Flatten nested lists
val nested = List(List(1, 2), List(3, 4, 5, 6))
val flattened = nested.flatten
println(s"flat list = ${ flattened.mkString(", ") }")
// --> flat list = 1, 2, 3, 4, 5, 6
Rearrange List elements:
// Generate an `Iterator` with grouped chunks of items
val dataStream = List('a', 'b', 'c', 'd', 'e', 'f', 'g')
val chunks = dataStream.grouped(3)
val firstChunk = chunks.next
println(s"the first chunk of data is ${ firstChunk.mkString("") }")
// --> the first chunk of data is abc
// Reverse a list
val reversedCount = List(1, 2, 3, 4).reverse
println(s"reversed count: ${ reversedCount.mkString(", ") }")
// --> reversed count: 4, 3, 2, 1
// Sort a list
val sortedAges = List(35, 64, 53, 24, 89).sorted
println(s"sorted ages: ${ sortedAges.mkString(", ") }")
// --> sorted ages: 24, 35, 53, 64, 89
Group List elements:
// Group elements that have a given property in common
val roots = List(3, -2, 6, 2, -3)
val squaresInfo = roots.groupBy((x) => x * x)
squaresInfo.foreach {
case(square, roots) =>
println(s"$square root squares: ${ roots.mkString(", ") }")
}
// --> 4 root squares: -2, 2
// --> 36 root squares: 6
// --> 9 root squares: 3, -3
List elementsGet an element:
// Get element & Get element with a default value
val items = List(3, 4, 5, 6)
val third = items(2)
val missing = items.applyOrElse(10, (x: Int) => 0)
println(s"third element = $third, default value = $missing")
// --> third element = 5, default value = 0
Search an element:
// Get the position of the first element that satisfy a predicate
val orderedSquares = List(1, 4, 9, 16, 25, 36)
val threshold = orderedSquares.indexWhere(_ >= 10)
val squaresLowerThanTen = orderedSquares.slice(0, threshold)
println(s"squares lower than 10 are: ${ squaresLowerThanTen.mkString(", ") }")
// --> squares lower than 10 are: 1, 4, 9
Get first or last elements:
// Get or drop first and last elements
val positions = List("one", "two", "three")
val first = positions.head
val last = positions.last
val allButFirst = positions.tail
val allButLast = positions.init
println(s"""
positions:
- all : ${ positions.mkString(", ") }
- first : $first
- last : $last
- all but first : ${ allButFirst.mkString(", ") }
- all but last : ${ allButLast.mkString(", ") }
"""
)
/* -->
positions:
- all : one, two, three
- first : one
- last : three
- all but first : two, three
- all but last : one, two
*/
Get minimum and maximum elements:
// Min and Max
val rainyYears = List(1120, 2010, 310, 1010, 1950)
val lastRainyYear = rainyYears.max
val firstRainyYear = rainyYears.min
val lastRainyYear2ndMillenium = rainyYears.maxBy(
(x) => if (x >= 1000 && x < 2000) x else -9999
)
val firstRainyYear2ndMillenium = rainyYears.minBy(
(x) => if (x >= 1000 && x < 2000) x else 9999
)
println(s"""
rainy years:
- all rainy years : ${ rainyYears.mkString(", ") }
- last rainy year : $lastRainyYear
- first rainy year : $firstRainyYear
- 2nd millenium last rainy year : $lastRainyYear2ndMillenium
- 2nd millenium first rainy year : $firstRainyYear2ndMillenium
"""
)
/* -->
rainy years:
- all rainy years : 1120, 2010, 310, 1010, 1950
- last rainy year : 2010
- first rainy year : 310
- 2nd millenium last rainy year : 1950
- 2nd millenium first rainy year : 1010
*/
Check a List size:
// Check if a list is empty
val emptyList = List()
val isEmptyList = emptyList.isEmpty // `List.nonEmpty` also exists
println(s"there are ${ if (isEmptyList) "no " }elements in the list")
// --> there are no elements in the list
// Get list size
val manyItems = List(2, 4, 1, 2, 5, 3, 2, 0)
val numberOfItems = manyItems.length
println(s"here we have $numberOfItems items")
// --> here we have 8 items
List itemsLook for a values:
// Check if an element is in the list
val values = List(7, 8)
val found = values.contains(8)
val notFound = values.contains(0)
println(s"found(8) = $found, found(0) = $notFound")
// --> found(8) = true, found(0) = false
Count matching elements:
// Count elements which pass a test function
val elements = List(9, 8, 7, 6, 5, 4)
val nbElements = elements.count(_ % 2 == 0)
println(s"nb even = $nbElements")
// --> nb even = 3
Drop unwanted elements:
// Create a list of unique elements
val duplicates = List(1, 2, 3, 3, 3, 4, 4, 5)
val unique = duplicates.distinct
println(s"unique elements = ${ unique.mkString(", ") }")
// --> unique elements = 1, 2, 3, 4, 5
// Drop elements which pass a test function
val allNumbers = List(1, 2, 3, 4, 5, 6)
val oddNumbers = allNumbers.filter(_ % 2 != 0)
println(s"odd numbers = ${ oddNumbers.mkString(", ") }")
// --> odd numbers = 1, 3, 5
Split a List according to a condition:
// Split/partition a list into two, according to a predicate
val winterTemperatures = List(2, -3, -1, -16, 3)
val (negativeTemperatures, positiveTemperatures) =
winterTemperatures.partition(_ > 0)
println(s"""
Recorded winter temperatures:
- all temperatures : ${ winterTemperatures.mkString(", ") }
- positive temperatures : ${ negativeTemperatures.mkString(", ") }
- negative temperatures : ${ positiveTemperatures.mkString(", ") }
"""
)
/* -->
Recorded winter temperatures:
- all temperatures : 2, -3, -1, -16, 3
- positive temperatures : 2, 3
- negative temperatures : -3, -1, -16
*/
Check all elements:
// Check that all elements pass a test function
val lessThanTen = List(2, 3, 4, 5, 6, 7)
val allLessThanTen = lessThanTen.forall(_ < 10)
println(s"all items are < 10 ? $allLessThanTen")
// --> all items are < 10 ? true
List itemsIterate over a List:
// Apply an function to each item, the result is discarded.
val values = List(6, 7)
values.foreach((x) => println(s"current value is $x"))
// --> current value is 6
// --> current value is 7
Transform each List item:
// Apply a function to all elements, (`reverseMap` = `map` in reverse order)
val familyMembersAge = List(40, 15, 44)
val ageCaregories = familyMembersAge.map(
(x) => if (x >= 18) "adult" else "minor"
)
println(s"family members age categories: ${ ageCaregories.mkString(", ") }")
// --> family members age categories: adult, minor, adult
Compute a value using List items:
reducemethod is in most cases the same asreduceLeft.foldmethod is in most cases the same asfoldLeft.
// Fold or reduce left
// --> 123
val leftFolded = (0 /: List(1, 2, 3))(_*10 + _)
println(s"$leftFolded")
// --> 123
val alsoLeftFolded = List(1, 2, 3).foldLeft(0)(_*10 + _)
println(s"$alsoLeftFolded")
// --> 123
val leftReduced = List(1, 2, 3).reduceLeft(_*10 + _)
println(s"$leftReduced")
// --> 123
// Fold or reduce right
val rightFolded = (List(1, 2, 3) :\ 0)(_ + _*10)
println(s"$rightFolded")
// --> 321
val alsoRightFolded = List(1, 2, 3).foldRight(0)(_ + _*10)
println(s"$alsoRightFolded")
// --> 321
val rightReduced = List(1, 2, 3).reduceRight(_ + _*10)
println(s"$rightReduced")
// --> 321
Compute sum and product:
// Numeric list items sum and product
val teamHealthPoints = List(99, 10, 63)
val totalHealthPoints = teamHealthPoints.sum
val combinedHealthPoints = teamHealthPoints.product
println(s"""
Multiplayer game health:
- team health : ${ teamHealthPoints.mkString("", "/100, ", "/100") }
- total health : $totalHealthPoints/300
- combined health : $combinedHealthPoints/1000000
"""
)
/* -->
Multiplayer game health:
- team health : 99/100, 10/100, 63/100
- total health : 172/300
- combined health : 62370/1000000
*/
MapScala Map are collections that hold pairs of key-value.
Map elementsCombine a Map with a pair or with an other Map:
The right arrow
->operator creates a tuple:(a -> b) == (a, b)
// Build a new map from a map and a pair
val merged = Map('a' -> 1, 'b' -> 2) + ('c' -> 3)
println(s"${ merged.mkString(", ") }")
// --> a -> 1, b -> 2, c -> 3
// Combine two maps into one
val mergedAgain = Map('a' -> 1) ++ Map('b' -> 2, 'c' -> 3)
println(s"${ mergedAgain .mkString(", ") }")
// --> a -> 1, b -> 2, c -> 3
Map elementsRetrieve a value from a Map:
// Get value
val scores = Map("Fatmir" -> 16, "Bora" -> 15)
val fatmirScore = scores("Fatmir")
val boraScore = scores.get("Bora")
val unknownScore = scores.getOrElse("unknown", 0)
println(s"""
Players scores:
- Fatmir : $fatmirScore
- Bora : $boraScore
- unknown : $unknownScore
"""
)
/*
Players scores:
- Fatmir : 16
- Bora : Some(15)
- unknown : 0
*/
Count Map items with a predicate or check its size:
// Count and check size
val romanNumbers = Map("I" -> 1, "II" -> 2, "III" -> 3, "IV" -> 4, "V" -> 5)
val size = romanNumbers.size
val nbEvenThatContainV = romanNumbers.count {
case (key, value) => key.contains("V") && value % 2 == 0
}
println(s"""
Roman numbers: ${ romanNumbers.mkString(", ") }
- nb items : $size
- nb even numbers that contain V : $nbEvenThatContainV
"""
)
/*
Roman numbers: II -> 2, IV -> 4, I -> 1, V -> 5, III -> 3
- nb items : 5
- nb even numbers that contain V : 1
*/
Access Map keys and values:
// Get map keys and values
val softwareVersion = Map("document-writer" -> 3.4, "video-player" -> 10)
val softwares = softwareVersion.keys
val versions = softwareVersion.values
println(s"""
Softwares:
- names: ${ softwares.mkString(", ") }
- versions: ${ versions.mkString(", ") }
"""
)
/*
Softwares:
- names: document-writer, video-player
- versions: 3.4, 10
*/
Map elementsRemove elements from a Map using their keys:
// Remove keys
val postalCodes = Map(
"Moon Town" -> 1243,
"Wet Lake" -> 9809,
"Zero City" -> 0
)
val everythingButZero = postalCodes - "Zero City"
val onlyMoon = postalCodes -- List("Wet Lake", "Zero City")
println(s"""
Postal codes:
- all : ${ postalCodes.mkString(", ") }
- all except Zero City: ${ everythingButZero.mkString(", ") }
- only Moon Town : ${ onlyMoon.mkString(", ") }
"""
)
/*
Postal codes:
- all : Moon Town -> 1243, Wet Lake -> 9809, Zero City -> 0
- all except Zero City: Moon Town -> 1243, Wet Lake -> 9809
- only Moon Town : Moon Town -> 1243
*/
Map itemsCheck if a key is in the Map:
// Check the presence of a key
val sales = Map("shoes" -> 3214, "shirts" -> 756)
val isSellingSalad = sales.contains("salad")
println(s"the trader is selling salads: $isSellingSalad")
// --> the trader is selling salads: false
Filter a Map:
// Filter map items
val artistInfo = Map(
"genre" -> "pop",
"artist-name" -> "The PopStar",
"last-album" -> "Best Hit"
)
val artist = artistInfo.filter { case (key, _) => key.endsWith("name") }
val genre = artistInfo.filterKeys(_ == "genre")
val albumName = artistInfo.find {
case (key, _) => key.endsWith("album")
} match {
case Some((_, value)) => value
case None => "unknown"
}
println(s"""
Artist info:
- ${ artist.mkString }
- ${ genre.mkString }
- album => $albumName
"""
)
/*
Artist info:
- artist-name -> The PopStar
- genre -> pop
- album => Best Hit
*/
Map itemsTransform each pair contained in the Map:
// Process pairs
val inhabitantsInMillion = Map(
"Old City" -> 1.5,
"New City" -> 0.9
)
val inhabitants = inhabitantsInMillion.map {
case (key, value) => (key -> (value * 1e6).toInt) }
println(s"number of citizens: ${ inhabitants.mkString(", ") }")
// --> number of citizens: Old City -> 1500000, New City -> 900000
Flat map Map items:
// Flat map items
val sportFans = Map(
"football" -> Map("Botswana" -> 23, "Brunei" -> 16),
"basket-ball" -> Map("Chile" -> 41, "Latvia" -> 99)
)
val allCountriesHaveFans = sportFans.forall {
case (sport, countryFans) => countryFans.nonEmpty
}
val allFansExceptLatvians = sportFans.flatMap {
case (_, value) => value - "Latvia"
}
println(s"""
Sport fans:
- fans except Latvia : ${ allFansExceptLatvians.mkString(", ") }
- all countries have fans : $allCountriesHaveFans
"""
)
/*
Sport fans:
- fans except Latvia : Botswana -> 23, Brunei -> 16, Chile -> 41
- all countries have fans : true
*/
Group Map items:
// Group items using a chunk size
val robotMoves = Map(
"move-up" -> 0,
"move-down" -> 1,
"move-left" -> 2,
"move-right" -> 3,
"move-forward" -> 4,
"move-backward" -> 5
)
val groupedByTwo = robotMoves.grouped(2)
groupedByTwo.foreach {
moves => println(s"two moves: ${ moves.keys.mkString(" & ") }")
}
// --> two moves: move-forward & move-backward
// --> two moves: move-up & move-right
// --> two moves: move-down & move-left
// Group items by a criteria
val eachDimension = robotMoves.groupBy { case (_, v) => v % 2}
println(s"""
One steps in each dimension:
- ${ eachDimension(0).keys.mkString(" then ") }
- ${ eachDimension(1).keys.mkString(" then ") }
""")
/*
One steps in each dimension:
- move-forward then move-up then move-left
- move-backward then move-right then move-down
*/
Fold a Map to reduce all items into one item:
// Fold left, same as `foldLeft` method and `fold` (in most cases)
val cssMargins = Map("top" -> 10, "bottom" -> 5, "left" -> 25, "right" -> 15)
val topBottom = (0 /: cssMargins) {
case (previousResult, (key, value)) if (key == "top" || key == "bottom") =>
previousResult + value
case (previousResult, _) => previousResult
}
// Fold right, same as `foldRight` method
val rightLeft = (cssMargins :\ 0) {
case ((key, value), previousResult) if (key == "right" || key == "left") =>
previousResult + value
case (_, previousResult) => previousResult
}
println(s"""
CSS margins: ${ cssMargins.mkString(", ") }
- Sum top & bottom : $topBottom
- Sum left & right : $rightLeft
"""
)
/*
CSS margins: top -> 10, bottom -> 5, left -> 25, right -> 15
- Sum top & bottom : 15
- Sum left & right : 40
*/
Reduce a Map to a resulting value:
reducemethod is in most cases the same asreduceLeft.reduceRightis likereduceLeftbut in opposite order.
// Reduce a map
val elements = Map(
"milk" -> "liquid",
"rock" -> "solid",
"water" -> "liquid",
"tree" -> "solid"
)
val solidElements = elements.reduce {
(a, b) => {
val (key_a, value_a) = a
val (key_b, value_b) = b
val elementNames = List[String]() ++ {
if (value_a == "solid") List[String](key_a) else Nil
} ++ {
if (value_b == "solid") List[String](key_b) else Nil
}
(elementNames.mkString(" & "), "solid")
}
}
println(s"Solid elements: ${ solidElements._1 }")
// --> Solid elements: rock & tree
This project is maintained by
Y. Somda (yoeo)
Logo by
Icon Island
— theme by
orderedlist
— remixed by
yoeo