1

As a beginner in Erlang, I am working my way through the Programming Erlang book (2nd ed). I have a very hard time grasping how to store and periodically update external information (such as intermittent user input) using the principles of functional programming exclusively.

To take my present example, I am now in the beginning of the concurrent programming section (Chapter 12) where the book talks about the area server. Below is my variant of it.

As an exercise, I am trying to add to this module a way to store all the requests the user makes. But despite having a bit of experience with recursive programming the lack of mutable variables, in the sense of imperative languages, seems to be crippling in this particular instance.

I have tried looking up a few related resources on SE sites such as mutable state in functional programming and immutability in fp but it doesn't really answer my question in a practical way. I know that what I am trying to accomplish can be done by use of the ETS (or even a database), or by using the process-memory of a new process which receives and maintains the history within itself.

But what I would really like to understand (and the point of this question) is if this can be accomplished using generic functional programming principles without having to use Erlang-specific tools. The commented out lines in the code segment indicate what I am naively expecting the first steps to look like.

  -module(geometry_server4).
  -export([start/0, client/2, loop/0]).

   start() ->
       spawn(geometry_server4, loop, []).

   client(Pid_server, Geom_tuple) ->
       Pid_server ! {self(), Geom_tuple},
       %ok = storerequests(Geom_tuple),
      receive
          {area, Pid_server, Area} -> io:format("Client: Area of ~p is ~p~n", [Geom_tuple, Area]);
          {error, Error} -> io:format("~p~n", [Error])
      end.

  %storerequests(Geom_tuple) -> addtolist(Geom_tuple, get_history()).
  %
  %addtolist(Item, History) ->
  %   [Item | History].
  %get_history() -> ???


  loop() ->
      receive
          {Client, {rectangle, S1, S2}} ->
              Area = S1 * S2,
              Client ! {area, self(), Area},
              loop();
          {Client, {square, S}} ->
              Area = S * S,
              Client ! {area, self(), Area},
              loop();
          {Client, _} ->
              Client ! {error, "invalid parameters"},
              loop()
      end.

Based on the book, this toy server gets called in the terminal as:

1> c(geometry_server4).
2> P = geometry_server4:start().
3> geometry_server4:client(P, {square, 3}).
ahron
  • 803
  • 6
  • 29

1 Answers1

2

But what I would really like to understand (and the point of this question) is if this can be accomplished using generic functional programming principles without having to use Erlang-specific tools.

Yes, it can. You can use a loop variable to store what's known as the state.

First, a couple of preliminary points:

  1. Don't post code with line numbers. You want someone to be able to copy your code and paste it in their text editor and be able to run the code.

  2. In erlang, by convention you use camel case for variable names, such as ServerPid.

  3. For your own sanity, don't use module names that are more than two letters long.

  4. Consider putting all your server code in one portion of the file, and all the client code in another portion of the file. Your client code is in the middle of the server code.


-module(my).
%%-export([setup/1]).
-compile(export_all).
%%-include_lib("eunit/include/eunit.hrl").
%%

start() ->
    spawn(my, loop, [[]]).

loop(History) ->
    receive
        {Client, {rectangle, S1, S2}=Tuple} ->
            Area = S1 * S2,
            Client ! {area, self(), Area},
            loop([Tuple|History]);  %Add Tuple to the history
        {Client, {square, S}=Tuple} ->
            Area = S * S,
            Client ! {area, self(), Area},
            loop([Tuple|History]);
        {Client, history} ->
            Client ! {history, self(), History},
            loop([history|History]);
        {Client, Other} ->
            Client ! {error, self(), "invalid parameters"},
            loop([{error, Other}|History])
    end.

client(ServerPid, Req) ->
    ServerPid ! {self(), Req},
    receive
        Reply -> io:format("~p~n", [Reply])
    end.


test() ->
    ServerPid = start(),
    Requests = [
        {rectangle, 2, 3},
        {square, 4},
        history,
        "hello",
        history
    ],
    send_requests(Requests, ServerPid).

send_requests([], _) ->
    done;
send_requests([Req|Reqs], ServerPid) ->
    client(ServerPid, Req),
    send_requests(Reqs, ServerPid).

In the shell:

1> c(my).
{ok,my}

2> my:test().
{area,<0.64.0>,6}
{area,<0.64.0>,16}
{history,<0.64.0>,[{square,4},{rectangle,2,3}]}
{error,<0.64.0>,"invalid parameters"}
{history,<0.64.0>,[{error,"hello"},history,{square,4},{rectangle,2,3}]}
done

3> 
7stud
  • 46,922
  • 14
  • 101
  • 127
  • "a loop variable" - would that be similar to the loop function I use above? If yes, thanks! that seems like it could work, I'll try it soon. I'm also going to see how the loop function is actually implemented, although I doubt I'll understand it in full. I actually did split the server and client code into separate files, then brought them together in the style of the textbook and for the sake of brevity in this question. I have seen camelCase in code online but the book uses underscores, so I'm a bit confused. Apologies about the line numbers, I have fixed it now. – ahron Jul 13 '17 at 05:29
  • 2
    precisely, the loop function would accept a parameter which will be carried in every loop. – Asier Azkuenaga Jul 13 '17 at 05:34
  • @AsierAzkuenaga I had mostly seen loop functions with a receive..end block in them, but after looking around now it seems promising to try out as a real function with parameters passed to it and pattern matching! – ahron Jul 13 '17 at 05:41
  • 1
    @Yogesch, *"a loop variable" - would that be similar to the loop function I use above?*--Sorry I was working up an example for you, but I got hung up with a stupid error. – 7stud Jul 13 '17 at 06:18
  • goodness, you should so not have done that! saying to use the loop func was enough. i haven't looked at the code, cuz i am going to do it myself. but in case i am unable to figure it out, many many thanks in advance for the example code!!! – ahron Jul 13 '17 at 06:20
  • 1
    @Yogesch, That's a good way to proceed--struggling a bit always teaches you more. In functional languages, although t is said that variables are immutable, parameter variables in recursive functions are essentially mutable variables. – 7stud Jul 13 '17 at 06:25
  • 1
    @Yogesch, Over the past few months, I've been slowly working my way through Programming Erlang as well, and I've been posting my answers to the exercises on github [here](https://github.com/7stud/Programming-Erlang-Exercises-Solutions-Answers). I'm currently stuck on Chapter 18/Websockets because the code in the book doesn't work anymore. – 7stud Jul 13 '17 at 06:31
  • 1
    @Yogesch, *I have seen camelCase in code online but the book uses underscores, so I'm a bit confused.*--You'll have to post some page numbers where you think that is occurring. Function names use lower *snake case*, and atoms are usually lower case snake case as well. – 7stud Jul 13 '17 at 06:45
  • 2
    @Yogesch, Also, if you have the time you should check out the free online erlang courses at https://www.futurelearn.com/. They are taught by Simon Thompson who wrote Erlang Programming, and they are excellent. You will learn a ton. I suggest that you take the two courses in order (Functional followed by Concurrent), but the Functional class is currently running, and the Concurrent course is starting in a few weeks. They have changed the way things work over there, so I think you get one free course where you can take the tests, but for the second course you have to pay to take the tests. – 7stud Jul 13 '17 at 06:53
  • @7stud sorry, it was my turn to go off and think about it a bit. i have got the idea of the loop function with made up some examples and next up is figuring out a way to simulate mutable variables with it (without looking at code samples / algorithm ideas of course ;) – ahron Jul 13 '17 at 07:07
  • @7stud You're right, it is indeed camel case for variable names and lower snake case for atoms and function names. I guess I just used a single made-up convention for both function and variable names. I'll try to fix it. – ahron Jul 13 '17 at 07:08
  • @7stud Thanks a ton for the links to the courses. I'll make the time to check them out. Hope you get through Chapter 18 (and the rest)! – ahron Jul 13 '17 at 07:14
  • @Yogesch, You're welcome! When you get sick of typing in the shell, check out what I started doing [here](https://github.com/7stud/Programming-Erlang-Exercises-Solutions-Answers/blob/master/Chapter%2013/_running_programs.md). – 7stud Jul 13 '17 at 07:29
  • @7stud So....I get it at long last. With a bit of help from your code :) – ahron Jul 13 '17 at 10:15
  • @7stud Though it now occurs to me that what is being done is actually taking advantage of the in-process memory, in this case, of the spawned loop function. This seems to be Erlang-specific (spawning processes), how can this be done otherwise, for example in a hypothetical functional language, where threads/processes cannot be spawned so easily? Or in this case, outside of the message-passing paradigm of Erlang.. – ahron Jul 13 '17 at 10:24
  • 1
    @Yogesch, The reason the erlang loop preserves state is because the receive blocks and waits for messages. I don't think Haskell has any such mechanism to pause a loop. In a pure functional language, you can't even write to a database or save to a file because that is a side effect. You would need to use ruby or python to call the pure function then save the result. For a functional language to be practical it has to make compromises, and erlang was designed to be practical. – 7stud Jul 14 '17 at 01:16
  • @7stud sorry was held up with stuff. I recall having read a bit about Haskell a while back and how it needs monads and other things to handle i/o, file ops, etc. It actually makes sense now and your explanation helps greatly. A purely functional program is indeed just a series/layers of functions of processes, and hence externalities need to be handled specially. Thanks a lot for helping understand this better :-) – ahron Jul 15 '17 at 12:19