Joy of Elixir

8. Working with strings, input and output

In this chapter we'll go through some of the built-in functions that Elixir has. We'll be focussing specifically on strings, input and output. Elixir has some very handy functions already available to use and we'll see just a small taster of that here.

Working with strings

We'll start by looking at the functions to work with strings. Strings are where we started back in Chapter 1, so it only makes sense to start with them here, too.

Reversing a string

Have you ever wanted to reverse a sentence, but didn't want to type all the different characters yourself? Elixir has a handy function for this called String.reverse. Here it is in action:

iex> String.reverse("reverse this")
"siht esrever"

Izzy elicits a noticeable "wooooowww, that's so cool" at this. "What else does Elixir have?", she asks quickly. We'll get to that, Izzy. Let's take the time now to understand what's happening here first.

The functions that Elixir provides are separated into something akin to kitchen drawers or toolboxes, called modules. Whereas in your top kitchen drawer you might have forks, knives, sporks, and spoons (like every sensible person's kitchen does), and in another you might have measuring cups, and in another tea towels, in Elixir the functions to work with the different kinds of data are separated into different modules. This makes finding functions to work with particular kinds of data in Elixir very easy.

Here, we're using the reverse function from the String module ("drawer" / "toolbox"). We know that this is a module because its first letter is upper-case. We're passing this String.reverse function one argument, which is a string "reverse this". This function takes the string and flips it around, producing the reversed string: "siht esrever".

Note here how we don't need to put a dot between the function name and its arguments, like we had to do with the functions we defined ourselves. You don't need to do this when you're running a function from a module. You only need the dot if you've defined the function and assigned it to a variable. For instance, with our old greeting function:

iex> greeting = fn (place) -> "Hello, #{place}!" end
#Function<6.52032458/1 in :erl_eval.expr/5>
greeting.("world")

When calling the String.reverse function, Elixir knows that it's a function because of that String. prefix. We don't need a dot right after the function name:

iex> String.reverse("reverse this")
"siht esrever"

Splitting a string

What about if we wanted to split a string into its individual words? For that, we can use the String.split function:

iex> String.split("split my string into pieces")
["split", "my", "string", "into", "pieces"]

We now have a list of the words in the string. We'll see what we could do with such a list in the next chapter. For now, let's look at what else is in this String module.

Replacing parts of a string

What about if we wanted to replace all the occurrences of a particular letter in a string with another one? For that, Elixir has the String.replace function:

iex> String.replace("moo", "o", "e")
"mee"

This function takes three arguments:

  1. The string we want to modify
  2. The part of the string we want to change
  3. What we want it to change to

The way we've used the function here means that it will change all the "o"s in the string into "e"s.

It's worth noting that we can change more than single character at a time too:

iex> String.replace("the cow jumped over the moon", "oo", "ee")
"the cow jumped over the meen"

Notice here that it didn't change the "o" in the word "cow" or the other "o" in the word "over". This is because we told the function to look for two "o"s in a row.

Making all the letters of a string uppercase

What about if we wanted to make the computer turn a string into its shouty variant? We can use upcase for this:

iex> String.upcase("not so quiet any more")
"NOT SO QUIET ANY MORE"

Making all the letters of a string lowercase

At the opposite end of that particular spectrum, there is downcase:

iex> String.downcase("LOUD TO QUIET")
"loud to quiet"

So as you can see, the String module has some helpful functions that can help us whenever we need to split a string, turn it all into upper / lower ("down") case. There's plenty more functions in the String module, and we'll see some of those in due time.

Input and output

Input and output are two fundamental things that you'll work with while programming. Programming is all about taking some data as an input and turning it into some form of an output. We've seen this multiple times already with the functions we've defined and used throughout this book. For instance, in that String.downcase function just above, the string "LOUD TO QUIET" is the input, and the "loud to quiet" generated by the method is the output.

What we'll cover in this section is getting some input from a different source: a new prompt. We'll prompt the user for their name and then we will use whatever they enter to output a message containing that input.

Making our own prompts

Let's say that we wanted to prompt people for their names and we wanted to prompt them in a way that meant that they didn't have to read Joy of Elixir to understand that strings had to be wrapped in double quotes and that they had to enter their input into an iex prompt.

Fortunately for us, Elixir has a module that provides us a function to do just this. That module is called IO (Input / Output) and the function is called gets. The name gets means "get string" and it will allow us to do exactly that. Let's see this function in practice:

iex> name = IO.gets "What is your name?"
What is your name?

"Hey what happened to our iex> prompt?", Izzy asks. Good question! We're using gets and passing it a string. This string then becomes a new prompt. This prompt is asking us for our name. Let's type in our name and press enter:

iex> name = IO.gets "What is your name?"
What is your name?The Reader
"The Reader\n"

Ok, so there's some output here. But what does it mean? If we check our name variable's contents we'll see that it contains this "The Reader\n" string.

iex> name
"The Reader\n"

Izzy continues asking great questions: "What's that \n on the end?". That is a new line character and tells the computer that we pressed enter. While the IO.gets function stopped prompting us after we pressed enter, it still kept the enter in case we wanted it too. In this particular case we don't really want that character. We can get rid of it by using another function from the String module, called trim.

iex> name = String.trim(name)
"The Reader"

That's much better! Now we have our name without that pesky new line character suffixed. What String.trim does is remove all the extra spacing from the end of a string, giving us just the important parts.

Taking input and making it output

We've now got some input, but what's the point of taking input if you're not going to do anything with it? So let's do something with it! What we'll do with this input is to output a greeting message.

Let's deviate here from using the iex prompt and instead write our code inside one of those Elixir Script (.exs) files we mentioned back at the end of Chapter 5. Let's call this file greet.exs and put this content inside of it:

name = IO.gets "What is your name? "
age = IO.gets "And what is your age? "
IO.puts "Hello, #{String.trim(name)}! You're #{String.trim(age)}? That's so old!"

Well that's a bit sneaky of that IO.puts to just appear out of nowhere! Just like gets means "get string", puts means "put string". This function will generate some output when our script runs. If we didn't have this IO.puts here, our program would only take input, and it would not generate any output.

In this function we're interpolating the output of the String.trim function twice. Remember: we're doing this to remove the new line character (\n) from the result of the IO.gets calls.

There's some more new syntax that we've never seen before either. We've seen that we could interpolate variables into strings, but we've never seen that we could call functions while interpolating too. It's absolutely something you can do in Elixir. When interpolating inside a string you can put any code inside the interpolation brackets (#{}) — but as a general rule-of-thumb it's good to keep this interpolated code as short and simple as possible. Normally, you would only interpolate variables. We're making a small exception here to interpolate a function instead.

Let's run our greet.exs script now. First, we'll need to stop our iex prompt, which we can do by pressing Ctrl+C twice. Then we can run the script with this command:

elixir greet.exs

Here's what we'll see initially:

What is your name?

The script is prompting us for our name and it is doing that because the first line of code in that script is running the IO.gets function. Let's enter our name again and press enter.

What is your name? Reader
And what is your age?

This little script is now prompting us for our age. This is because the second line is calling another IO.gets. Let's enter our age and then press enter again,

What is your name? Reader
And what is your age? 30ish
Hello, Reader! You're 30ish? That's so old!

Our script gets to the third and final line, where it runs the IO.puts function and outputs its little teasing message. Apparently being 30ish is old! Kids these days have no respect.

This is just a small example of what we can do with IO.gets and IO.puts. We could use any number of IO.gets and IO.puts function calls to build up a program that took user input and generated some output from it.

The unchanging world

Now's a good time as any to introduce to you to another feature of Elixir, called immutability. Immutability is another one of those big computer science-y fancy words which is used to describe things that do not change over time. "Do not change over time" made a whole load more sense to me than "immutability" when I first heard the word, to be perfectly honest.

We're talking about it in this chapter because nearly everything you've worked with so far in Elixir is immutable; unchanging and unchangeable. Most things in Elixir cannot be changed, modified, altered, messed with or any other synonym for those terms, and the way we talk about this particular attribute for these things is to say that these things are immutable.

When we've called a function such as String.downcase or String.upcase, we pass these functions strings as arguments and then we get back another string from the function; two completely different strings. All functions in Elixir behave this way: they cannot modify what they're given. Let's look at a quick example:

iex> sentence = "perfectly normal sentence"
"perfectly normal sentence"
iex> upcased_sentence = String.upcase(sentence)
"PERFECTLY NORMAL SENTENCE"
iex> sentence
"perfectly normal sentence"

By running String.upcase on the sentence, it doesn't change what the sentence is -- it's still exactly as we defined it. The data stored in the sentence variable is immutable. The only way we could change what is in that variable is if we re-assigned that variable:

iex> sentence = "another, even more perfectly normal sentence"
"another, even more perfectly normal sentence"

It's important to know in Elixir that calling functions on data will never change that data. What will happen instead is that we'll get back some new... thing as a result of that function. This will become more apparent the more Elixir code you write, but I thought it best to mention it here before you get too deep and someone mentions that big fancy word — "immutable". Now you're, as they say, "in the know" too.

Exercises

  • Make a program that generates a very short story. Get it to take some input of a person, a place and an object -- using IO.gets/1 and combine all three into a little sentence, output with IO.puts/1.
  • Ponder on what happens when you remove the IO.puts from the beginning of Line 3 in greet.exs and then run the program with elixir greet.exs. Think about how this would be different if you put that code into an iex prompt.

We've done a lot of work with strings so far in this chapter. Let's look at lists and maps again in the next chapter and the built-in functions that we can use with them.