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:
- The string we want to modify
- The part of the string we want to change
- 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 withIO.puts/1
. - Ponder on what happens when you remove the
IO.puts
from the beginning of Line 3 ingreet.exs
and then run the program withelixir greet.exs
. Think about how this would be different if you put that code into aniex
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.