DEV Community

Hercules Lemke Merscher
Hercules Lemke Merscher

Posted on • Originally published at open.substack.com

Tsonnet #4 - Refactoring numbers

Welcome to the Tsonnet series!

In the previous post, we added arithmetic operations:

The interpret_bin_op function was a bit ugly, plus we had a catch-all case on the pattern matching raising an exception for non-numeric expr.

Let's get going and wrap numerical types into a Number type -- just like JSON.

diff --git a/lib/ast.ml b/lib/ast.ml
index 326c6db..55ddd52 100644
--- a/lib/ast.ml
+++ b/lib/ast.ml
@@ -4,9 +4,12 @@ type bin_op =
   | Multiply
   | Divide

-type expr =
+type number =
   | Int of int
   | Float of float
+
+type expr =
+  | Number of number
   | Null
   | Bool of bool
   | String of string
Enter fullscreen mode Exit fullscreen mode

The lexer does not need to change. The parser only needs to wrap INT and FLOAT in a Number:

diff --git a/lib/parser.mly b/lib/parser.mly
index 7d6ea28..2b6db25 100644
--- a/lib/parser.mly
+++ b/lib/parser.mly
@@ -27,8 +27,8 @@ prog:
   ;

 expr:
-  | i = INT { Int i }
-  | f = FLOAT { Float f }
+  | i = INT { Number (Int i) }
+  | f = FLOAT { Number (Float f) }
   | NULL { Null }
   | b = BOOL { Bool b }
   | s = STRING { String s }
Enter fullscreen mode Exit fullscreen mode

The print function needs to match the Number too -- it is getting annoying, but let's roll with it for now and sort it out next.

The interpret_bin_op is where the bulk of the changes are:

diff --git a/lib/tsonnet.ml b/lib/tsonnet.ml
index 69858a3..832b56c 100644
--- a/lib/tsonnet.ml
+++ b/lib/tsonnet.ml
@@ -7,8 +7,10 @@ let parse (s: string) : expr =
   ast

 let rec print = function
-  | Int i -> Printf.sprintf "%d" i
-  | Float f -> Printf.sprintf "%f" f
+  | Number n ->
+    (match n with
+    | Int i -> Printf.sprintf "%d" i
+    | Float f -> Printf.sprintf "%f" f)
   | Null -> Printf.sprintf "null"
   | Bool b -> Printf.sprintf "%b" b
   | String s -> Printf.sprintf "\"%s\"" s
@@ -22,27 +24,33 @@ let rec print = function
     )
   | _ -> failwith "not implemented"

-let interpret_bin_op op n1 n2 =
-  let float_op =
-    match op with
-    | Add -> (+.)
-    | Subtract -> (-.)
-    | Multiply -> ( *. )
-    | Divide -> (/.)
-  in match (n1, n2) with
-  | Int i1, Int i2 -> Float (float_op (Float.of_int i1) (Float.of_int i2))
-  | Float f1, Float f2 -> Float (float_op f1 f2)
-  | Float f1, Int e2 -> Float (float_op f1 (Float.of_int e2))
-  | Int e1, Float f2 -> Float (float_op (Float.of_int e1) f2)
-  | _ -> failwith "invalid operation"
+let interpret_bin_op (op: bin_op) (n1: number) (n2: number) : expr =
+  match op, n1, n2 with
+  | Add, (Int a), (Int b) -> Number (Int (a + b))
+  | Add, (Float a), (Int b) -> Number (Float (a +. (float_of_int b)))
+  | Add, (Int a), (Float b) -> Number (Float ((float_of_int a) +. b))
+  | Add, (Float a), (Float b) -> Number (Float (a +. b))
+  | Subtract, (Int a), (Int b) -> Number (Int (a - b))
+  | Subtract, (Float a), (Int b) -> Number (Float (a -. (float_of_int b)))
+  | Subtract, (Int a), (Float b) -> Number (Float ((float_of_int a) -. b))
+  | Subtract, (Float a), (Float b) -> Number (Float (a -. b))
+  | Multiply, (Int a), (Int b) -> Number (Int (a * b))
+  | Multiply, (Float a), (Int b) -> Number (Float (a *. (float_of_int b)))
+  | Multiply, (Int a), (Float b) -> Number (Float ((float_of_int a) *. b))
+  | Multiply, (Float a), (Float b) -> Number (Float (a *. b))
+  | Divide, (Int a), (Int b) -> Number (Float ((float_of_int a) /. (float_of_int b)))
+  | Divide, (Float a), (Int b) -> Number (Float (a /. (float_of_int b)))
+  | Divide, (Int a), (Float b) -> Number (Float ((float_of_int a) /. b))
+  | Divide, (Float a), (Float b) -> Number (Float (a /. b))

-(** [interpret expr] interprets the intermediate AST [expr] into an AST. *)
+(** [interpret expr] interprets and reduce the intermediate AST [expr] into a result AST. *)
 let rec interpret (e: expr) : expr =
   match e with
-  | Null | Bool _ | String _ | Int _ | Float _ | Array _ | Object _ -> e
+  | Null | Bool _ | String _ | Number _ | Array _ | Object _ -> e
   | BinOp (op, e1, e2) ->
-    let n1, n2 = interpret e1, interpret e2
-    in interpret_bin_op op n1 n2
+    match (interpret e1, interpret e2) with
+    | (Number v1), (Number v2) -> interpret_bin_op op v1 v2
+    | _ -> failwith "invalid binary operation"

 let run (s: string) : expr =
   let ast = parse s in
Enter fullscreen mode Exit fullscreen mode

Now, I know what you're thinking -- "That's a lot of pattern matching!" and yeah, it's not the prettiest code I've ever written. But here's why it's actually pretty cool:

  1. We've isolated arithmetic operations into interpret_bin_op and got rid of the exception handling for non-numerical values
  2. The pattern matching makes the code explicit (even if verbose)
  3. Most importantly, we're following the single-responsibility principle -- our numeric operations are now completely separate from other binary operations

This is the way

This refactoring might seem like a lot of work for little gain, but trust me, it'll pay off when we start adding other binary operations like string concatenation. We can now pattern-match BinOp without touching our numeric operation logic at all. Clean separation of concerns!

In the next post, we'll tackle improving our JSON output presentation. But for now, pour yourself a drink and admire those clean-type boundaries we just created!


Thanks for reading Bit Maybe Wise! Subscribe to receive new posts about Tsonnet.

Top comments (0)