Communicating with Humans - Introducing Elixir: Getting Started in Functional Programming (2014)

Introducing Elixir: Getting Started in Functional Programming (2014)

Chapter 5. Communicating with Humans

Elixir rebuilds the Erlang tools for working with strings from scratch, bringing them up to speed for Unicode (UTF-8) and recognizing that strings deserve more focus than just a list of characters. Chapter 4 showed you a bit of string handling and presentation (IO.puts), but there are more pieces you’ll want to learn to handle communications with people and sometimes with other applications. At the very least, this chapter will let you build more convenient interfaces for testing your code than calling functions from IEx.

NOTE

If you’re feeling completely excited about the recursion you learned in Chapter 4, you may want to jump ahead to Chapter 6, where that recursion will once again be front and center.

Strings

Atoms are great for sending messages within a program, even messages that the programmer can remember, but they’re not really designed for communicating outside of the context of Erlang processes. If you need to be assembling sentences or even presenting information, you’ll want something more flexible. Strings are the structure you need. You’ve already used strings a little bit, as the double-quoted arguments to IO.puts in Chapter 4:

IO.puts("Look out below!")

The double-quoted content (Look out below!) is a string. A string is a sequence of characters. If you want to include a double quote within the string, you can escape it with a backslash, like \". \n gives you a newline. To include a backslash, you have to use \\. Appendix A includes a complete list of escapes and other options.

If you create a string in the shell, Elixir will report back the string with the escapes. To see what it “really” contains, use IO.puts:

iex(1)> x= "Quote - \" in a string. \n Backslash, too: \\ . \n"

"Quote - \" in a string. \n Backslash, too: \\ . \n"

iex(2)> IO.puts(x)

Quote - " in a string.

Backslash, too: \ .

:ok

NOTE

If you start entering a string and don’t close the quotes, when you press Enter, IEx will just give you a new line with the same number. This lets you include newlines in strings, but it can be very confusing. If you think you’re stuck, usually entering " will get you out of it.

Elixir also provides operations for creating new strings. The simplest is concatenation, where you combine two strings into one. Elixir uses the unusual-looking but functional <> operator:

iex(3)> "el" <> "ixir"

"elixir"

iex(4)> a="el"

"el"

iex(5)> a <> "ixir"

"elixir"

Elixir also has string interpolation, using {} as a wrapper around content to be added to the string. You used this in Chapter 4 to see the value of variables:

IO.puts("#{n} yields #{result}.")

When Elixir encounters #{} in a string, it processes its contents to get a result, converts them to a string if necessary, and combines the pieces into a single string. That interpolation happens only once. Even if the variable used in the string changes, the contents of the string will remain the same:

iex(1)> a = "this"

"this"

iex(2)> b = "The value of a is #{a}."

"The value of a is this."

iex(3)> a = "that"

"that"

iex(4)> b

"The value of a is this."

You can put anything that returns a value in the interpolation: a variable, a function call, or an operation on parts. I find it most readable to just have variables, but your usage may vary. Like any other calculation, if the value to be interpolated can’t be calculated, you’ll get an error.

WARNING

Interpolation works only for values that are already strings or can naturally be converted to strings (such as numbers). If you want to interpolate any other sort of value, you must wrap it in a call to the inspect function:

iex(1)> x = 7 * 5

35

iex(2)> "x is now #{x}"

"x is now 35"

iex(3)> y = {4, 5, 6}

{4,5,6}

iex(4)> "y is now #{y}"

** (Protocol.UndefinedError) protocol String.Chars not implemented

for {4, 5, 6}

(elixir) lib/string/chars.ex:3: String.Chars.impl_for!/1

(elixir) lib/string/chars.ex:17: String.Chars.to_string/1

iex(4)> "y is now #{inspect y}"

"y is now {4,5,6}"

Elixir also offers two options for comparing string equality, the == operator and the === (exact or strict equality) operator. The == operator is generally the simplest for this, though the other produces the same results:

iex(5)> "el" == "el"

true

iex(6)> "el" == "ixir"

false

iex(7)> "el" === "el"

true

iex(8)> "el" === "ixir"

false

Elixir doesn’t offer functions for changing strings in place, as that would work badly with a model where variable contents don’t change. However, it does offer a set of functions for finding content in strings and dividing or padding those strings, which together let you extract information from a string (or multiple strings) and recombine it into a new string.

If you want to do more with your strings, you should definitely explore the documentation for the String and Regex (regular expressions) Elixir modules.

Multiline Strings

Multiline strings, sometimes called heredocs , let you create strings containing newlines. Chapter 2 mentioned them briefly, as a convenience for creating documentation, but you can use them for other purposes as well.

Unlike regular strings, multiline strings open and close with three double quotes:

iex(1)> multi = """

...(1)> This is a multiline

...(1)> string, also called a heredoc.

...(1)> """

"This is a multiline\nstring, also called a heredoc.\n"

iex(2)> IO.puts(multi)

This is a multiline

string, also called a heredoc.

:ok

Apart from the different way you enter them, you process multiline strings the same way as any other strings.

Unicode

Elixir works well with Unicode (UTF-8) strings. The String.length/1 function returns the number of Unicode graphemes in its argument. This is not necessarily the same as the number of bytes in the string, as it requires more than one byte to represent many Unicode characters. If you do need to know the number of bytes, you can use the byte_size/1 function:

iex(1)> str="서울 - 대한민국" # Seoul, Republic of Korea

"서울 - 대한민국"

iex(2)> String.length(str)

9

iex(3)> byte_size(str)

21

Character Lists

Elixir’s string handling is a major change from Erlang’s approach. In Erlang, all the strings were lists of characters, the same kind of lists you’ll learn about in Chapter 6. As many Elixir programs will need to work with Erlang libraries, Elixir provides support for character lists as well as strings.

WARNING

Character lists are slower to work with and take up more memory than strings, so they shouldn’t be your first choice.

To create a character list, you use single quotes instead of double quotes:

iex(1)> x = 'ixir'

'ixir'

You concatenate character lists with ++ instead of <>:

iex(2)> 'el' ++ 'ixir'

'elixir'

You can convert character lists to strings with List.to_string/1 and strings to character lists with String.to_char_list/1:

iex(3)> List.to_string('elixir')

"elixir"

iex(4)> String.to_char_list("elixir")

'elixir'

For purposes other than working with Erlang librares, you should probably stick with strings. (Chapter 6 will explain more about working with lists, and these may be helpful if you have data that you want to treat explicitly as a list of characters.)

String Sigils

Elixir offers another way to create strings, character lists, and regular expressions you can apply to the other two formats. String sigils tell the interpreter, “This is going to be this kind of content.”

Sigils start with a ~ sign, then one of the letters s (for binary string), c (for character list), r (for regular expression), or w (to produce a list split into words by whitespace). If the letter is lowercase, then interpolation and escaping happen as usual. If the letter is uppercase (S, C, R, or W), then the string is created exactly as shown, with no escaping or interpolation. After the letter, you can use any nonalphanumeric character, not just quotes, to start and end the string.

This sounds complicated, but it works pretty easily. For example, if you needed to create a string that contained escapes that some other tool was going to process, you might write:

iex(5)> pass_through = ~S"This is a {#msg}, she said.\n This is only a {#msg}."

"This is a {#msg}, she said.\\n This is only a {#msg}."

iex(6)> IO.puts(pass_through)

This is a {#msg}, she said.\n This is only a {#msg}.

:ok

Elixir also offers w and W, for lists of words. This sigil takes a binary string and splits it into a list of strings separated by whitespace:

iex(1)> ~w/Elixir is great!/

["Elixir", "is", "great!"]

NOTE

You can also create your own sigils for your own formats. See the Elixir website for more on these possibilities.

Asking Users for Information

Many Elixir applications run kind of like wholesalers — in the background, providing goods and services to retailers who interact directly with users. Sometimes, however, it’s nice to have a direct interface to code that is a little more customized than IEx command line. You probably won’t write many Elixir applications whose primary interface is the command line, but you may find that interface very useful when you first try out your code. (Odds are good that if you’re working with Elixir, you don’t mind using a command-line interface, either.)

You can mix input and output with your program logic, but for this kind of simple facade, it probably makes better sense to put it in a separate module. In this case, the ask module will work with the drop module from Example 3-8.

NOTE

Erlang’s io functions for input have a variety of strange interactions with the Erlang shell, as discussed in the following section. You will have better luck working with them in other contexts.

Gathering Characters

The IO.getn function will let you get just a few characters from the user. This seems like it should be convenient if, for example, you have a list of options. Present the options to the user, and wait for a response. In Example 5-1, which you can find at ch05/ex1-ask, the list of planemos is the option, and they’re easy to number 1 through 3. That means you just need a single-character response.

Example 5-1. Presenting a menu and waiting for a single-character response

defmodule Ask do

def chars() do

IO.puts(

"""

Which planemo are you on?

1. Earth

2. Moon

3. Mars

"""

)

IO.getn("Which? > ")

end

end

Most of that is presenting the menu. The key piece is the IO.getn call at the end. The first argument is a prompt, and the second is the number of characters you want returned, with a default value of 1. The function still lets users enter whatever they want until they press Enter, but it will tell you only the first character (or however many characters you specified), and it will return it as a string:

iex(9)> c("ask.ex")

[Ask]

iex(10)> Ask.chars

Which planemo are you on?

1. Earth

2. Earth's Moon

3. Mars

Which? > 3

"3"

iex(11)>

nil

iex(12)>

The IO.getn function returns the string "3", the character the user entered, after pressing Enter. However, as you can tell by the nil and the duplicated command prompt, the Enter still gets reported to IEx. This can get stranger if users enter more content than is needed:

iex(13)> Ask.chars

Which planemo are you on?

1. Earth

2. Earth's Moon

3. Mars

Which? > 2222222

"2"

iex(14)> 222222

222222

iex(15)>

There may be times when IO.getn is exactly what you want, but odds are good, at least when working within IEx, that you’ll get cleaner results by taking in a complete line of user input and picking what you want from it.

Reading Lines of Text

Erlang offers a few different functions that pause to request information from users. The IO.gets function waits for the user to enter a complete line of text terminated by a newline. You can then process the line to extract the information you want, and nothing will be left in the buffer.Example 5-2, in ch05/ex2-ask, shows how this could work, though extracting the information is somewhat more complicated than I would like.

Example 5-2. Collecting user responses a line at a time

defmodule Ask do

def line() do

planemo=get_planemo()

distance=get_distance()

Drop.fall_velocity(planemo, distance)

end

defp get_planemo() do

IO.puts(

"""

Which planemo are you on?

1. Earth

2. Earth's Moon

3. Mars

"""

)

answer = IO.gets("Which? > ")

value=String.first(answer)

char_to_planemo(value)

end

defp get_distance() do

input = IO.gets("How far? (meters) > ")

value = String.strip(input)

binary_to_integer(value)

end

defp char_to_planemo(char) do

case char do

"1" -> :earth

"2" -> :moon

"3" -> :mars

end

end

end

To clarify the code, the line function just calls three other functions. It calls get_planemo to present a menu to the user and get a reply, and it similarly calls get_distance to ask the user the distance of the fall. Then it calls Drop.fall_velocity to return the velocity at which a frictionless object will hit the ground when dropped from that height at that location.

The get_planemo function uses IO.puts and a multiline string to present information and an IO.gets call to retrieve information from the user. Unlike IO.getn, IO.gets returns the entire value the user entered as a string, including the newline, and leaves nothing in the buffer:

defp get_planemo() do

IO.puts(

"""

Which planemo are you on?

1. Earth

2. Earth's Moon

3. Mars

"""

)

answer = IO.gets("Which? > ")

value=String.first(answer)

char_to_planemo(value)

end

The last two lines process the result. The only piece of the response that matters to this application is the first character of the response. The easy way to grab that is with the built-in function String.first, which pulls the first character from a string.

The Drop.fall_velocity function won’t know what to do with a planemo listed as 1, 2, or 3; it expects an atom of :earth, :moon, or :mars. The get_planemo function concludes by returning the value of that conversion, performed by the char_to_planemo function:

defp char_to_planemo(char) do

case char do

"1" -> :earth

"2" -> :moon

"3" -> :mars

end

end

The case statement matches against the string. The atom returned by the case statement will be returned to the get_planemo/0 function, which will in turn return it to the line/0 function for use in the calculation.

Getting the distance is somewhat easier:

defp get_distance() do

input = IO.gets("How far? (meters) > ")

value = String.strip(input)

String.to_integer(value)

end

The input variable collects the user’s response to the question, “How far?” The processing for value uses String.strip to remove any surrounding whitespace from input, including the newline at the end. Finally, the binary_to_integer function extracts an integer from value. Using binary_to_integer isn’t perfect, but for these purposes, it’s probably acceptable.

A sample run demonstrates that it produces the right results given the right input:

iex(1)> c("ask.ex")

[Ask]

iex(2)> c("drop.ex")

[Drop]

iex(3)> Ask.line

Which planemo are you on?

1. Earth

2. Earth's Moon

3. Mars

Which? > 1

How far? (meters) > 20

19.79898987322333

iex(4)> Ask.line

Which planemo are you on?

1. Earth

2. Earth's Moon

3. Mars

Which? > 2

How far? (meters) > 20

8.0

Chapter 10 will return to this code, looking at better ways to handle the errors users can provoke by entering unexpected answers.