Pages

Fast Functional Goats, Lions and Wolves

In a recent post, Andreas declared his love for the programming language FORTRAN. He concluded his post with the question “Can a functional language do this as well?”, where he was referring to the efficiency of his FORTRAN solution for the goats, wolves and lion problem. He followed it up with another post where her presented a more memory efficient solution that solves the goats, wolves and lion problem for an initial forest of about 6000 animals in 45 minutes. The purpose of this post is to give an answer to Andreas’ question.

To begin with, I promise that this is my last post that deals with the goats, wolves and lions problem. The problem is an interesting one in order to put different programming languages to the test. We’ll consider programming languages in our test that we use for the implementation of our products UnRisk-Q and UnRisk FACTORY:

The Wolfram Language

I gave a functional solution to the goats, wolves and lions problem in an earlier post, which dealt with construction of a functional solution using the Wolfram Language from a didactical point of view. Efficiency was not a concern. This time, we’ll develop a more efficient functional solution by using advanced features present in the Wolfram Language.

C++ 11 and Java 8

Functional programming language constructs are now being added to existing programming languages in the same way object-oriented features were introduced to procedural languages in the 1990s. The latest versions of both C++ and Java have been extended with Lambda functions and closures.

JavaScript

JavaScript has become one of the most popular programming languages due the ubiquitousness of the web. JavaScript had support for functional programming concepts from the very beginning through first-class functions and closures.

The Rules for the Test

The solution must make use of functional programming constructs available in the respective language.

The implemented solutions must be adequate to the idioms, techniques and the style of the respective language. We’ll follow the principle “when in Rome, do as the Romans do”.

The solution must not use third-party libraries.

All solutions must be cross-platform and run on Windows, Linux and OS X.

An Efficient Solution in the Wolfram Language

Since Andreas’ FORTRAN solution did not produce the devouring strategy that led to the stable forest, just the final stable forest itself, our efficient solution in the Wolfram Language will only compute the stable forests that result from an initial forest. We’ll use a vector to represent the state of a forest consisting of counters for the three different animal species:

{17, 55, 6}

We can use vector addition to compute the next state of a forest after a meal by adding {-1,-1,+1} (a wolf devours a goat), {-1,+1,-1} (a lion devours a goat) or {+1,-1,-1} (a lion devours a wolf) to a forest state vector:

In[1]:= {17, 55, 6} + {-1, +1, -1}
Out[1]= {16, 56, 5}

In the Wolfram Language a vector addition can be performed efficiently in one step on a list of vectors:

The functions ConstantArray and Thread generate lists of vectors of the same shape which can then be added with the Plus operator.

The Union takes care of eliminating duplicate forest vectors and flattening of the resulting list at the same time. The vector addition may produce invalid forest states (i.e., states where one of the animal counters drops below zero). The outermost DeleteCases uses pattern matching to filter these invalid forest states.

The remaining parts of the solution are similar to the solution from the original post:

In the C++ community there is littlelove for containers other than std::vector. So, to go with the flow, we’ll use a std::vector<forest_t> to represent the list of forest states. The C++ meal function that corresponds to Meal in the Wolfram Language then looks like this:

Because std::unique only removes consecutive duplicate elements, we have to sort the vector of forest states first in order to place identical ones next to each other. The function is an example for an application of the erase-remove idiom in C++.

Here are the C++ versions of DevouringPossible, StableForests and FindStableForests:

The private constructor and the factory method are straightforward. The hashCode, equals and toString methods are required by the contract of a value-based class. Their implementations are automatically created by the IDE.

The methods wolfDevoursGoat, lionDevoursGoat and lionDevoursWolf use the new Java 8 class Optional. The type Optional makes explicit that for a given reference we may not have a value and allows for monadic processing of the enclosed value.

The meal method of the Forest class returns all possible following forest states as a Stream. Streams are another new concept introduced with Java 8. Streams can be seen as lazily constructed Collections, where the production of the elements can be postponed until client code demands it. Furthermore a stream allows for sequential aggregate operations as shown in the implementation of the static method meal for a list of forest states:

Using a monadic flatMap operation on the stream of current forest states, first the set of possible follower forest states is computed, then duplicate forests are eliminated with a distinct filter and finally the resulting forest states are collected in a new list.

The methods devouringPossible and stableForests and findStableForests are also implemented in terms of Streams:

given times are wall-clock time measured with the shell command time and the function AbsoluteTiming for Wolfram Language code.

The following plot shows the data from the table. In this plot, the running time of the largest forest with 6000 animals has been omitted to avoid outliers.

Running Times Of Functional Solutions

By looking at the plot and the table we can draw some immediate conclusions:

Performance-wise, C++ trounces all the other programming languages in the test. The C++ program processes around 5 million forests per second on a single core, whereas the Wolfram Language program tops out at around 250 thousand forests.

There is a huge performance gap between the group of statically typed languages (C++, FORTRAN, Java) and the group of dynamically typed languages (JavaScript, Wolfram Language). The optimizations that the compiler can derive from having type information available at compile time still gives statically typed languages an edge over dynamically typed languages.

The performance of the Wolfram Language and JavaScript are roughly on par.

From the different running times, we can compute an average speedup that can be attained by moving from a higher level language to a lower level language:

From

To

Speedup

Java

C++

4.0

JavaScript

Java

6.1

Wolfram Language

Java

6.5

JavaScript

C++

22.4

Wolfram Language

C++

24.2

Of course these numbers cannot be generalized, they only apply to the tested programs. Also note that FORTRAN is not included in the list, because no sane person would switch to FORTRAN from another programming language voluntarily.

Conclusion

We can give the following answer to Andreas’ question “Can a functional language do this as well?” from this post. The computation time of all developed functional solutions is below two seconds for an initial forest of (117, 155, 106) animals. So the answer has to be “yes”.