Elm is a functional programming lanaguage, but the Elm Architecture is a cross-languages idiom or design pattern. For example, Elmification of Swift describes porting the Elm Architecture to Swift.
In the first example of the Elm Architecture, Evan Czaplicki describes creating a counter. Let's assume that we can shoehorn the Elm concept of a "module" into the C++ concept of a "class". Writing a Makefile first (or whatever your preferred build system is) is a decent way to start this kind of thing. I personally like Ake, which is similar to Make but smaller, but I'm using Make here because it's old enough that it forms a focal point.
test: counter_test
./counter_test
counter_test: counter_test.cpp counter.h counter.o
g++ counter_test.cpp counter.o -o counter_test
counter.o: counter.cpp counter.h
g++ -c counter.cpp -o counter.o
clean:
rm -f counter_test counter.o
Running Make at this point gives us No rule to make target counter_test.cpp, needed by counter_test.
So let's write a few tests. Some TDD practitioners say or imply that it's a bad thing to write several tests in a row. Actually, it's a good idea, so long as you feed those tests into the TDD cycle slowly, and are willing to replan if and when the code goes in an unanticipated direction.
Out of the few tests that I thought of, the simplest seemed to be that you can init a Counter at zero, and then view it, and it should be a string zero. So I could pull that test up and out of the others, which I might leave in a comment block. So I might have something like this:
#include "counter.h"
#include <assert.h>
int main()
{
Counter c;
assert( c.view( c.init( 0 ) ) == "0" );
/*
assert( c.view( c.init( 1 ) ) == "1" );
assert( c.view( c.update( c.increment(), c.init( 0 ) ) ) ==
c.view( c.init( 1 ) ) );
assert( c.view( 0, c.update( c.decrement(), c.init( 1 ) ) ) ==
c.view( 0, c.init( 0 ) ) );
*/
return 0;
}
Again, this is using basic C-style assert with no unit testing framework primarily because it's a focal point; I actually think that using basic C-style asserts in a main function would be fine for small investigative projects like this but probably you have your own preferred unit testing framework.
Now we get a different error message (Yay! A different error message! Progress!) from Make: No rule to make target counter.h, needed by counter_test.
. Ok, what do we need to get the test to compile?
#ifndef COUNTER_H
#define COUNTER_H
#include <string>
class Counter {
public:
typedef int Model;
Model init(int) const;
std::string view(Model) const;
};
#endif
With no members, this looks a bit like maybe it should have been a C++ namespace rather than a C++ class, but let's press on with the hypothesis that "C++ Class" is the best analog of "Elm Module"; we can revisit the hypothesis.
Implementing those two well enough to pass the one test is pretty easy:
#include "counter.h"
Counter::Model Counter::init(int x) const {
return 0; // known bad, just for TDD
}
std::string Counter::view(Counter::Model m) const {
return "0"; // known bad, just for TDD
}
Then we triangulate, adding another test: assert( c.view( c.init( 1 ) ) == "1" );
and so requiring the changed input to lead to changed output. That leads to the (possibly?) good implementation of these two functions:
#include "counter.h"
#include <sstream>
Counter::Model Counter::init(int x) const {
return x;
}
std::string Counter::view(Counter::Model m) const {
std::stringstream formatter;
formatter << m;
return formatter.str();
}
Now let's try one of the other tests.
#include "counter.h"
#include <assert.h>
int main( int argc, char* argv[] )
{
Counter c;
assert( c.view( c.init( 0 ) ) == "0" );
assert( c.view( c.init( 1 ) ) == "1" );
assert( c.view( c.update( c.increment(), c.init( 0 ) ) ) ==
c.view( c.init( 1 ) ) );
/*
assert( c.view( 0, c.update( c.decrement(), c.init( 1 ) ) ) ==
c.view( 0, c.init( 0 ) ) );
*/
return 0;
}
Now the test failure demands that we write increment and update methods on counter. I don't know what concrete type increment ought to return, so I "kick the can", postponing the decision by being vague, using a typedef and a void pointer.
#ifndef COUNTER_H
#define COUNTER_H
#include <string>
class Counter
{
public:
typedef int Model;
Model init( int ) const;
typedef void* Action;
Action increment() const;
Model update( Action, Model ) const;
std::string view( Model ) const;
};
#endif
That gets us a little further (Yay! A different error message! Progress!).
To fix the errors, we introduce an enum. Enums in C++ are not really the same thing as variants, things like type Visibility = All | Active | Completed
in Elm. They're a hacky approximation, but it's enough that we can get the tests to pass.
#include "counter.h"
#include <assert.h>
#include <sstream>
Counter::Model Counter::init(int x) const {
return x;
}
enum concrete_action_t {
INCREMENT,
DECREMENT
};
void* Counter::increment() const {
return (void*)INCREMENT;
}
Counter::Model Counter::update(Counter::Action a, Counter::Model m) const {
if (a == (void*)INCREMENT) {
return m + 1;
} else if (a == (void*)DECREMENT) {
return m - 1;
} else {
assert(0);
}
}
std::string Counter::view(Counter::Model m) const {
std::stringstream formatter;
formatter << m;
return formatter.str();
}
Now we're in the happy situation of being able to explore weird or alternative implementations while covered by tests. In order to eliminate the enum, we might try introducing function pointers, like this:
#include "counter.h"
#include <assert.h>
#include <sstream>
Counter::Model Counter::init(int x) const {
return x;
}
static Counter::Model increment_action(Counter::Model m) {
return m + 1;
}
static Counter::Model decrement_action(Counter::Model m) {
return m - 1;
}
void* Counter::increment() const {
return (void*)increment_action;
}
void* Counter::decrement() const {
return (void*)decrement_action;
}
Counter::Model Counter::update(Counter::Action a_uncast, Counter::Model m) const {
Counter::Model (*a)(Counter::Model) = a_uncast;
return a(m);
}
std::string Counter::view(Counter::Model m) const {
std::stringstream formatter;
formatter << m;
return formatter.str();
}
In compiling this, we learn that function pointers can't be stored in void pointers portably:
counter.cpp: In member function ‘Counter::Model Counter::update(Counter::Action, Counter::Model) const’:
counter.cpp:26:41: error: invalid conversion from ‘Counter::Action {aka void*}’ to ‘Counter::Model (*)(Counter::Model) {aka int (*)(int)}’ [-fpermissive]
Counter::Model (*a)(Counter::Model) = a_uncast;
^
make: *** [counter.o] Error 1
Apparently function pointers might be larger than void pointers on some machines (Harvard vs von Neumann architectures). Part of the point of TDD is that you can do TDD without really understanding all of the domain, and TDD (and version control) helps you learn those weird parts within a safety net. However, we could speculate that we're running on a machine where a void pointer is at least as wide as a function pointer if we could modify the command line arguments to the compiler.
Unfortunately, modifying the command line arguments inside of the Makefile is a terrible idea, because it circumvents Make's ability to rebuild incrementally. So in order to get a file-based (that is, visible-to-Make) expression of the logical dependencies (that the output binary depends on the flags used to compile that binary), we need to edit the Makefile:
default: test
test: counter_test
./counter_test
counter_test: compile_and_link.sh counter_test.cpp counter.h counter.o
bash compile_and_link.sh counter_test counter_test.cpp counter.o
counter.o: compile.sh counter.cpp counter.h
bash compile.sh counter.o counter.cpp
compile_and_link.sh: replace_flags.sed compile_and_link.in
sed -f replace_flags.sed compile_and_link.in >compile_and_link.sh
compile.sh: replace_flags.sed compile.in
sed -f replace_flags.sed compile.in >compile.sh
clean:
rm -f counter_test counter.o compile_and_link.sh compile.sh
So with this change, we have three new files, replace_flags.sed
is a sed script that is basically a Unix search-and-replace, switching the word FLAGS
for whatever we want. If we want to change the compile flags, then we edit replace_flags.sed. compile.in
and compile_and_link.in
are (one line) templates for (one line) shell scripts, containing the word FLAGS
. The two new rules in the makefile express how the flags (from replace_flags.sed) are combined with the templates to create shell scripts, which then can be used to, on the one hand, compile, and on the other hand, compile and link.
With this change, we can enable -fpermissive
and change the error into a warning, and indeed the tests do pass.
counter.cpp: In member function ‘Counter::Model Counter::update(Counter::Action, Counter::Model) const’:
counter.cpp:26:41: warning: invalid conversion from ‘Counter::Action {aka void*}’ to ‘Counter::Model (*)(Counter::Model) {aka int (*)(int)}’ [-fpermissive]
Counter::Model (*a)(Counter::Model) = a_uncast;
^