Getting Started with Scala pt 7 - Ranges, Tuples, and Comprehensions
Last time around we went over the powerhouse of the collections library, the List. Today, we're going to take a look at a couple more collections that pop up fairly frequently, or that are at least representative of the types of collections that come up often. We have the Range, which is a generated collection that we can use all our awesome collection-y tools on (map, filter, folds, etc), as well as the Tuple, which lets us store data of differing types in the same data structure of a fixed length. Let's take a gander!
Man, you know â typing out all those lists was really tedious! My fingers hurt and the paint on my L key is starting to wear off. Wouldnât it be nice if there were a way I could tell Scala that I just wanted all the numbers from 1 until 10 and just have it type those out for me?
scala> 1 until 10 res0: scala.collection.immutable.Range = Range(1, 2, 3, 4, 5, 6, 7, 8, 9)
Crikey I suppose there is. Those guys who wrote Scala are on top of things. It looks like thereâs a type called a Range that looks a lot like a list. Thinking about this for a moment, I bet itâs because like our lists of numbers or characters, thereâs an ordering of these that makes sense. Iâd venture that we can probably use some of the same functions we used on our ordered lists on these Range thingies as well:
scala> (1 to 10).max res2: Int = 10 scala> ('a' until 'm').min res3: Char = a scala> (5 to 20 by 5).sum res4: Int = 50
Wow, thatâs nuts! It looks like to is a handy function thatâs like until, except that it makes the Range inclusive â just like saying 1 to 5 inclusive[1] â and that by will let us define a step amount, like every other with by 2 or every fifth with by 5.
Now youâre gaining some comprehension
Literally. Scalaâs for comprehension is a neat piece of syntactic sugar that does the magic that we expect to be done of Monads. However, we donât know those yet, so for now, weâll concern ourselves with the simpler end of what for gives us in regards to Lists and List-like things. Maybe we want 5 even numbers. We already have a few ways of doing that:
scala> 2 to 10 by 2 res0: scala.collection.immutable.Range = Range(2, 4, 6, 8, 10) scala> (2 to 5000 by 2).take(5) res1: scala.collection.immutable.Range = Range(2, 4, 6, 8, 10)
A for comprehension would let us write this slightly differently, as âdouble of the first 5 numbersâ:
scala> for (x <- 1 to 5) yield x * 2 res2: scala.collection.immutable.IndexedSeq[Int] = Vector(2, 4, 6, 8, 10)
I really like the look of this statement and how it reads: âFor each of the things from 1 to 5, letâs call that thing âxâ, and I would like this statement to yield the results of those âxâs multiplied by 2â. Of course because Scalaâs way of handling list-y things is infinitely wonky, we get neither a Range nor a List back, but a Vector which meets the requirement that we yield an IndexedSeq, but thatâs unimportant, trust me those are most List-like things for our purposes.
So in a for comprehension, the thing inside the parenthesis is a called an enumerator and involves a generator pointing at a binding. Then, the yield is evaluated for each binding that is generated. So, the 1 to 5 generator generates us a 1 which is bound to x, and then the yield evaluates x * 2, where x is bound to 1, and gets a 2, this becomes the first element in our IndexedSeq, and the generator moves on to the next element and so on. If youâre already familiar with it, this functionality is called mapping, and weâll be using map all over the place in no time.
However, for does not stop there, oh no! Maybe you want the double of numbers, but you only want the double of numbers that arenât between â5 and 5. for is like: âyo buddy itâs cool, I got that!â:
scala> for (x <- -10 to 10 if (x > 5 || x < -5)) yield x * 2 res3: scala.collection.immutable.IndexedSeq[Int] = Vector(-20, -18, -16, -14, -12, 12, 14, 16, 18, 20)
Yup, we can add filtering conditions to our generator to make sure we only yield for elements we need!
It can get really crazy, because we can actually have multiple generators, and a yield block will be attempted for every combination of bindings:
scala> for (x <- 1 to 3; y <- 5 to 15 by 5) yield y + x res12: scala.collection.immutable.IndexedSeq[Int] = Vector(6, 11, 16, 7, 12, 17, 8, 13, 18)
As you can see, first we get 1 + 6, 1 + 10, 1 + 15, then 2 + 5, 2 + 10, and so on. Naturally, we can still combine this with filtering to say, select only the even ones:
scala> for (x <- 1 to 3; y <- 5 to 15 by 5; if ((x + y) % 2 == 0 ) ) yield y + x res14: scala.collection.immutable.IndexedSeq[Int] = Vector(6, 16, 12, 8, 18)
So we know how to store homogenous collections of things and get to them (with functions, even!), but weâre missing out on the flip side of that still - at some point weâll probably want to have some kind of collected-together information that is of differing types without Scala inferring some inane common type for them like AnyVal or Any or None. This is where tuples come in. We already know that Lists and other collections have a (relatively) infinite number of objects of the same type. Tuples have a fixed number of objects of potentially differing (heterogenous) types. Making a tuple is super easy, you just put parenthesis around whatever you want to tuplify[2]:
scala> (1, "two") res0: (Int, String) = (1,two)
As a reminder, what happens when we do that with a List?:
scala> List(1, "two") res1: List[Any] = List(1, two)
Boo and hiss, hiss and boo! We certainly canât do much with an Any. How nice that the tuple assures us in itâs type signature of (Int, String) that it contains exactly one Int and one String. Thanks tuple!
Tuples can contain more than two things if we need to, and pedantic math-y people will then tell you that they are called âtriplesâ and âquadruplesâ or even worse, ân-tuplesâ (like 7-tuple), but weâre just going to go ahead and call them all tuples because weâre rebels like that:
scala> ('a', 4, "woohoo!", 'b) res2: (Char, Int, String, Symbol) = (a,4,woohoo!,'b) scala> (List('a', 'b', 'c'), "easy as", List(1, 2, 3), "simple as", List("do", "re", "mi"), 'abc, 123) res3: (List[Char], String, List[Int], String, List[String], Symbol, Int) = (List(a, b, c),easy as,List(1, 2, 3),simple as,List(do, re, mi),'abc,123)
See, even Michael likes his heterogenous collection types.
When itâs time to get the information out of tuple, we use the accessors for its elements, which are just vals preceded by an underscore:
scala> res0._1 res4: Int = 1 scala> res0._2 res5: String = two
Unlike collection indices, these start their numbering at 1, because thatâs just how they decided to do it. You can store up to 22 elements in a tuple (via the Tuple22) type. Thatâs a lot of things! You might be asking â âWhy would they only go up to 22?â Well, remember that each tuple is in fact its own type, and so after defining the first 21 tuple types, I assume that someone got to typing out this type signature and then said âOK screw it thatâs enough already.â:
case class Tuple22[+T1, +T2, +T3, +T4, +T5, +T6, +T7, +T8, +T9, +T10, +T11, +T12, +T13, +T14, +T15, +T16, +T17, +T18, +T19, +T20, +T21, +T22](_1: T1, _2: T2, _3: T3, _4: T4, _5: T5, _6: T6, _7: T7, _8: T8, _9: T9, _10: T10, _11: T11, _12: T12, _13: T13, _14: T14, _15: T15, _16: T16, _17: T17, _18: T18, _19: T19, _20: T20, _21: T21, _22: T22) extends Product22[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, T11, T12, T13, T14, T15, T16, T17, T18, T19, T20, T21, T22] with Product with Serializable
As far as I know, thereâs no real technical reason to stop at 22-tuples, but if you need more than that you can first define it yourself, and then go seek help, because you need it.
It turns out there are two pretty common uses for tuples. For one, you get them from zip-ing collections together. Calling zip on a collection (list a List) that supports it with another collection will take the first element of each of those, and make it a tuple, then take the second element of each of those collections and then make those a tuple and so on, until one of them runs out of elements!
scala> List(1,2,3,4,5,6).zip(List("a", "b", "c", "d")) res6: List[(Int, String)] = List((1,a), (2,b), (3,c), (4,d))
As you can see, any leftovers in either collection are ignored. Aside from combining collections, it turns out that tuples serve another handy purpose where one might want an ordered set of different types and fixed lengths: function parameters are essentially tuples. In fact, function objects have a tupled method which swaps their parameter lists for a single tuple of the same type:
scala> val f = (a:String, b:Int, c:Symbol) => a + b.toString f: (String, Int, Symbol) => String = <function3> scala> f.tupled res7: ((String, Int, Symbol)) => String = <function1> scala> f.tupled ("bob", 6, 'what) res8: String = bob6
What can we do with some of what weâve learned so far? How about finding right triangles: weâll need for comprehensions to find lists of tuples representing the lengths of the sides. Perfect! Specifically, letâs find an algorithm for finding triangles whose sides all add up to a certain number.
First, weâll probably need to know what our choices are here. I suppose all of the triangles up to a maximum side length of a could simply be defined like so:
scala> def allTriangles(a:Int) = for(x <- 1 to a; y <- 1 to a; z <- 1 to a) yield (x, y, z) allTriangles: (a: Int)scala.collection.immutable.IndexedSeq[(Int, Int, Int)]
On second thought, thatâs kind of dumb though. If weâre assembling right triangles, we really only need the triangles where the sum of the first two sidesâ squares is equal to the thirdâs square. We should probably switch that up to include a filter:
scala> def rightTriangles(a:Int) = for(x <- 1 to a; y <- 1 to a; z <- 1 to a; if (Math.pow(x,2) + Math.pow(y,2) == Math.pow(z,2))) yield (x, y, z) rightTriangles: (a: Int)scala.collection.immutable.IndexedSeq[(Int, Int, Int)]
That looks better. To try this out, letâs get all the right triangles whose sides are all 5 or less, which should be just the 3â4â5 right triangle. Letâs give it a shot:
scala> rightTriangles(5) res0: scala.collection.immutable.IndexedSeq[(Int, Int, Int)] = Vector((3,4,5), (4,3,5))
Hmm. Kind of. We did get the correct triangle, but we actually got it twice in two different forms. Looks like we should change up our generators.
scala> def rightTriangles(a:Int) = for(z <- 1 to a; y <- 1 to z; x <- 1 to y; if (Math.pow(x,2) + Math.pow(y,2) == Math.pow(z,2))) yield (x, y, z) rightTriangles: (a: Int)scala.collection.immutable.IndexedSeq[(Int, Int, Int)] scala> rightTriangles(5) res1: scala.collection.immutable.IndexedSeq[(Int, Int, Int)] = Vector((3,4,5))
Niiiiice. Now weâre not checking a bunch of extra triangles that we donât need to. Now, we just need to find triangles that have a sum of sides equal to something in particular. We could probably do this with another for filter.
scala> def rightTriangles(a:Int, b:Int) = for(z <- 1 to a; y <- 1 to z; x <- 1 to y; if (Math.pow(x,2) + Math.pow(y,2) == Math.pow(z,2)); if (x + y + z) == b) yield (x, y, z) rightTriangles: (a: Int, b: Int)scala.collection.immutable.IndexedSeq[(Int, Int, Int)]
Cool! Letâs see if it works! We know the 3â4â5 triangleâs sides add up to 12, so we can check against that:
scala> rightTriangles(5, 12) res2: scala.collection.immutable.IndexedSeq[(Int, Int, Int)] = Vector((3,4,5)) scala> rightTriangles(5, 10) res3: scala.collection.immutable.IndexedSeq[(Int, Int, Int)] = Vector()
Nailed it! Letâs see how he does on some other triangles:
scala> rightTriangles(50, 24) res4: scala.collection.immutable.IndexedSeq[(Int, Int, Int)] = Vector((6,8,10)) scala> rightTriangles(50, 30) res5: scala.collection.immutable.IndexedSeq[(Int, Int, Int)] = Vector((5,12,13)) scala> rightTriangles(50, 44) res6: scala.collection.immutable.IndexedSeq[(Int, Int, Int)] = Vector() scala> rightTriangles(50, 40) res7: scala.collection.immutable.IndexedSeq[(Int, Int, Int)] = Vector((8,15,17))
Awesome. This has got me missing Trigonometry class. Well hey, we have the lengths of the sides of some right triangles, which means we can probably get the measures of the inside angles â and see how handy having these packaged as tuples is at the same time!
rightTriangles(5, 12) map { t => (180 * Math.acos(t._2/5.0) / Math.PI, 180 * Math.acos(t._1/5.0) / Math.PI) } res8: scala.collection.immutable.IndexedSeq[(Double, Double)] = Vector((36.86989764584401,53.13010235415598))
So, did all of this just blow your mind?! Good. Stick around, weâre just getting startedâŠ
Which works, by the way. Â â©
This is not a technical term. I made it up but it sounds awesome. Â â©