Fork me on GitHub
#beginners
<
2018-07-28
>
David Reno17:07:17

This is my first function. I’d appreciate any feedback/comments. I’d guess there’s already a library to do this but doing it for practice. Also guessing it could be shorter or more elegant. Not sure about my commenting method.

dorab17:07:41

How about

valtteri17:07:42

@dcreno congrats for your first function! 🎉 A few quick notes: - idiomatic naming for string argument is s - how do you expect the function to behave with non-string inputs, such as nil, 1, [], {:a 1}?

David Reno17:07:07

yeah, it doesn’t like nil or '1. I thought maybe that was OK as it’s documented to only take strings?

David Reno17:07:44

Maybe I should validate the input and return false if not a string?

valtteri18:07:20

I’d expect it to return false. Usually when I’m unsure how my function should behave, I test how similar functions in clojure core behave and try to make it consistent.

valtteri18:07:45

Like here you could check how string? uuid? etc. behave

valtteri18:07:28

Also, since Clojure is a lisp, you can check if number is between 0 and 255 by simply (<= 0 % 255). You can have arbitrary amount of args, so no need for and necessarily.

dorab18:07:42

Since the regex does not match the minus sign, % will always be non-negative. So, (<= 0 % 255) can be just (< % 256)

valtteri18:07:50

Also, I’d probably remove the if clause completely for simplicity

David Reno18:07:41

huh, not sure why I didn’t see that…

David Reno18:07:23

oh, wait. I put that in there to avoid the exception when there was no match, let me look again though.

David Reno18:07:02

v2 with feedback from @valtteri and @dorab

dorab18:07:11

(every? true? (map pred coll)) is pretty much the same as (every? pred coll)

dorab18:07:49

And (map f (map g coll)) is the same as (map #(f (g %)) coll) Assuming f and g have no side-effects.

David Reno18:07:40

was thinking imperatively…

seancorfield19:07:13

Also the same as (map (comp f g) coll)

dorab18:07:41

One of the benefits of side-effect-free functions is that you can have rules like those above that you can apply with full confidence that they will not change the semantics of your code.

David Reno18:07:45

v3 with more feedback. Still not sure about removing an if…seems to break the case of (ipv4? "")

dorab18:07:01

Take a look at if-let and see if that helps you make the code shorter.

valtteri18:07:37

Check also not-empty

dorab18:07:42

Idiomatic Clojure typically uses (if (seq something-that-could-be-nil-or-a-seq) ...) to check.

David Reno18:07:42

seq and not-empty seem very similar except one works on seq and the other on coll. I guess seq is the more general and thus preferred?

dorab02:07:08

On second thoughts, what I said is true, but not germane to this discussion.

sundarj18:07:28

could probably do (if-let [match (next (re-matches ,,,))])

David Reno18:07:59

thinking about the diff between rest and next now…thought it would be the same in this situation but unsure about the return value when empty.

sundarj18:07:40

(next coll) is essentially (seq (rest coll))

sundarj18:07:12

so (next ()) is nil, rather than ()

David Reno18:07:41

Thinking about combining the two if’s but seems difficult due to if-let…

seancorfield18:07:43

Remember that (if cond expr false) is the same as (and cond expr) or (when cond expr) (modulo returning truthy/falsey rather than strictly true/false).

dorab18:07:49

(defn ipv4?
    [s]
  "Takes a string.
  Returns true if it is a valid IP v4 address.
  Else false."
  (if (string? s)
    (if-let [gs (re-matches #"(\d+)\.(\d+)\.(\d+)\.(\d+)" s)]
      (every? #(< (Long/parseLong %) 256) (rest gs))
      false)
    false))

dorab18:07:05

similar to what you had.

seancorfield18:07:14

@dorab (every? pred []) is true -- I don't think your code has the same behavior as David's in the case where (rest gs) returns an empty sequence?

seancorfield18:07:14

(or are we guaranteed that if re-matches returns non-`nil` then its rest will be non-empty for that regex? I didn't try that in the REPL yet)

dorab18:07:50

that was my assumption. did not verify with repl.

seancorfield18:07:32

Yeah, I think that's a safe assumption now I think about it...

seancorfield18:07:01

If the regex matches at all, you'll get a 5-tuple back.

seancorfield18:07:25

So

(and (string? s)
     (when-let [gs (re-matches #"(\d+)\.(\d+)\.(\d+)\.(\d+)" s)]
       (every? #(< (Long/parseLong %) 256) (rest gs))))

David Reno18:07:22

trying to understand the differences between if-let/`when-let`

seancorfield18:07:11

(when-let binding lots of stuff) is equivalent to (if-let binding (do lots of stuff) nil)

David Reno19:07:07

seems like it’s also equivalent to (if-let binding (do lots of stuff)), am I wrong?

seancorfield19:07:46

Yes, both if and if-let can omit the "else" expression and that is equivalent to having nil there.

David Reno19:07:01

these both return nil: (when-let [s false] true) (if-let [s false] true)

seancorfield19:07:09

But it's considered non-idiomatic to have a one-armed if so when would be used instead.

David Reno19:07:19

yeah, I get it now.

seancorfield19:07:48

A lot of the time in Clojure, you write predicates based on truthy/falsey without needing to worry about returning strictly true/`false` but if you put ? at the end of a predicate name, there's an assumption that it returns a Boolean, not just truthy/falsey values. So it's a bit of a trade off there.

seancorfield19:07:29

There are some functions that care about the difference between nil and false tho' so it can matter sometimes.

seancorfield18:07:09

(assuming that it's OK to return nil rather than false -- or just wrap that in a call to boolean if you want true/`false` explicitly)

David Reno19:07:29

Wow, this has been an excellent learning exercise. No embedded comments anymore, but certainly short. Thanks everyone!

jonahbenton19:07:45

forgive me for beating the horse, but two more points: \d+ will match a numeric sequence that is too long for Long/valueOf, and will cause it to throw. java/clj has a min/max syntax: \d{1,3} that will cause the match to fail

jonahbenton19:07:12

and the docstring, somewhat counter-intuitively, goes before the args

jonahbenton19:07:19

the reason that it doesn't cause an error is that a string is a perfectly valid expression

jonahbenton19:07:34

and the reason it makes sense for it to go before the args is that clj supports different function bodies for different numbers of arguments

jonahbenton19:07:55

though that's not the common case

valtteri19:07:38

nil is considered as ‘falsey’ so in most cases it’s OK to return nil. However if you wish to return always a boolean value you can use if-let instead.

(defn ipv4? [s]
  (if-let [matches (and (string? s)
                        (re-matches #"^(\d+)\.(\d+)\.(\d+)\.(\d+)$" s))]
    (every? #(<= (Long/valueOf %) 255) (rest matches))
    false))
Then it behaves similarly to functions in clojure core that end with ?.

valtteri19:07:09

Btw, notice the neat thing with and is that it returns the last truthy value. (and true "hey!") => "hey!"

seancorfield19:07:40

I think I like that better than my version. Nice.

jonahbenton20:07:03

So in the vein of "now you have two problems"- the {1,3} syntax should replace the + and be contained in the group

David Reno20:07:15

With @jonahbenton’s comments on size of octets. Now I find myself wondering if that regexp can be reduced in size without adding too much complexity… time to walk away? [edit to add the comment part]

jonahbenton20:07:05

In theory one can create a larger group- exact 3 of 1-3 digits followed by a ., followed by 1-3 digits- but that's very messy to process

David Reno20:07:36

yeah, the theory of an “octet” seems nice but messy.

jonahbenton20:07:44

and regexp is a heavy/expensive tool

David Reno20:07:56

think I should have split on . and processed directly?

jonahbenton20:07:41

btw, looks like slack has gotten confused about comment order- but see my earlier comment about the syntax- each should be (\d{1,3}) rather than (\d+){1,3}

David Reno20:07:26

well, I deleted it and re-added because I forgot to paste the version with the comment in the right place.

jonahbenton20:07:29

in practice, split is not much better- the split is on a regexp

David Reno20:07:43

so if you need a regexp just use it?

David Reno20:07:56

seemed like a good application

jonahbenton20:07:03

yeah, split wants to split on a regex, rather than a char

jonahbenton20:07:25

in lower level languages, str -> ip walks the string. for some that's the right level of abstraction. for others, regexp is the right level.

kennytilton21:07:48

It has been years since I did a CLJC project, and I just went googling for a template to create such a beast, but have found nothing. Help the addled: is nothing particular required, do I just add the .cljc extension and go?

eggsyntax17:07:07

Dunno if you got a response to this -- answer is yes! Can be any project, just add a .cljc file (& if you put it in a separate source dir, which some people do, you may have to add that source dir to your project). And be aware that you can use reader conditionals if you need to include clj- or cljs-specific code in it.

Chase22:07:23

so i think i understand the usage of "and" and "or" in that "and" will give you the first false value or if no false values then it will give you the last truthy value. but i'm not quite understanding why it works like that. It doesn't make intuitive sense to me. I have little experience with and's and or's in other languages but it wasn't presented like this.

Chase22:07:58

i think when i saw it before the use of "and" and "or" was specifically to give you a boolean value only.

Timo Freiberg22:07:48

You understand why it works though?

genmeblog22:07:07

or and and are implemented as if chains

genmeblog22:07:15

(and true false true) is equivalent to

(let [a true]
  (if a
    (let [a false]
      (if a true a))
    a))

Timo Freiberg22:07:15

1. The implementations are valid because returning a thruthy value is the same as returning true, vice versa for falsy 2. The implementation was probably easier, definitely not harder that way, and it allows for cool elegant code

Chase22:07:08

ok, that helps. i think it's just the whole truthy thing. the way i understood the "and" and "or" logic is that you would get true or false. I hadn't encountered it where you would just get a value like 4 that is considered truthy.

Chase22:07:26

like when you use "and" in logic all the things have to be true right and then you get true. but here you get the first false value.

Timo Freiberg22:07:40

If you're used to a static, strict language, it feels weird. At least it did to me, coming from Java with a bit of haskell in my spare time

Timo Freiberg22:07:48

If you use the return values as booleans (in ifs etc) then you don't even have to notice

Timo Freiberg22:07:13

But if not, you get a bit of extra information

golin22:07:33

logic wise, and and or is the same, whether it accepts and returns truthy/falsy values vs boolean values. Since everything in clojure can be evaluated as truthy/falsy, may as well have it accept those rather than hard boolean types. Its just more generic and flexible.

Chase22:07:25

yep, this is helping. it's starting to make sense. i was coming from fiddling around with strictly typed languages like Timo mentioned. That if chain posted above is not quite clicking yet so i'm working on that understanding right now.

sundarj22:07:09

try playing around with macroexpand:

=> (macroexpand '(and true true))
(let* [and__5314__auto__ true] (if and__5314__auto__ (clojure.core/and true) and__5314__auto__))

Timo Freiberg22:07:34

Maybe use other values than booleans to see the difference :)

sundarj22:07:47

or rather,

=> (clojure.walk/macroexpand-all '(and true true))
(let* [and__5314__auto__ true] (if and__5314__auto__ true and__5314__auto__))

jonahbenton22:07:49

@U9J50BY4C if you haven't had a chance to watch Rich and also Stu Halloway's talks (check the ClojureTV channel on YouTube)- there are several that go into the implications of thinking about the world in terms of predicates rather than types

jonahbenton23:07:14

in all choices there are tradeoffs

Chase23:07:45

i've listened to a few of their talks. it's almost like i want to save them though until i'm further along so i can really dig into the wisdom. but yeah, clojure talks and projects are a big thing that drew me to this language. they all seem creative and well thought out and just the right vibe for what i'm looking for in this whole experience. it's hard to explain.

jonahbenton23:07:00

absolutely, I think grokking that vibe is a common experience. the talks are dense enough that they generally reward followup viewings

golin22:07:26

As a side note, clojure has a convention for "predicate" functions. They typically are named with a ? at the end and return booleans. i.e. int?, some?

golin22:07:17

and and or don't have ? at the end of their names. Following this convention, they don't return booleans.

golin23:07:23

conceivably, clojure could have implemented a predicate version of and as and? that strictly returns booleans only, but the behavior would be redundant.

mfikes23:07:30

@hiskennyness Right, you just put *.cljc files on the classpath and go. No real project-level considerations.

Chase23:07:07

so basically you just know if you get a value other than false or nil then there was something truthy in the expression? am i getting closer to understanding?

sundarj23:07:02

yup! the only falsy values in Clojure are false and nil; everything else is truthy

sundarj23:07:25

this is opposed to some other languages that have things like 0 and "" be falsy

dpsutton23:07:43

I saw someone mention a nasty bug that the java native "false" is even truthy in clojure

mfikes23:07:02

Yeah. (Boolean. false) is true

mfikes23:07:03

That will bite you if you serialize booleans and use them on the other end.

mfikes23:07:56

Same happens with (js/Boolean. false)

sundarj23:07:14

looks like doing an explicit boolean cast resolves that particular problem:

=> (if (Boolean. false) true false)
true
=> (if (boolean (Boolean. false)) true false)
false

mfikes23:07:17

(if (js/Boolean. false) :t :f)

mfikes23:07:09

Yeah, one approach is to use clojure.walk to coerce any Boolean instances using boolean

mfikes23:07:34

(If you have a large value with unknown structure)

mfikes23:07:23

Interestingly, that trick doesn't work in ClojureScript (if (boolean (js/Boolean. false)) :t :f) yields :t

sundarj23:07:55

i suppose that's in keeping with JavaScript's semantics?

sundarj23:07:52

since if (new Boolean(false)) { true } else { false } is true

mfikes23:07:52

Dunno. I honestly don't think there is much desire to worry about the semantics of wrappers.

sundarj23:07:12

though if you leave the new off it's false 😛

mfikes23:07:17

(boolean? (js/Boolean. false)) is false in ClojureScript, (boolean? (Boolean. false)) is true in Cloure

mfikes23:07:20

Unfortunately the docstring in ClojureScript is a bit ambiguous

mfikes23:07:22

Return true if x is a Boolean could mean Boolean as in the Boolean wrapper object, or it could mean Boolean, as in the type derived from the dude named George Boole.

sundarj23:07:26

JavaScript's primitive object instances are treated differently to the primitives themselves

Boolean("")
false
Boolean(String(""))
false
Boolean(new String(""))
true

mfikes23:07:57

Hah. I refuse to even dwell upon that. 🙂