PurelyFunctional.tv Newsletter 330: Tool: Figwheel
Issue 330 - June 10, 2019 · Archives · Subscribe
Clojure Tool 🔨
If you've done any ClojureScript development, you must have heard of Figwheel. If not, get ready for your socks to be knocked off.
Figwheel brings live coding and Repl-Driven Development to the browser. It watches your code files for changes, recompiles, and sends the new code to the browser. The experience is the best frontend flow you have ever seen. You can modify live UIs before your eyes. I cannot recommend it highly enough.
What's more, Bruce Hauman, the creator, has put it tireless effort to make the whole thing really smooth, including showing really great compilation errors right in the browser. There's a lot more to it than that. Give it a shot.
Killer Book 📖
Computation: Finite and Infinite Machines by Marvin Minsky is quite a book. On the surface, it's simple. It talks about finite state machines and their limits, then shows how adding an infinite tape removes those limits and adds other weird limits. But the book goes much deeper. It talks about a lot of stuff I learned in school in my Automata Theory class. But it really talks about the consequences in a way that most books don't dare today.
This book was written in 1967. Marvin Minsky was the founder of the AI Lab at MIT, and this book shows the signs of that research. They were trying to understand the mind using the newfound theories of computation that were emerging with the study of computers. This book touches on (though I wish went deeper into) the ideas of the abilities and limits of our minds. I really appreciated the treatment of Turing Machines. He even defines a Lispy Turing Machine---a Turing Machine defined using two linked lists to represent the tape. Pretty cool!
I heard of this book in a couple of talks by Alan Kay. It intrigued me. It's out of print, so I had to get it through interlibrary loan. I don't think I would buy an old copy, though. They are going for ~$90 on Amazon. It's not that kind of book.
Clojure Challenge 🤔
Last week's challenge
The puzzle in Issue 329 was to create a test.check generator for email addresses.
I got no submissions. Ray mentioned that a good way to validate an email address was to contact the server. That sounds good, but it won't generate new email addresses.
I thought it would be a cool challenge, so I'm surprised that it didn't get any submissions. I wonder if test.check is not as familiar to people as I thought. Do you know test.check? Would you like to? Hit reply and let me know.
So, let's go over a simple one.
Let's say I already have a regex that emails my system allows should
conform to. Let's call it email-re
. If we use this regex as part of
our spec, we might get something like this:
(spec/def :t/email (spec/and string? #(re-matches email-re %)))
But when we try to sample the generator, we get an Exception.
(gen/sample (spec/gen :t/email))
;; Couldn't satisfy such-that predicate after 100 tries.
The generators that spec creates are very naive. When defined like we define this one, as a series of predicates, they just filter out values that don't match the predicates. It's unlikely that a random string will look like an email address, so we don't even find one in 100 random strings.
This is a common problem when using the automatic generators built by spec. We need to build a custom generator. But how?
The simple answer is that instead of generating random strings and filtering, we need to build the email string from easy-to-generate parts.
Here's a super simple example from the test.check docs:
First, we build a generator that makes random domain names. We can just randomly select one from a list.
(def gen-domain (gen/elements ["gmail.com" "hotmail.com"
"computer.org"]))
Next, we build a generator that makes random user names. It's just a non-empty alpha-numeric string.
(def gen-username (gen/not-empty gen/string-alphanumeric))
Then we put them together with a @
between them.
(def gen-email
(gen/fmap (fn [[username domain]]
(str username @ domain))
(gen/tuple gen-username gen-domain)))
Here's the kind of stuff it generates.
(gen/sample gen-email)
;; => ("63@computer.org" "9@computer.org" "X@computer.org" "Eb@hotmail.com"
"ku@hotmail.com" "JY@computer.org" "wj3@gmail.com" "936v@hotmail.com"
"ED@gmail.com" "VUXr59K0@gmail.com")
Those are emails, but we're obviously not covering a huge part of the space. Domain names can get complicated, and so can usernames. They're not just alpha-numeric strings.
Let's get a little more sophisticated.
I searched the web for a regex for email addresses. I found this one, which I like:
(def email-re
#"(([^<>()\[\]\.,;:\s@"]+(\.[^<>()\[\]\.,;:\s@"]+)*)|(".+"))@(([^<>()\[\]\.,;:\s@"]+\.)+[^<>()\[\]\.,;:\s@"]{2,})")
It's long and looks complicated. But it boils down to a repeated segment which defines what characters are allowed:
(def email-char-re #"[^<>()\[\]\.,;:\s@"]")
We can use this segment to generate random characters that conform.
(def gen-email-char
(gen/such-that #(re-matches email-char-re (str %)) gen/char))
Then we can make random strings from those characters. We'll make it always not empty because we never need empty ones according to the regex.
(def gen-email-char-string
(gen/fmap #(apply str %)
(gen/not-empty (gen/vector gen-email-char))))
Now, we can use that to build up the parts. Here are usernames, which are segements separated by dots.
(def gen-email-name
(gen/fmap #(str/join "." %)
(gen/not-empty (gen/vector gen-email-char-string))))
Domain names are similar, except the last segment has to be at least two characters.
(def gen-email-domain
(gen/fmap (fn [[domain-segs tld1 tld2]]
(str domain-segs "." tld1 tld2))
(gen/tuple gen-email-name
gen-email-char-string
gen-email-char-string)))
Now we can put them together.
(def gen-email
(gen/fmap (fn [[username domain]]
(str username @ domain))
(gen/tuple gen-email-name gen-email-domain)))
When we sample it, we get lots of crazy email addresses.
(gen/sample gen-email)
;; => ("@¥.ú .²e" "Ø.@ñ.NÎ" "¿G.Ø@ñwßÃ..ëxÂ!" "¨.Ðâë.¼/.Âù@òÉ.Â.ë.ù"
"vu.BÖ¯¢.Í@þÛü.î.´Ê!6" "Â.L¹¹èf.Zô.æ@\b*î.0¡6.½.¶"
"ÏÂÆ{É.ûÈÉu.âàWN4@7.j1WÂ.µR*A.Hcõ&E.PE$.òIÛOÚäû"
"&9ÅÂÇ.Où¸.r¾.Þa&.auj@#mõê.cÓ\\æ÷"
"Yù.íï®±|y.ÐNFÕÊ!.TÔéã.P.ipÂðêc}.P@^P.I˸j¤ñ³.óZÀËa.ú}#zvá.m.^}öSFjØ.bÀÊEcÖ¡"
"æ}¯.sÇ.F4.Ý*fÝBº.V.ø.L¥hôít².³ÒbÕ@wHföª«.â·cÔq¬.Yí-§ª.cÞÇ\bñĽö.Æ×`.í Oâ.³«.Ø$¨ã§8ÿú/")
Are those really valid? Maybe. They do all pass the regex I found though. If you've got a better regex, you can create a custom generator to satisfy that.
There's an alternative solution to this, which is to use the library test.chuck which lets you create generators from regexes automatically. Here's what it looks like.
(def gen-email (gen'/string-from-regex email-re))
And the sample is much slower, and produces a lot of unreadable characters.
(gen/sample gen-email4)
;; => ("@𜽓.𩊕" "練@撆.𫊣." ".@."
"..𧣓.@𗳛.."
".ᡦ..@𣧷㣆.肋.얇."
"璻.𫏦..𭠟𪄾@..𗐄."
"𨩸@𮝁.."
"..𪗋.隫⁴.弻𱐝@𩔊.𗙨𱊲"
"...漜.𗡰..㷺𬞻.@𪗍..𬷋...㱏"
"ꘑ.𗍢.ݮ𭖫.𗺈𩰅.@𝗲..𭮡")
That's available if you want it.
This week's challenge
properties of sort
When we are testing a function with property-based testing, we want to make sure our tests are not re-implementing the function we are testing. On the contrary, we want the test to be simpler and more obviously correct.
It's often challenging to come up with properties, especially if you do know how t he function works. This week, we will face that challenge head on.
The challenge this week is to come up with at least three properties we
would like to assert on the
sort
function. You can
write them however you like. You can write them in English, or if you
want, as test.check properties. What's important is the thought process
behind it.
As usual, please send me your answers. I'll share them all in next week's issue. If you send me one, but you don't want me to share it publicly, please let me know.
Rock on!
Eric Normand