I'm writing an interpreter for the toy Monkey programming language, following Writing an Interpreter in Go.
In the section of the book that I'm on, we are building an evaluator for boolean expressions. The author is defining global references to some unique, constant values like TRUE, FALSE, and NULL, to avoid re-creating them each time one needs to be used.
For example, here is some Go code from the book:
(In object.go
)
const ( // [...]
BOOLEAN_OBJ = "BOOLEAN"
NULL_OBJ = "NULL"
)
type ObjectType string
type Object interface {
Type() ObjectType
}
type Boolean struct {
Value bool
}
func (b *Boolean) Type() ObjectType { return BOOLEAN_OBJ }
type Null struct{}
func (n *Null) Type() ObjectType { return NULL_OBJ }
(In evaluator.go
)
var (
NULL = &object.Null{}
TRUE = &object.Boolean{Value: true}
FALSE = &object.Boolean{Value: false}
)
And then they can be used like so:
func Eval(node ast.Node) object.Object { // [...]
case *ast.Boolean:
return nativeBoolToBooleanObject(node.Value)
// [...]
}
func nativeBoolToBooleanObject(input bool) *object.Boolean {
if input {
return TRUE
}
return FALSE
}
The author says "Now there are only two instances of object.Boolean in our package: TRUE and FALSE and we reference them instead of allocating new object.Booleans," and the same goes for NULL.
Now, here is my attempt to translate this into Rust.
(in object.rs
)
#[derive(Debug, Clone, Default)]
pub enum Object {
Integer(i64), // Adding this just to indicate we have many types of objects.
Boolean(bool),
#[default]
Null,
}
(in evaluator.rs
)
use std::sync::Arc;
use lazy_static::lazy_static;
use crate::ast::{Expression, Program, Statement};
use crate::object::Object;
pub type BoxedObject = Arc<Object>;
lazy_static! {
static ref NULL: BoxedObject = Arc::new(Object::Null);
static ref TRUE: BoxedObject = Arc::new(Object::Boolean(true));
static ref FALSE: BoxedObject = Arc::new(Object::Boolean(false));
}
fn eval_expression(expression: &Expression) -> BoxedObject {
match expression {
Expression::IntegerLiteral { value, .. } => Arc::new(Object::Integer(*value)),
Expression::Boolean { value, .. } => {
if *value {
Arc::clone(&TRUE)
} else {
Arc::clone(&FALSE)
}
_ => todo!(),
}
}
As you can see, I'm using an enum
to hold all the different object types, which seems more idiomatic than using, say, dyn Trait.
But other than that, I think my code hews closely to the original Go code.
My question is: Is using Arc<Object>
like this for unique objects holding TRUE, FALSE, and NULL the most optimal and idiomatic approach for imitating the global references we see in the Go code above? Is there a better way? This interpreter isn't going to be the most performant thing in the world, but still, I don't want to use Arc
if it's way overkill and there are better solutions, and I'm not sure if it's the best use of static variables, either. At the same time, I don't know of another way to reuse the same global TRUE, FALSE, and NULL values, and the Internet hasn't been helpful in giving solutions. (ChatGPT suggested Cow
, but I'm not sure it really understood what this code is trying to do.)