After sitting in the to-be-reviewed queue at Apple for a week, I’m happy to announce that version 1.1 of GV Places is now available for purchase. The full list of changes is here.
tech
GV Places 1.1 Submitted To App Store
Yesterday, I submitted version 1.1 of GV Places to the App Store for review. I am hopeful that it won’t take as long to get through this time, but I’m not holding my breath.
The biggest news is that I dropped the iOS target version, so now people with Verizon iPhones can run it. I had hoped that the app store would not let someone download an app that they couldn’t run, but based on the 1-star review I got from a Verizon owner, that may not be the case. Here are the release notes
- Should now work on Verizon iPhones.
- Fixed a sorting/display bug that occurred when a place was autoswitched. The next time the app was run, the list would not be sorted properly.
- Improved the region detection on iPhone 4. This should properly handle overlapping regions, which worked fine on the 3GS, but not so much on the iPhone 4.
- Fixed a couple of memory leaks.
- Note that when autoswitching is turned on, the app is not constantly running, looking for location changes. On the iPhone 4, it uses the region monitoring API, which means it tells the OS the regions it is interested in, and then goes to sleep. Whenever the phone enters one of those regions, the OS will wake the app up, telling it that it entered that region. This uses no extra battery on my iPhone 4, that I could tell. On the 3GS, it uses the significant location change monitoring API. With this API, the app tells the OS it is interested in significant location changes (this is mostly when cell towers change), and then goes to sleep. When one of these events occurs, the OS wakes the app up, telling it that a significant location change has occurred. The app then gets the current location and sees if it is within one of your places. If it is, it switches to that place. This is not as accurate as on the iPhone 4, but it’s all the 3GS has. This would probably use up a bit more battery than on the iPhone 4, because it has to do more work, but I didn’t notice any significant battery degradation on my 3GS.
- Also note that autoswitching of places is NOT supported on the iPhone 3G, because neither of the APIs I described above were available on the 3G. Everything else works on the 3G.
Downgrading Your iPhone From iOS5 to iOS4.3.3: It Ain’t Easy
Like lots of other iPhone owners, I was excited about the announcement on Monday of iOS5, and all the new features. I don’t like to wait and since it isn’t going to be released until September, I had to get it somehow. Since I’m an iOS developer, I was able to download a developer seed of iOS5 and install it on my iPhone 3GS. I did so Tuesday morning.
It was painfully slow on the 3GS. Granted this is just beta 1, and it will probably improve before it launches, but it was nearly unusable.
So today, I went out and bought an iPhone 4. I had been wanting one for a long time anyway, and my contract was up in February so I’ve been able to get a subsidized price since then… Anyway, I bought a 4, and was not planning on putting iOS5 on it just yet. But when I attached it to iTunes and tried to restore my last 3GS backup to it, iTunes wouldn’t let me because the backup was of iOS5 and the iPhone 4 was running 4.3.3. So, against my better judgment, I installed iOS5 on it.
Big mistake.
While speed was no longer an issue, bugginess was. Lots of 3rd-party apps just seemed to not work right, and even some features of iOS were a bit squirrelly. A downgrade was in order. But, as the release notes tell you, you can’t downgrade.
Well, you can, but it’s a major pain in the butt. Below is a list of what I did to restore mine. But first, please be aware that your mileage may vary, use at your own risk, by reading this, you agree that I am not liable for any damage to your iPhone, iTunes, data, computer, house, car, cat, dog or anything else.
(This is in list form, but it’s not an instruction list, per se. It’s a list of what I did, thus the verbs are in the past tense.)
- Restored the entire ~/Library/Application Support/MobileSync/Backup directory from Monday, before I did the upgrade of the 3GS, using Time Machine. (On Windows, the directory is C:Users<UserName>AppDataRoamingApple ComputerMobileSyncBackup)
- Downloaded the iOS 4.3.3 software image from links here (the links go to Apple) http://osxdaily.com/2011/05/04/ios-4-3-3-download/
- Launched iTunes, but hit the little ‘X’ button in the top area to stop it from syncing.
- Selected the iPhone 4 in the Devices section, then pressed Option while clicking on the “Check for Update” button. This popped up a dialog to find the .ipsw file that I had downloaded. I navigated to where I had saved it, and selected it.
- iTunes started to restore it, then barfed with one of those “unknown” errors. It did leave the iPhone in recovery mode.
- Out of paranoia, I shut down iTunes, deleted the partial backup that it had tried to do, thus leaving the Backup directory as it was on Monday.
- Started iTunes.
- iTunes popped up a dialog saying that the phone was in recovery mode and did I want to restore it. I said “yes.”
- It then showed me the EULA for 4.3.3, re-downloaded the .ipsw, then started the restore.
- Ten minutes later, it rebooted and showed up in iTunes, with iTunes asking if I wanted to restore it from one of my backups.
- I selected the 3GS backup and off it went.
- When the phone rebooted, iTunes started re-syncing it.
- Half an hour more, and it was done restoring all my data and settings. Then it started restoring 4,500 songs, which takes about 3 hours.
So, that’s it. All in all, it took me about 2 hours, start to finish. If you don’t have a backup of the Backup directory, you’re screwed.
I use pre-release software all the time, but this is the first time I’ve really, truly, gotten burned by it. I have now learned my lesson, and will at least wait for the release of iOS5… beta… 3 before trying again. 🙂
GV Places Is Now In the iTunes App Store

I am pleased to announce that my first iPhone app, called GV Places, is now available in the iTunes App Store. If you have an iPhone (preferably a 3GS or 4) and a Google Voice account, you might like it.
What it does is lets you create geographical areas that will enable or disable your Google Voice callback numbers, in various combinations. For example, I have three places that I use: Home, Office and Georgia. For the Home place, I have a region that fits snugly around my house, maybe .25 mile in each directory. For this place, I have my house line, my Skype-In number and my Google Talk IM enabled. My cell phone is disabled, because AT&T coverage here stinks.
For the Office place, I have a 2 mile-ish area around my office. Enabled numbers are my cell phone, my Skype-In and my Google Talk IM. My home number is disabled, naturally, because I don’t want calls ringing at home when I’m not there.
Finally, the Georgia place covers the entire state, plus some bits of the adjoining states. For this place, I have my cell phone enabled, and nothing else. This makes sense because I am most likely not at home, or my office, if this place is active.
Once you have defined your places, and associated phones with them, there are two ways to activate a place: manual and automatic. Manual mode means you go to the main Places screen and tap on the one you want to activate. Automatic mode only works on iPhone 3GS and 4, and uses the location awareness features of the phone to automatically switch places for you as you move around. When Automatic mode is enabled, even if GV Places is not running, iOS will notify it when you enter one of your places and it will then activate that place for you. Automatic mode works best on an iPhone 4, though if you are in a good coverage area, it works pretty well on a 3GS. (This is a hardware limitation of the 3GS.)
Here’s the Settings screen where you provide your Google Voice email address and password (which is stored in the Keychain for security purposes). If you have a Google Voice account that ends in @gmail.com, then you can leave the Hosted Apps account switch turned off. If you know that you have a hosted apps account, turn this on.
Next is the App Settings section, where you can tweak a few points of how the app works. By default, when you tap on a place to activate it, GV Places will prompt you to make sure that’s really what you want to do. If this annoys you, you can turn it off here.
Automatic place activation is controlled by the next option. This is disabled by default, but can easily be turn on here. Note that you must have an iPhone 3GS or iPhone 4 to use automatic place activation.
The final option here is only useful if automatic place activation is enabled. If “Notify on automatic activation” is turned on, then a notification will appear on your phone when GV Places automatically activates a place. If you don’t like this, you can easily turn it off.
Next is the Place Editor. This is where you define your places using the map. When you create a place, it starts out centered on your current location. You can use your fingers to drag the map around, and pinch to zoom, just like on the built-in Maps app. When you tap Save, the place will encompass everything that is shown on the map. Once you have the map how you’d like it, be sure to give the place a meaningful name. Finally, tap the button at the bottom of the screen. If you are just creating a place, it will say “Select Phones.” After that, it will indicate how many phones you have selected.
This is the screen where you select which of your phones you want associated with this place. Just tap the ones you want to place a checkmark next to it. Tap again to remove the checkmark. Those that are checked will be enabled for this place.
Finally, we come to the main Places screen. This shows each of the places you have defined, sorted by what I call their “snugness.” What this means is that the place that fits the most closely around your current location will be on top. The farther down the list you go, the less snug the place fits. Places that don’t encompass your current location at all will still show, but they will appear toward the end of the list, sorted by how close they are to your current location. Your currently-selected place will show in blue letters.
Snugness is neat because if you have overlapping regions, the region that fits most snugly will sort higher in the list. In my example, if I’m in my house, the Home place will sort higher than the Georgia place, because Home fits more snugly around my house than Georgia does.
Tap a row to activate that place. If you want to edit a place, tap the blue disclosure icon on the right-hand side of its row. To add a new place, tap the + icon at the bottom right. To get to the settings page, tap Settings.
I have a wiki setup for it, which current just has mostly this same information, at https://bitbucket.org/joeygibson/gvplaces/wiki/Home. If you buy it and find a bug, you can report it at https://bitbucket.org/joeygibson/gvplaces/issues.
If you’re just dying to own a copy of your very own, you can buy it for $1.99 in the app store.
First Rejection of My iPhone App :-(
I submitted my first iPhone app, called GV Places, to the Apple App Store on Monday, May 23. On Friday, May 27, around 9:00 in the morning, it entered the review process. About ten hours later I received the first rejection notice. The reason? I didn’t provide them a demo account to use for testing. I didn’t provide one, because of what my app does.
My app works with Google Voice, so you need a Google Voice account to use it. That’s not something I can just create willy-nilly and provide them for testing. Yes, I could create another account, but to really use it, you have to assign some phones to it, and to do that, Google Voice calls them to verify that they are yours. So I would need to use my own phone lines to set it up. I don’t think I should have to do that. I assumed that the tester would have his own Google Voice account to test with. Just like I would assume that if they test a Facebook app, they use their own Facebook accounts. I said as much in my response to them. I suppose we’ll see what they say.
It was sort of amusing that in the rejection email it said they weren’t able to test “all of the features” without a demo account. That’s funny because you can test any of the features without a Google Voice account.
GVoice: An Open Source Objective-C Google Voice Library
05/12/2011 Note: I failed to mention that the library, as it currently stands, builds for iOS only. I’m sure it could be rejiggered to work with OSX, because I don’t think I used any iOS specific features. All the project files would need to be changed, and I’m not sure what’s involved with that.
I am please to announce my free and open source Objective-C library for working with Google Voice. It’s called GVoice and you can find it here. It’s BSD licensed, which means you can use it for anything you want, both commercial and free/opensource.
It’s quite easy to use, and this example should help:
[objc]
// USERNAME, PASSWORD, SOURCE and ACCOUNT_TYPE should be replaced with proper
// values.
self.voice = [[GVoice alloc] initWithUser: USERNAME password: PASSWORD source: SOURCE
accountType: ACCOUNT_TYPE];
// This causes some logging to happen.
self.voice.logToConsole = YES;
BOOL res = [self.voice login];
if (!res) {
// error handling
}
// Assuming you have a phone whose id is 23, this would cause Google Voice
// to route calls to it.
res = [self.voice enablePhone: 23]
if (!res) {
// error handling
}
// Send an SMS. Replace TEXT_PHONE_NUMBER with a proper 10-digit phone number
// capable of receiving SMS messages
res = [self.voice sendSmsText: @"Testing 1, 2, 3" toNumber: TEXT_PHONE_NUMBER];
if (!res) {
// error handling
}
[/objc]
On line 3, we create an instance of GVoice, passing in the email address and password from the user. The third field, source, is a field required by Google to identify where the connection is coming from. It’s free-form, but they suggest a reverse_domain_name-app-version approach, , something like com.joeygibson-GVTest-1.0, for example. The fourth parameter is what sort of account you’re trying to connect to, and there are two choices: GOOGLE and HOSTED. (This is an enum that also has a value of HOSTED_OR_GOOGLE, but I would suggest letting your user decide which account they have. It will save you problems later.)
Line 9 is where the actual login happens. If you get back a YES, all is well. If not, you can look at the GVoice object’s errorDescription property.
Line 17 demonstrates using one of the features of the library: enabling a phone. You pass the phone’s Id, which is obtained through another part of the API, and GV will then ring that number when a call comes in. You can also disable phones in the same fashion.
Line 25 shows how to send an SMS message from the GV account to the specified mobile phone.
There are many features that are fully formed, though some are still not as polished as I’d like. Two things still remain to be done: handling redirects and CAPTCHAs. After a certain number of failed logins, a URL will be returned that leads to a CAPTCHA image. To login after than you need to send a response to the CAPTCHA, but none of that is implemented in the library yet. Similarly, sometimes requests can be redirected by Google, but the library doesn’t handle those either. I’ve never seen either of these cases occur, but they could.
There is a full test suite included, which provides many more examples of how to use it. Before trying to compile it, you need to copy a file in the GVTests directory called GVCredentials-Sample.h to GVCredentials.h, replacing the dummy values with proper values. After that, you should be able to compile it and run the tests.
Full API documentation is available in the doc/ directory.
I wrote this library to use with my own iPhone app, which is currently in final testing before submission to the app store. I thought it would be useful for other people, so that’s why it’s free. If you’d like to use it, please do. If you’d like to improve it, let me know, and I can give you access to the repo.
It’s hosted on Bitbucket: https://bitbucket.org/joeygibson/gvoice.
99 Scala Problems #28 – I Like My Solution Better
I’ve been working through this list of 99 Scala Problems, which is modeled after this list of 99 Prolog Problems. As I’ve been going through them, I have been comparing my solutions to those provided (obviously). Sometimes, my solution is more or less the same as the “official” solution. Sometimes, theirs is better. In the case of problem 28, I think mine is far easier to read and understand.
Problem 28 has two parts. The first part reads:
a) We suppose that a list contains elements that are lists themselves. The objective is to sort the elements of the list according to their length. E.g. short lists first, longer lists later, or vice versa.
Running the function should look like this:
[scala]
scala> lsort(List(List("a", "b", "c"), List("d", "e"), List("f", "g", "h"), List("d", "e"), List("i", "j", "k", "l"), List("m", "n"), List("o")))
res0: List[List[java.lang.String]] = List(List(o), List(d, e), List(d, e), List(m, n), List(a, b, c), List(f, g, h), List(i, j, k, l))
[/scala]
For this part, my solution was almost identical. Here’s what I came up with:
[scala]
def lsort[T](ls: List[List[T]]) = {
ls.sortWith {(a, b) => a.length < b.length}
}
[/scala]
You can see that this function takes a List of type T, and then calls the sortWith method on that list, passing in a function value that sorts the lists based on their length, shortest to longest. The “official” solution was only slightly different:
[scala]
def lsort[A](ls: List[List[A]]): List[List[A]] =
ls sort { _.length < _.length }
[/scala]
Here, they used A instead of T, but that doesn’t affect anything, and they specified the return type, while I left mine inferred. Instead of assigning each bucket of the list to a named variable, as I did, they use the underscore placeholder. The two functions are functionally (get it?) identical, but theirs is a bit shorter because they removed the outer braces, and were able to skip the parameter list, since they used the underscores.
Now, the second part is where I diverge from the official solution. Here’s the problem description:
b) Again, we suppose that a list contains elements that are lists themselves. But this time the objective is to sort the elements according to their length frequency; i.e. in the default, sorting is done ascendingly, lists with rare lengths are placed [first], others with a more frequent length come later.
And the expected call and result is
[scala]
scala> lsortFreq(List(List("a", "b", "c"), List("d", "e"), List("f", "g", "h"), List("d", "e"), List("i", "j", "k", "l"), List("m", "n"), List("o")))
res1: List[List[java.lang.String]] = List(List(i, j, k, l), List(o), List(a, b, c), List(f, g, h), List(d, e), List(d, e), List(m, n))[/scala]
First, let’s look at what they presented as the solution. It referenced functions from other files, but I have included them all here for easy of viewing.
[scala]
def lsortFreq[A](ls: List[List[A]]): List[List[A]] = {
val freqs = Map(encode(ls map { _.length } sort { _ < _ }) map { _.swap }:_*)
ls sort { (e1, e2) => freqs(e1.length) < freqs(e2.length) }
}
def encode[T](ls: List[T]): List[(Int, T)] = {
val packedList = pack(ls)
packedList map {list => (list.length, list.head)}
}
def pack[T](ls: List[T]): List[List[T]] = ls match {
case Nil => Nil
case h :: tail => (h :: tail.takeWhile(_ == h)) :: pack(tail.dropWhile(_ == h))
}
[/scala]
I think this is very confusing code. It’s calling the encode function which does run-length encoding of the passed-in thing. It then uses a Map of these encodings to sort the passed-in list. The presence of five underscores in the first line, obscures where those parameters are coming from, and the final underscore is actually part of the _* method of the Array class!
My solution, while being a longer function, is far more readable, in my opinion. And, it’s the same number of lines as the three-method solution. Here it is
[scala]
def lsortFreq[T](ls: List[List[T]]) = {
val lengthMap = scala.collection.mutable.Map[Int, Int]()
for (l <- ls) {
val len = l.length
if (!lengthMap.contains(len)) {
lengthMap(len) = 1
} else {
lengthMap(len) += 1
}
}
ls sortWith {(a, b) => lengthMap(a.length) < lengthMap(b.length)}
}
[/scala]
In my function, I created a mutable Map and then iterate over the list, getting each item’s length, and then keep a running tally of how many items had that length. The map has these lengths as its keys, and the number of items with that length as its values. Get it? I then sort the original list by having each item in the comparison lookup how many items share its length, and use that as the sort criterion.
I have no idea which of these solutions is more efficient. For small problems like this, I doubt there’s any measurable difference. But I do believe that my solution is easier to read and understand. So much so, in fact, that I think someone who is not familiar with Scala would be able to easily figure out what it’s doing. I don’t know that the same can be said of the other solution.
I got criticized for promoting terse code in this article, so this is my attempt at balance. 🙂
Note: I did change the inputs to these functions from symbols to strings. The code formatter I use on the blog wasn’t colorizing things properly when there were symbols involved.
Procedural vs. Functional
With the rise of Scala and Clojure, there’s been a lot of talk lately about procedural vs. functional styles of coding. Most developers are accustomed to procedural coding, and functional can be hard to get a handle on. I was working through Programming in Scala (again) this morning, and I came upon this function:
// Procedural implementation
def longestWord(words: Array[String]) = {
var word = words(0)
var idx = 0
for (i <- 1 until words.length)
if (words(i).length > word.length) {
word = words(i)
idx = i
}
(word, idx)
}
The purpose of this function is to find the longest word in the passed-in array, and return a tuple with that longest word, and its index in the array. You can see that in this function, we have two vars, one for the current longest word, and another for its index in the array. We then use a for expression to walk the array, reassigning word and idx when we find a longer word. This is very much like how you would write this in Java.
I decided to rewrite this function in a more functional style, just to see how my functional chops are coming along. Here’s what I ended up with:
// A more functional implementation
def longestWord(words: Array[String]) =
(("", -1) /: words.zipWithIndex) {(old, cur) =>
if (cur._1.length > old._1.length) cur
else old
}
First of all, notice how much shorter this function is than the first one. Also, notice that there is only a single expression in the function, so the outer curly braces aren’t necessary. What this expression is doing is calling zipWithIndex on the passed-in array, which results in an array of tuples containing each word and its index. We then call foldLeft using its operator name of /:, with its initial argument being a tuple with an empty string and -1 for an index. What foldLeft does is apply the function value passed to it to pairs of arguments. On the first pass, the arguments are what was passed in and the first element in the array. On the second iteration, the arguments are the result of the first pass and the second element in the array. This then continues through the entire array. What is returned after the final pass will be a tuple that contains the longest word in the array, and its index.
Now, I don’t claim to be a functional master or anything, but I think this is a decent illustration of how the functional style can reduce the lines of code, and the number of mutable variables, while making the code easier to read and understand.
Slides From My Presentation on Operator Overloading In Scala
Last night I spoke at the Atlanta Scala Enthusiats meeting about operator overloading and a little on implicit conversions. I think the talk went well as I got lots of really good questions from the audience, and they laughed at my jokes. This presentation grew out of a blog post I wrote a few months ago entitled Scala Gets Operator Overloading Right; I beefed it up and made some slides and more code samples. Incidentally, if you Google for “scala operator overloading” that blog post is the first result.
For those of you who weren’t there, here are my slides and the code samples that go with them. I wrote these samples against Scala 2.7.7.final. They should work with the latest Scala 2.8, but I haven’t verified this.
And here’s the source code: oopres.zip
What the Heck Is a Tuple, Anyway?
Yesterday I was talking with a friend about Scala and the subject of tuples came up. We both had a bit of a laugh that neither of us was sure how to pronounce it, though we both leaned toward TUH-ple instead of TOO-ple. Anyway, the utility of tuples in Scala was not immediately apparent to him, so I thought I’d take a whack at explaining it here.
A Tuple in Scala is an immutable container used for storing two or more objects, possibly of different types. While a List or Array can only store objects that all have the same type, Tuples can store objects of any type. The most common use of tuples is when you have a method that needs to return more than one value, but creating a class for that return value is more trouble than it’s worth. It’s true that for same-type objects you could return a List, and for different-type objects you could return a List[Any], but both of these have downsides, which we’ll discuss.
Let’s look at a very contrived example. Let’s say you created a function that takes a string and returns the starting index of the first numbers if finds and the numbers themselves. That code might look like this
def reFind(str: String) = {
val re = """(d+)""".r
val m = re findFirstMatchIn str
m match {
case Some(m) => (m.start, str.substring(m.start, m.end))
case None => (0, "")
}
}
(I’ve removed any error checking for brevity.) You can see here that we’re creating a regular expression that looks for one or more numbers grouped together. We then match that against the passed-in string. The matching method returns a Some[Match], so we pattern match against that to see if we actually got a match. If we did, we create a tuple with the starting index of the match, and the match itself, and return it. If not, we return a tuple with 0 for the starting index and an empty string.
Calling this function looks like this
scala> val t = reFind("foo 123 bar")
t: (Int, java.lang.String) = (4,123)
You can see that what was returned is something with type (Int, java.lang.String); that’s actually an instance of Scala’s Tuple2 class. (There’s a synonym for Tuple2, called Pair.)
Now that we have this tuple, what do we do with it? If you want to access the values it contains, you do it in a way that might seem a bit strange at first. To get at the elements, you could do this
scala> val i = t._1 i: Int = 4 scala> val m = t._2 m: java.lang.String = 123
There are two things to point out here. First, unlike Lists and Arrays, you don’t use the () notation. You use a method consisting of an underscore and the index of the part you want. Second, unlike Lists and Arrays, tuples are 1-based instead of 0-based. (According to Programming In Scala, this is a nod to Haskell and ML.) Also notice the types of the vals you are assigning. That’s one of the benefits of using a Tuple instead of something like List[Any]; you still get compile-time type safety. Had you instead written the function like this
def reFind(str: String) = {
val re = """(d+)""".r
val m = re findFirstMatchIn str
m match {
case Some(m) => List[Any](m.start, str.substring(m.start, m.end))
case None => List[Any](0, "")
}
}
and called it, look what happens when you try to store the Int index in a local variable
scala> val l = reFind("foo 123 bar")
l: List[Any] = List(4, 123)
scala> val i: Int = l(0)
<console>:10: error: type mismatch;
found : Any
required: Int
val i: Int = l(0)
^
You would get a similar error trying to assign the String element to a local String val. That’s the major downside to using a List[Any]. (In the first example I used Scala’s type inference to set the types of the local variables; this time I wanted to be explicit to show the failure.)
As I mentioned earlier, you could define a class just to handle the return values of this function. There is nothing wrong with that solution, and some will find it superior to using a tuple, because you can assign meaningful names to the elements. You could define it like this
class ReResult(val index: Int, val part: String)
def reFind(str: String) = {
val re = """(d+)""".r
val m = re findFirstMatchIn str
m match {
case Some(m) => new ReResult(m.start, str.substring(m.start, m.end))
case None => new ReResult(0, "")
}
}
and call it like this
scala> val l = reFind("foo 123 bar")
l: ReResult = ReResult@57c52e72
scala> val i: Int = l.index
i: Int = 4
scala> val m: String = l.part
m: String = 123
If you think this is more maintainable, then by all means, use it. If you just want to easily return more than one value from a function, then consider using a tuple.
Another point on tuples is that you can assign all the elements of a tuple to local variables in a single step, rather than using multiple calls. So this is equivalent to all the assignments from the earlier examples
scala> val (i: Int, m: String) = l i: Int = 4 m: String = 123
Depending on what you’re doing, this could be a useful way to get at the elements.
And one more thing. There are tuple classes that range from two elements all the way up to twenty-two. The classes are named Tuple2, Tuple3 … Tuple22. The () notation for creating tuples applies all the way up to twenty-two arguments, so you rarely need to actually use the class names. For example,
scala> val t = (23, "foo", 18.0)
t: (Int, java.lang.String, Double) = (23,foo,18.0)
scala> t.getClass
res31: java.lang.Class[_] = class scala.Tuple3
scala> val t1 = ('a', "quick", 23, "year-old", """foxy""".r, List(1, 2, 3))
t1: (Char, java.lang.String, Int, java.lang.String, scala.util.matching.Regex, List[Int]) = (a,quick,23,year-old,foxy,List(1, 2, 3))
scala> t1.getClass
res32: java.lang.Class[_] = class scala.Tuple6
I’m not going to provide an example of creating a Tuple22; that is left as an exercise. 🙂 I would argue that if you need more than three elements, you really should define a class to hold them. I think that beyond three elements it gets difficult to keep them straight. Tuples are great for holding two or three pieces of information, but don’t go crazy with them.




