DEV Community

Hercules Lemke Merscher
Hercules Lemke Merscher

Posted on • Originally published at open.substack.com

1

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.

Postmark Image

Speedy emails, satisfied customers

Are delayed transactional emails costing you user satisfaction? Postmark delivers your emails almost instantly, keeping your customers happy and connected.

Sign up

Top comments (0)

A Workflow Copilot. Tailored to You.

Pieces.app image

Our desktop app, with its intelligent copilot, streamlines coding by generating snippets, extracting code from screenshots, and accelerating problem-solving.

Read the docs