Fork me on GitHub
#code-reviews
<
2020-06-17
>
Chase15:06:05

Would anyone mind taking a quick look at my small temperature converter program? The actual "business logic" is only a couple of lines but I'm more curious on how you would approach taking in the user input and validating it. That takes so much more code. It's still half the LOC of my Rust implementation of the same exercise. https://github.com/Chase-Lambert/temperature-converter/blob/master/src/temperature_converter/core.clj

noisesmith15:06:15

I see multiple places where you can move forms to eliminate duplicate code

noisesmith15:06:26

never use a namespace by full name - always require and provide an alias

Chase15:06:50

even if only using once huh? Ok, ok, I can dig it.

noisesmith15:06:52

some clojure environments preload clojure.string, but that's not a guarantee that clojure makes, and your code can easily break if you don't require it

noisesmith15:06:51

dedupe example: change

(if (= x "y")
  (println "foo" (frob x) "y")
  (println "foo" (grub x) "z"))
vs.
(println "foo"
         (if (= x "y") (frob x) (grub x))
         x)

noisesmith15:06:17

also at the end you can replace (if cond? (do (f) (recur)) (recur)) with (when cond? (f)) (recur) since let already has a do block

Chase15:06:22

Ahh! You mean when is like having a do block?

noisesmith15:06:56

it is, but what I mean, is that both branches of if called recur, so you can take the recur out of the if

noisesmith15:06:09

and let already has a do block, so you can just put the recur after the if

noisesmith15:06:18

and an if with only one branch can be replaced with when

noisesmith15:06:25

(I hope that shows all my work...)

Chase15:06:38

Ok, I get it. Thanks so much!

noisesmith15:06:47

some more suggestions: in clojure 1.0 is a double, not float, and there are few reasons for using Float/parseFloat instead of Double/parseDouble

noisesmith15:06:00

all clojure math functions already promote float to double as well

noisesmith15:06:48

(drop-last temp) - using collection operations on strings works in simple examples like this, but it's a code smell and extremely inefficient

noisesmith16:06:10

also why are you even dropping anything from what you read?

Chase16:06:37

Well the user input comes in like "100C" so I want to split that into it's two relevant parts

noisesmith16:06:43

oh! you are extracting a digit

Chase16:06:43

How would you approach parsing that string?

noisesmith16:06:06

(ins)user=> (re-matches #"(\d+)(.)" "100C")
["100C" "100" "C"]

noisesmith16:06:33

with a little fiddling that also helps you handle optional whitespace in a human friendly way

Chase16:06:43

ahhh, ok. I remember doing something like that on an exercism problem I did back in the day. Regex's are still difficult for me.

noisesmith16:06:23

fixed - yea res are weird

(ins)user=> (re-matches #"([\d\.]+)(.)" "100.3C")
["100.3C" "100.3" "C"]

Chase16:06:23

so then in my let block I could use destructuring to bind to degrees and scale like [[_ degrees scale] (re-matches ...)] and continue on with my try/catch checks?

noisesmith16:06:38

right - that sounds correct to me

Chase16:06:51

but what if the re-matches fails because they didn't input it right... hmmm

noisesmith16:06:14

also (or (= x y) (= x z)) can be replaced by (#{y z} x) if both y and z are non nil and non-false

noisesmith16:06:39

@chase-lambert in that case, re-matches returns nil, the destructure offers nil, and the behavior is the same as your current code

Chase16:06:53

oh perfect, which they should be since they are at the end of the and statement that already checks if they are nil and false

noisesmith16:06:02

oh I meant (and degrees scale (or (= scale "C") (= scale "F"))) can be (and degrees (#{"C" "F"} scale))

noisesmith16:06:12

but maybe that's what you meant too :D

Chase16:06:47

yup except I still was double checking that scale was there. haha. this is great

noisesmith16:06:16

right - the check that scale is either "C" or "F" already checks that it wasn't nil or false, implicitly

noisesmith16:06:24

I should do my refactor suggestions in smaller steps

noisesmith16:06:44

@chase-lambert often a clojure refactor feels like solving a sudoku or something

noisesmith16:06:12

you should notice your code becoming smaller and easier to understand at each step

Chase16:06:17

I just watched a great sudoku solving video, you might have seen the same.

noisesmith16:06:29

oh the one with the knights moves?

noisesmith16:06:01

yeah, that sort of puzzle solving and simplifying clojure code scratch the same spot in my brain

Chase16:06:41

I like all the changes but the regex still concerns me. I can see what it does but could I recreate it myself. Could you parse your regex into an English sentence for me? "Ok, so once you hit a digit, divide this into blah blah blah"?

Chase16:06:24

I definitely felt icky doing it my way with the drop-last and such though. I just sensed that it wasn't efficient.

noisesmith16:06:22

find 1 or more of "digit or .", followed by a single token

noisesmith16:06:58

[] - group, matches any item in the group \d - any digit \. - matches ".", since . is special + - one or more of preceding . - any token () - another kind of group - return string matched by this group separately

noisesmith16:06:52

I think that covers it

Chase16:06:00

Ahh ok, I'm actually writing these in a notebook by hand right now. Haha. Old school

noisesmith16:06:05

one more refactor: since (System/exit 0) already causes the rest of the function body to be skipped, you can remove the let from the if, and reducing nesting makes things more readable

noisesmith16:06:59

(if cond?
  (exit)
  (f)) 
;; vs
(when cond? (exit))
(f) 
those both dothe same thing

Chase16:06:01

ok, ok. so then my code would be

(let [temp (read-line)] 
  (when blah blah) 
  (let [[_ degrees scale blah blah 
So just taking out one level of nesting

noisesmith16:06:10

the same thing I suggested with pulling the two recur calls out of the if, I think

Chase16:06:04

Good stuff. Ironically it all ends up at the same LOC but much cleaner. Did I misinterpret anything? https://github.com/Chase-Lambert/temperature-converter/blob/master/src/temperature_converter/core.clj

noisesmith16:06:19

I think the "F" and "C" in the conert-temp defn should be moved out and just be scale as an arg to str

Chase16:06:52

But it's gotta be the opposite scale

Chase16:06:02

I'm printing "32F = 0.00C"

noisesmith16:06:03

oh! I missed that

noisesmith16:06:06

I'd use ({"C" "F" "F" "C"} scale) and call that inside str, instead of hard-coding

noisesmith16:06:24

and using an extra arg to format like that is odd, I'm surprised it works

noisesmith16:06:32

otherwise yes, that looks right - simplified significantly even if the line count hasn't changed

noisesmith16:06:41

my repl shows that the format call doesn't break but the C or F is silently ignored

noisesmith16:06:00

of course instead of the lookup above, the F or C could be inside the format string

Chase16:06:18

I'm just not sure on how I want to format the expression now. Whenever I through an if statement or whatever into the middle of a string formation I start to struggle to see what is happening. But this seems funky too:

(defn convert-temp [degrees scale]                                                
  (str degrees                                                                    
       scale                                                                      
       " = "                                                                      
       (if (= scale "C")                                                          
         (format "%.2f" (+ 32.0 (* degrees 1.8)))                                 
         (format "%.2f" (/ (- degrees 32.0) 1.8)))                                
       ({"C" "F" "F" "C"} scale))) 

Chase16:06:20

Maybe a little less vertical:

(defn convert-temp [degrees scale]                                                
  (str degrees scale " = "                                                        
       (if (= scale "C")                                                          
         (format "%.2f" (+ 32.0 (* degrees 1.8)))                                 
         (format "%.2f" (/ (- degrees 32.0) 1.8)))                                
       ({"C" "F" "F" "C"} scale)))

Chase16:06:48

I think my original might still be cleaner because it's such a simple either/or conversion

noisesmith16:06:31

yeah - that's definitely funky

noisesmith16:06:36

(let [degrees (if (= scale "C") ...)
      degree-string (format ...)
      degree-indicator ({"C" "F" "F" "C"} scale)]
   (str degrees scale " = " degree-string degree-indicator))

noisesmith16:06:40

something like that

noisesmith16:06:03

also, if you held onto the original read-in line, you could use it in your output

noisesmith16:06:13

instead of piecing it back together manually

Chase16:06:43

ooh, true. Waste not

noisesmith16:06:49

oh - btw. functions act as recur targets, so you can literally just remove the (loop [] ...) and replace it with the ... that was in it, without changing the behavior

noisesmith16:06:11

almost forgot to mention that one

Chase16:06:12

but won't I re-print my beginning messages that way?

noisesmith16:06:25

oh right, never mind then

Chase16:06:41

Yup. I thought of that one originally. Thanks for all your help!

Chase16:06:34

I'm hooked on this code reviewing so you might have awakened a dragon here. Haha. I'm just solo learning so this slack is where it's at.