I wanted to try convert Peter Norvigs' Python Scheme interpreter to C++. I had tried this a few years before and failed abysmally but I saw that the latest C++ standard includes several features which might make things easier so I tried again. The tokenizing and parsing bits were easy but I struggled for a while with eval() until I had an epiphany and then that was straightforward to implement too.
I have several more features to add before the program is equivalent to the python version (e.g. a REPL, if, quote, set! and lambda, the rest of the standard environment) but I have enough to run the example program Norvig gives so I thought it would be an appropriate juncture to ask for a critique. Apart from the standard issues of style etc. there are some specific questions I'd like to ask.
Is std::list the best choice for the List datatype? There are a couple of places where using operator[] would make the code a little easier to read but std::list doesn't provide that. On the other hand it seems that e.g std::deque or std::vector use more memory/have other features I don't need.
Am I using std::any correctly? All that typeid-ing and casting seems a bit clunky to me. At least I wish there was some way to add a "tag" more comprehensible than the gibberish the compile provides as a type name. Would I be better off in the long run providing my own class to hold Scheme data types (perhaps a wrapper around std::any?) instead of using raw std:any?
builtin functions in the Python version are very elegant thanks to that languages lambda operator. "Well C++ has lambdas now" I thought but I ran into some difficulty. It is easy to stuff a lambda into a std::any but getting it back out is another matter entirely. I think it is because each lambda gets a unique and essentially random type name so we won't know what to std::any_cast it to. Is this correct? My workaround was to wrap lambdas in a std::function which does have an identifiable type name. Is this the right way to do it?
This one is minor but is M_PI standard C++ or not? It seems not to be but if you include cmath, g++ defines it without giving a warning.
#include <any>
#include <cmath>
#include <functional>
#include <iostream>
#include <list>
#include <sstream>
#include <stdexcept>
#include <string>
#include <typeinfo>
#include <unordered_map>
using Symbol = std::string;
using Expression = std::any;
using List = std::list<Expression>;
using Environment = std::unordered_map<Symbol, Expression>;
using Function = std::function<Expression(List&, Environment&)>;
Expression atom(const std::string& token) {
try {
auto result = stol(token);
return result;
}
catch (std::invalid_argument) {
try {
auto result = stod(token);
return result;
}
catch (std::invalid_argument) {
return token;
}
catch (std::out_of_range& e) {
throw e;
}
}
catch (std::out_of_range) {
throw std::runtime_error(token + " is out of range");
}
}
Expression read_from_tokens(std::list<std::string>& tokens) {
if (tokens.size() == 0) {
throw std::runtime_error("unexpected EOF");
}
auto token = tokens.front();
tokens.pop_front();
if (token == "(") {
List L;
while (tokens.front() != ")") {
L.push_back(read_from_tokens(tokens));
}
tokens.pop_front(); // pop off ')'
return L;
} else if (token == ")") {
throw std::runtime_error("unexpected )");
} else {
return atom(token);
}
}
const std::list<std::string> tokenize(const std::string& str) {
std::string replaced;
for (auto& c: str) {
switch(c) {
case '(':
replaced.append(" ( ");
break;
case ')':
replaced.append(" ) ");
break;
default:
replaced.append(1, c);
}
}
std::istringstream stream(replaced);
std::list<std::string> tokens;
std::string token;
while (stream >> token) {
tokens.push_back(token);
}
return tokens;
}
Expression parse(const std::string& str) {
auto tokens = tokenize(str);
return read_from_tokens(tokens);
}
Expression eval(Expression exp, Environment& env) {
auto& type = exp.type();
try {
if (type == typeid(Symbol)) {
auto symbol = std::any_cast<Symbol>(exp);
try {
return env.at(symbol);
} catch (std::out_of_range&) {
throw std::runtime_error(symbol + " is undefined");
}
} else if (type == typeid(double)) {
return std::any_cast<double>(exp);
} else if (type == typeid(long)) {
return std::any_cast<long>(exp);
} else if (type == typeid(List)) {
auto list = std::any_cast<List>(exp);
auto proc = std::any_cast<Symbol>(list.front());
list.pop_front();
if (proc == "define") {
auto var = std::any_cast<Symbol>(list.front());
list.pop_front();
env[var] = eval(list.front(), env);
return {};
} else {
List args;
for (auto& arg: list) {
args.push_back(eval(arg, env));
}
return
std::invoke(std::any_cast<Function>(env[proc]), args, env);
}
} else {
std::string error{exp.type().name()};
error += " is an unknown type";
throw std::runtime_error(error);
}
}
catch(std::bad_any_cast& e) {
std::string error{exp.type().name()};
error += " is the wrong type";
throw std::runtime_error(error);
}
return exp;
}
std::ostream& operator<<(std::ostream& out, const Expression& exp) {
auto& type = exp.type();
try {
if (type == typeid(Symbol)) {
out << std::any_cast<Symbol>(exp) << ' ';
} else if (type == typeid(double)) {
out << std::any_cast<double>(exp);
} else if (type == typeid(long)) {
out << std::any_cast<long>(exp);
} else if (type == typeid(List)) {
out << "[ ";
auto list = std::any_cast<List>(exp);
for (auto& item : list) {
out << item;
}
out << "] ";
} else {
std::string error{exp.type().name()};
error += " is an unknown type";
throw std::runtime_error(error);
}
}
catch(std::bad_any_cast& e) {
std::string error{exp.type().name()};
error += " is the wrong type";
throw std::runtime_error(error);
}
return out;
}
auto number(Expression& exp) {
return exp.type() == typeid(double)
? std::any_cast<double>(exp)
: exp.type() == typeid(long)
? std::any_cast<long>(exp)
: throw std::runtime_error("not a number");
}
int main () {
std::string program{"(begin (define r 10) (* pi (* r r)))"};
Environment standard_env;
standard_env["begin"] = Function([](List& args, Environment&) -> Expression{
return args.back();
});
standard_env["*"] = Function([](List& args, Environment&) -> Expression {
auto a = number(args.front());
args.pop_front();
auto b = number(args.front());
return a * b;
});
standard_env["pi"] = M_PI;
try {
std::cout << eval(parse(program), standard_env) << '\n';
}
catch (std::runtime_error& e) {
std::cerr << e.what() << '\n';
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}