Chapter 5. Communicating with Humans

Erlang’s origins in telecom switching have left it with a fairly minimal set of tools for communicating with people, but there’s enough there to do worthwhile things. You’ve already used some of it (io:format/1 and io:format/2), 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 the Erlang shell.

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 assemble sentences or even presenting information, you’ll want something more flexible. Strings, sequences of characters, are the structure you need. You’ve already used strings a little bit, as the double-quoted arguments to io:format in Chapter 4:

io:format("Look out below!~n") ;

The double-quoted content (Look out below!~n) 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 ". To include a backslash, you have to use \, and Appendix A includes a complete list of escapes and other options. If you create a string in the shell, Erlang will report back the string with the escapes. To see what it is meant to contain, use io:format:

1> X = "Quote -  " in a string. 
  Backslash, too: \ . 
".
"Quote -  " in a string. 
  Backslash, too: \ ."
2> io:format(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, the Erlang shell 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.

Erlang development hasn’t focused heavily on text historically, but if your programs involve sharing information with humans, you’ll want to get familiar with how to get information into and out of strings. This is an area where you may want to spend a fair amount of time in the shell playing with different tools.

Note

Technically, strings don’t really exist as a type in Erlang because strings are lists of characters. Thinking about strings as lists of characters, however, is useful in only a few situations, typically where you want to process a string from start to end. You’ll learn about lists in Chapter 6, and the code in this chapter will have to use a list built-in function, but for now you should just think about string operations rather than lists.

The simplest approach, usually, is concatenation, where you combine two strings into one. Erlang offers two easy ways to do this. The first uses the ++ operator:

1> "erl" ++ "ang".
"erlang"
2> A="ang".
"ang"
3> "erl" ++ A.
"erlang"

The other approach uses an explicit string:concat/2 function:

4> string:concat("erl", "ang").
"erlang"
5> N="ang".
"ang"
6> string:concat("erl", N).
"erlang"

The ++ operator is usually more convenient because it lets you work with more than two arguments without nesting functions.

Note

Erlang has a shortcut where you can concatenate two strings just by putting them next to each other: "erl" "ang" will end up as "erlang". However, if you try to mix variables into that, you’ll get a syntax error. This shortcut is of limited value except maybe when you’re cutting and pasting quoted values as you’re writing your code, and doesn’t work in every context.

Erlang also offers three options for comparing string equality, the == operator, the =:= (exact equality) operator, and a string:equal/2 function. The == operator is generally the simplest for this, though the others produce the same results:

7> "erl" == "erl".
true
8> "erl" == "ang".
false
9> G ="ang".
"ang"
10> G == "ang".
true

Erlang 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 re (regular expressions) Erlang modules. If the strings you want to work with represent file or directory names, definitely explore the filename module. If you need to perform Unicode-encoding conversion on Erlang strings, you’ll also want to explore the unicode module. (By default, Erlang represents characters using UTF-8 values.)

Note

I wrote a single wrapper module that assembles Erlang’s tools for working with strings into one place. For more, visit https://github.com/simonstl/erlang-simple-string.

Asking Users for Information

Many Erlang 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 the Erlang console. You probably won’t write many Erlang 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 Erlang, 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 input 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 Terms

The simplest way to build an interface—an interface probably just for programmers—is to create a way for users to enter Erlang terms using io:read/1. This lets users enter a complete Erlang term—an atom, number, or tuple, for example. An initial version of this might look like Example 5-1, which you can find in ch05/ex1-ask.

Example 5-1. Asking the user for an Erlang term
-module(ask).
-export([term/0]).

term() ->
  Input = io:read("What {planemo, distance} ? >>"),
  Term = element(2,Input),
  drop:fall_velocity(Term).

The Input variable will be set by the call to io:read/1, getting an Erlang term. If all goes well, it will contain a tuple like {ok,{mars,20}}, where the first value is ok and the second value of the tuple is the term the user entered. Extracting that value—in this case a tuple—requires a call to the element/2 method. Finally, the code calls the drop:fall_velocity method with that value.

Note

If you wanted, you could cram that all into one line as term() -> drop:fall_velocity(element(2,io:read("What {planemo, distance} ? >>")))., but that’s both hard to read and hard to modify.

For your own use, this could be perfectly fine. A simple session might look like the following:

1> c(drop).
{ok,drop}
2> c(ask).
{ok,ask}
3> ask:term().
What {planemo, distance} ? >>{mars,20}.
12.181953866272849

If you leave off the period at the end of the term, Erlang will repeat the prompt but not show where you were, trusting you to read the line above. Also, the things you enter at an io:read/1 prompt become part of the console’s command history, and you can repeat them with the up arrow. (These issues are interactions with the Erlang shell, not issues with the function itself.)

Things can get weird quickly, however, if the user enters unexpected terms—a number instead of a tuple, say—or broken terms, with bad syntax:

4> ask:term().
What {planemo, distance} ? >>20.
** exception error: no function clause matching
          drop:fall_velocity(20) (drop.erl, line 4)
5> ask:term().
What {planemo, distance} ? >>.
** exception error: no function clause matching
            drop:fall_velocity({1,erl_parse,
            ["syntax error before: ","'.'"]}) (drop.erl, line 4)

In both cases, passing the extracted Term directly to fall_velocity/1 is a bad idea. In the first case, it’s because fall_velocity/1 expects a tuple, not a bare number. In the second case, fall_velocity/1 has a similar problem, but it’s being sent an error message, not a term it can process. Example 5-2, in ch05/ex2-ask, shows a better way to handle these kinds of problems. It gives the user a direct error message when it encounters the wrong type of information or broken information. (It also uses pattern matching instead of element/2.)

Example 5-2. Asking the user for an Erlang term and handling bad results
-module(ask).
-export([term/0]).

term() ->
  Input = io:read("What {planemo, distance} ? >>"),
  process_term(Input).

process_term({ok, Term}) when is_tuple(Term) -> drop:fall_velocity(Term);

process_term({ok, _}) -> io:format("You must enter a tuple.~n");

process_term({error, _}) -> io:format("You must enter a tuple with correct syntax.~n").

This doesn’t solve every possible problem. Users could still enter tuples with the wrong content, and drop:fall_velocity will report an error. Chapter 9 will explore how to address that problem in much greater detail.

When you go to the trouble of building this kind of interface, however, it’s probably not because typing ask:term() is shorter than typing drop:fall_velocity. Odds are good that you want to try a number of values and possibilities, so you want the question repeated. Example 5-3, in ch05/ex3-ask, presents the result of a (correctly formatted) call to the user and then calls term() again, setting up a recursive loop. (It also offers a nice way to exit the loop.)

Example 5-3. Asking the user for an Erlang term and handling bad results
-module(ask).
-export([term/0]).

term() ->
  Input = io:read("What {planemo, distance} ? >>"),
  process_term(Input).

process_term({ok, Term}) when is_tuple(Term) ->
  Velocity = drop:fall_velocity(Term),
  io:format("Yields ~w. ~n",[Velocity]),
  term();

process_term({ok, quit}) ->
  io:format("Goodbye.~n");
  % does not call term() again

process_term({ok, _}) ->
  io:format("You must enter a tuple.~n"),
  term();

process_term({error, _}) ->
  io:format("You must enter a tuple with correct syntax.~n"),
  term().

When you compile the ask module and call ask:term/0, you’ll see the question repeated as long as you keep entering appropriate tuples. To break out of that loop, just enter the atom quit followed by a period.

6> c(ask).
{ok,ask}
7> ask:term().
What {planet, distance} ? >>{mars,20}.
Yields 12.181953866272849.
What {planet, distance} ? >>20.
You must enter a tuple.
What {planet, distance} ? >>quit.
Goodbye.
ok

Gathering Characters

The io:get_chars/2 function will let you get just a few characters from the user. This would be convenient if, for example, you have a list of options. Present the options to the user, and wait for a response. In this example, the list of planemos is the option, and they’re easy to number 1 through 3, as shown in the code for Example 5-4, which you can find at ch05/ex4-ask. That means you just need a single character response.

Example 5-4. Presenting a menu and waiting for a single-character response
-module(ask).
-export([chars/0]).

chars() ->
  io:format("Which planemo are you on?~n"),
  io:format(" 1. Earth ~n"),
  io:format(" 2. Earth's Moon~n"),
  io:format(" 3. Mars~n"),
  io:get_chars("Which? > ",1).

Most of that is presenting the menu, and you could combine all of those io:format/1 calls into a single call if you wanted. The key piece is the io:get_chars/2 call at the end. The first argument is a prompt, and the second is the number of characters you want returned. The function still lets users enter whatever they want until they press Enter, but it will tell you only the first of however many characters you specified.

1> c(ask).
{ok,ask}
2> ask:chars().
Which planemo are you on?
 1. Earth
 2. Earth's Moon
 3. Mars
Which? > 3
"3"
3>
3>

After the user hits Enter, the io:get_chars function returns the string "3", the character the user entered. However, as you can tell by the duplicated command prompt, the Enter still gets reported to the Erlang shell. This can get stranger if users enter more content than is needed:

4> ask:chars().
Which planemo are you on?
 1. Earth
 2. Earth's Moon
 3. Mars
Which? > 222222
"2"
5> 22222
5>

There may be times when io:get_chars is exactly what you want, but odds are good, at least when working within the shell, 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:get_line/1 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-5, in ch05/ex5-ask, shows how this could work, though extracting the information is somewhat more complicated than I would like.

Example 5-5. Collecting user responses a line at a time
-module(ask).
-export([line/0]).

line() ->
  Planemo = get_planemo(),
  Distance = get_distance(),
  drop:fall_velocity({Planemo, Distance}).


get_planemo() ->
  io:format("Where are you?~n"),
  io:format(" 1. Earth ~n"),
  io:format(" 2. Earth's Moon~n"),
  io:format(" 3. Mars~n"),
  Answer = io:get_line("Which? > "),

  Value = hd(Answer),
  char_to_planemo(Value).

char_to_planemo(Char) ->
  if
    [Char] == "1" -> earth;
    Char == $2 -> moon;
    Char == 51 -> mars
  end.

get_distance() ->
  Input = io:get_line("How far? (meters) > "),
  Value = string:strip(Input, right, $
),
  {Distance, _} = string:to_integer(Value),
  Distance.

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

The get_planemo/0 function is a combination of io:format/1 calls to present information and an io:get_line/1 call to retrieve information from the user. Unlike io:get_chars/1, io:get_line/1 returns the entire value the user entered, including the newline, and leaves nothing in the buffer.

get_planemo() ->
  io:format("Where are you?~n"),
  io:format(" 1. Earth ~n"),
  io:format(" 2. Earth's Moon~n"),
  io:format(" 3. Mars~n"),
  Answer = io:get_line("Which? > "),

  Character = hd(Answer),
  char_to_planemo(Character).

The last two lines are the actual string processing. The only piece of the response that matters to this application is the first character of the string. The easy way to grab that is with the built-in function hd/1, which pulls the first item from a string or list.

Note

Because strings are really lists of numbers, you could instead call lists:nth(1, Answer). The first argument, 1, is the position you want to retrieve, and the second argument, Answer, is the list, in this case a string, from which you want to retrieve it. For this function, the first character in an Erlang string is in position 1, not 0 as in many other languages. That makes the function name nth make sense when it’s time to retrieve the 4th, 5th, 6th, and so on values.

The drop:fall_velocity/1 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/0 function concludes by returning the value of that conversion, performed by the char_to_planemo/1 function:

char_to_planemo(Char) ->
  if
    [Char] == "1" -> earth;
    Char == $2 -> moon;
    Char == 51 -> mars
  end.

The if statement shows three different ways of testing the character. If you prefer to evaluate the character as text, you can put square brackets around it and compare it to a string, like "1" here. You can also test against Erlang’s character notation, in which $2 is the value for the character two. Finally, if you’re comfortable with character values, you can compare it to those values, like 51, which corresponds to the character 3. 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.

You could also rewrite that function to skip the case statement and just use pattern matching:

char_to_planemo($1) -> earth;
char_to_planemo($2) -> moon;
char_to_planemo($3) -> mars.
Note

Erlang’s character notation understands Unicode as well. If you try $☃, the Unicode Snowman, Erlang will understand that it is character 9731, hex 2603. It also understands Emoji characters from Unicode’s Astral Plane, which are often difficult for simple Unicode implementations.

Getting the distance is somewhat easier:

get_distance() ->
  Input = io:get_line("How far? (meters) > "),
  Value = string:strip(Input, right, $
),
  {Distance, _} = string:to_integer(Value),
  Distance.

The Input variable collects the user’s response to the question “How far?”, and Value strips extra noise out of that response. The string:to_integer/1 function extracts an integer from Value. The pattern match on the left grabs the first piece of the tuple it returns, which is the integer, while the underscore discards the rest of what it sends, which is anything else on the line. That will include the newline, but also any decimal part users enter. You could use string:to_float/1 for more precision, but that won’t accept an integer. Using string:to_integer/1 isn’t perfect, but for these purposes it’s probably acceptable.

Note

It isn’t necessary for this conversion, but if you just want to strip newlines out of user responses, you can use string:strip(Input, right, $ ), where Input is what just came from the user.

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

1> c(ask).
{ok,ask}
2> ask:line().
Where are you?
 1. Earth
 2. Earth's Moon
 3. Mars
Which? > 1
How far? (meters) > 20
19.79898987322333
3> ask:line().
Where are you?
 1. Earth
 2. Earth's Moon
 3. Mars
Which? > 2
How far? > 20
8.0

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

Strings are not Erlang’s strongest suit, but it has the facilities to make pretty much anything you need work. As you read the next two chapters on lists, remember that strings are actually lists of characters underneath, and you can use any of the list tools on strings.

Note

You can learn more about working with strings in Chapter 2 of Erlang Programming (O’Reilly); Sections 3.8 and 8.8 of Programming Erlang, 2nd Edition (Pragmatic); Section 2.2.6 of Erlang and OTP in Action (Manning); and Chapter 1 of Learn You Some Erlang For Great Good! (No Starch Press).

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset