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)