<?xml version="1.0" encoding="UTF-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <title><![CDATA[Jake McCrary's articles]]></title>
  <link href="https://jakemccrary.com/atom.xml" rel="self"/>
  <link href="https://jakemccrary.com/"/>
  <updated>2026-03-14T17:06:03+00:00</updated>
  <id>https://jakemccrary.com/</id>
  <author>
    <name><![CDATA[Jake McCrary]]></name>
  </author>
  <entry>
    <id>https://jakemccrary.com/blog/reading-in-2025/index.html</id>
    <link href="https://jakemccrary.com/blog/reading-in-2025/index.html"/>
    <title><![CDATA[Reading in 2025]]></title>
    <updated>2026-01-26T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>At the beginning of every year, I take the time to update my records of what I&apos;ve read the previous year and write up a summary.</p><p>Previous summaries: <a href="/blog/2014/01/01/using-incanter-to-review-my-2013-reading/">2013</a>, <a href="/blog/2015/01/08/reading-in-2014/">2014</a>, <a href="/blog/2016/03/13/reading-in-2015/">2015</a>, <a href="/blog/2017/01/04/reading-in-2016/">2016</a>, <a href="/blog/2018/03/03/reading-in-2017/">2017</a>, <a href="/blog/2019/01/21/reading-in-2018/">2018</a>, <a href="/blog/2020/01/11/reading-in-2019/">2019</a>, <a href="/blog/2021/01/24/reading-in-2020/">2020</a>, <a href="/blog/2022/01/02/reading-in-2021/">2021</a>, <a href="/blog/2023/01/14/reading-in-2022/">2022</a>, <a href="/blog/2024/02/18/reading-in-2023/">2023</a>, and <a href="/blog/reading-in-2024/">2024</a>.</p><p>I&apos;ve continued to keep track of my reading using <a href="http://goodreads.com">Goodreads</a>. My <a href="https://www.goodreads.com/user/show/3431614-jake-mccrary">profile</a> has nearly the full list of the books I&apos;ve read since 2010.</p><h2 id="2025-goals">2025 Goals</h2><blockquote><p>For the last month or so, I&apos;ve been better about reading with a regular cadence. I&apos;d like to keep that up.</p></blockquote><p>I don&apos;t particularly feel like I kept a good cadence, but when I look at the chart of pages and books per month, it seems fairly well distributed over the year. Success, I guess?</p><p>One thing not captured here is that I&apos;m doing a lot more long-form reading online. I&apos;m okay with that not showing up here, but it&apos;s accurate to say that some of my reading time has been taken up by long articles from authors I follow on Substack.</p><h2 id="highlights">Highlights</h2><p>Below are the highlights from 2025. Any title link will bring you to Goodreads.</p><h3 id="five-star-books">Five-star books</h3><h4 id="alien-clay-by-adrian-tchaikovsky"><a href="https://www.goodreads.com/book/show/199851460">Alien Clay</a> by Adrian Tchaikovsky</h4><p>This was the first Adrian Tchaikovsky book I&apos;ve read, and it was great. I thought it built an interesting world and wrestled with good questions.</p><p>I was so hopeful that I had found a new-to-me science fiction author with a large catalog of books. Unfortunately, I&apos;ve found Tchaikovsky&apos;s other writing not as compelling.</p><h4 id="this-is-how-they-tell-me-the-world-ends:-the-cyberweapons-arms-race-by-nicole-perlroth"><a href="https://www.goodreads.com/book/show/54144854">This Is How They Tell Me the World Ends: The Cyberweapons Arms Race</a> by Nicole Perlroth</h4><p>I might have been a bit generous with my stars here, but I did enjoy this book. It doesn&apos;t leave the reader feeling great about our world. I was already familiar with a lot of what this book covers, but I&apos;ve probably been exposed to the topic more due to my profession. Still, a bunch of this was new to me, and even the parts that weren&apos;t were interesting.</p><h4 id="adapt:-lessons-learned-climbing-100-5.13&apos;s-by-kris-hampton"><a href="https://www.goodreads.com/book/show/222527388">Adapt: Lessons Learned Climbing 100 5.13&apos;s</a> by Kris Hampton</h4><p>I&apos;m a huge fan of Kris Hampton&apos;s work and I&apos;ll continue reading it and listening to it over the years to come.</p><h4 id="demon-copperhead-by-barbara-kingsolver"><a href="https://www.goodreads.com/book/show/60194162">Demon Copperhead</a> by Barbara Kingsolver</h4><p>I enjoyed it and thought it was compelling. Here are two other reviews (<a href="https://www.goodreads.com/review/show/5189383062">one</a>, <a href="https://www.goodreads.com/review/show/5031859304">two</a>) that can give you an idea of what you&apos;re getting into when you pick up this book.</p><h4 id="a-deepness-in-the-sky-(zones-of-thought,-#2)-by-vernor-vinge"><a href="https://www.goodreads.com/book/show/8291176">A Deepness in the Sky (Zones of Thought, #2)</a> by Vernor Vinge</h4><p>I also read the first book in this series in 2025 and would recommend you read both of them. I preferred this book to the first but, from talking to some friends, I know others feel the other way.</p><p>It touches on one of my favorite topics in science fiction: what does it mean to be human? Parts of that leaked into another <a href="/blog/humans-ask-computers-propose-humans-decide/">article</a> I wrote about using recent AI tooling.</p><p>This was the second book I read in 2025 where spider-like aliens exist.</p><h4 id="endling-by-maria-reva"><a href="https://www.goodreads.com/book/show/218510455">Endling</a> by Maria Reva</h4><p>I&apos;m not entirely sure I can describe why I like this so much. It was funny and dark with a fair amount of meta self-awareness.</p><h4 id="if-anyone-builds-it,-everyone-dies:-why-superhuman-ai-would-kill-us-all-by-eliezer-yudkowsky"><a href="https://www.goodreads.com/book/show/228646231">If Anyone Builds It, Everyone Dies: Why Superhuman AI Would Kill Us All</a> by Eliezer Yudkowsky</h4><p>I bought this book with the goal of helping it reach bestseller status. I was already sold on the arguments before reading the book. I think this is an important topic that is undercovered by media and not thought nearly enough about by people (both in power and normal folks).</p><h4 id="playground-by-richard-powers"><a href="https://www.goodreads.com/book/show/207685065">Playground</a> by Richard Powers</h4><p>Richard Powers does it again. He continues to deliver some deep emotional moments wrapped around neat and interesting technology that either exists or you can imagine existing in this world. To me, this is up there in quality with <em>The Overstory</em>.</p><h3 id="other-highlights">Other highlights</h3><p>Here are some other highlights from the year. Most of these are probably four-star books, though some might have been three-star.</p><h4 id="the-ministry-of-time-by-kaliane-bradley"><a href="https://www.goodreads.com/book/show/199798179">The Ministry of Time</a> by Kaliane Bradley</h4><p>Time travel and workplace romance/comedy. Of course it was enjoyable.</p><h4 id="nightbitch-by-rachel-yoder"><a href="https://www.goodreads.com/book/show/57123268">Nightbitch</a> by Rachel Yoder</h4><p>That was weird. I liked it a lot.</p><h4 id="i-who-have-never-known-men-by-jacqueline-harpman"><a href="https://www.goodreads.com/book/show/60457992">I Who Have Never Known Men</a> by Jacqueline Harpman</h4><p>Perfect length of a novel for living in this odd, unexplained world.</p><h4 id="dream-count-by-chimamanda-ngozi-adichie"><a href="https://www.goodreads.com/book/show/219520778">Dream Count</a> by Chimamanda Ngozi Adichie</h4><p>What can I say, I like Adichie&apos;s writing.</p><h4 id="children-of-time-(children-of-time,-#1)-by-adrian-tchaikovsky"><a href="https://www.goodreads.com/book/show/25499718">Children of Time (Children of Time, #1)</a> by Adrian Tchaikovsky</h4><p>Probably my second favorite book by this author. Shares a similar alien race with Vinge&apos;s A Deepness in the Sky. Again, science fiction that makes you wonder about society.</p><h4 id="disgrace-by-j.m.-coetzee"><a href="https://www.goodreads.com/book/show/55710146">Disgrace</a> by J.M. Coetzee</h4><p>Slow-motion train wreck of a story presented through excellent writing. Shortly after reading this, I listened to <a href="https://conversationswithtyler.com/episodes/jonny-steinberg/">Tyler Cowen&apos;s podcast on South African crime</a> and that helped me understand this book.</p><h4 id="what-we-can-know-by-ian-mcewan"><a href="https://www.goodreads.com/book/show/223855577">What We Can Know</a> by Ian McEwan</h4><p>It was good.</p><h4 id="endure:-mind,-body,-and-the-curiously-elastic-limits-of-human-performance-by-alex-hutchinson"><a href="https://www.goodreads.com/book/show/41014339">Endure: Mind, Body, and the Curiously Elastic Limits of Human Performance</a> by Alex Hutchinson</h4><p>I learned a ton reading this book. I think quite a bit of it will be applicable to my own sports practice.</p><h4 id="project-hail-mary-by-andy-weir"><a href="https://www.goodreads.com/book/show/222697645-project-hail-mary">Project Hail Mary</a> by Andy Weir</h4><p>I thought it was a good and entertaining book, if predictable and not really surprising. If you liked The Martian, you&apos;ll like this book.</p><h2 id="stats">Stats</h2><p>I read 37 books in 2025.</p><pre><code class="language-org">| Year | # of Pages | # of Books |
|------+------------+------------|
| 2025 |      14661 |         37 |
| 2024 |      12919 |         37 |
| 2023 |      14956 |         53 |
| 2022 |      10127 |         35 |
| 2021 |      19564 |         57 |
| 2020 |      12093 |         43 |
| 2019 |      15994 |         42 |
| 2018 |      13538 |         36 |
| 2017 |      18317 |         48 |
| 2016 |      22790 |         59 |
| 2015 |      21689 |         51 |
| 2014 |      24340 |         71 |
| 2013 |      19815 |         60 |
| 2012 |      14208 |         44 |
| 2011 |       9179 |         19 |
| 2010 |      14667 |         46 |
</code></pre><p>Reading was fairly well spread out over the year.</p><p><img alt="Book and pages count by month" src="/images/reading-by-month-2025.svg" title="Number of books and pages for each month" /></p><p>Electronic books continue to dominate.</p><pre><code class="language-org">|           | 2025 | 2024 | 2023 | 2022 | 2021 | 2020 | 2019 | 2018 | 2017 | 2016 | 2015 |
|-----------+------+------+------+------+------+------+------+------+------+------+------|
| audiobook |    0 |    0 |    0 |    0 |    0 |    1 |    0 |    0 |    0 |    0 |    0 |
| ebook     |   35 |   35 |   51 |   34 |   56 |   41 |   43 |   37 |   37 |   56 |   47 |
| hardcover |    1 |    1 |    0 |    1 |    0 |    0 |    1 |    1 |    7 |    0 |    1 |
| paperback |    1 |    1 |    2 |    0 |    1 |    1 |    7 |    5 |    5 |    3 |    3 |
</code></pre><p>My non-fiction reading increased last year.</p><pre><code class="language-org">|                           |   2025 |   2024 |  2023 | 2022 |   2021 |   2020 |   2019 |   2018 |
|---------------------------+--------+--------+-------+------+--------+--------+--------+--------|
| fiction                   |     26 |     33 |    47 |   28 |     46 |     26 |     28 |     29 |
| non-fiction               |     11 |      4 |     6 |    7 |     11 |     17 |     23 |     14 |
| fiction:non-fiction ratio | 2.36:1 | 8.25:1 | 7.8:1 |  4:1 | 4.18:1 | 1.53:1 | 1.22:1 | 2.07:1 |

</code></pre><p>Here is the star rating distribution.</p><pre><code class="language-org">|             | 2 stars | 3 stars | 4 stars | 5 stars |
|-------------+---------+---------+---------+---------|
| fiction     |       1 |       9 |      11 |       5 |
| non-fiction |       1 |       4 |       3 |       3 |
</code></pre><h2 id="2026-goals">2026 goals</h2><p>I&apos;d like to keep up a regular reading cadence. I&apos;ve done a pretty good job this month of making a little progress on books almost every day. I&apos;d like to keep doing that. Maybe not every day, but most days, I&apos;d like to read at least a little.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/notify-yourself-when-a-task-finishes/index.html</id>
    <link href="https://jakemccrary.com/blog/notify-yourself-when-a-task-finishes/index.html"/>
    <title><![CDATA[Notify yourself when a task finishes]]></title>
    <updated>2025-12-01T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>If you&apos;ve got a ridiculously good memory and you&apos;ve been reading my writing for a while, you know I&apos;m a fan of processes <a href="/blog/categories/notifications/">notifying</a> you when they are done. I often have some task running in a hidden terminal that performs actions when files change. This is most often running Clojure tests whenever a file changes using either <a href="https://github.com/jakemcc/test-refresh">test-refresh</a> or <a href="https://github.com/jakemcc/lein-autoexpect">lein-autoexpect</a>. Another common watch task is rendering this website whenever one of the markdown files changes.</p><p>I don&apos;t like needing to have these processes always visible, since I mostly only care about when they finish. When running unit tests, I don&apos;t need to see the output unless a test is failing. When writing articles, I only care about when the rendering is done so I know I can refresh my browser to review the output.</p><p>On macOS, one way of doing this is using <a href="https://github.com/julienXX/terminal-notifier">terminal-notifier</a>. <code>terminal-notifier</code> makes it trivial to send notifications.</p><p>Below is the script I run while working on this website. It uses <code>entr</code> to monitor the input files; when changes are detected, it renders this site using my homegrown <a href="https://babashka.org/">Babashka</a> static site generator, and when that finishes, it uses <code>terminal-notifier</code> to alert me.</p><pre><code class="language-bash">#!/bin/bash
while sleep 0.5; do 
    rg bb templates source --files -t css -t clojure -t markdown -t html \
        | entr -d -s &apos;rm -rf output/*; bb render &amp;&amp; terminal-notifier -message &quot;Rendering complete&quot;&apos;;
done
</code></pre><p>This site renders quickly, so I can usually make some edits, save, and toggle to a browser to refresh and see the output. Still, it is nice to see that little notification pop-up on my screen so I know for sure that if I hit refresh, I&apos;m seeing the latest render.</p><p>When I&apos;m running my Clojure tests, both <code>lein-autoexpect</code> and <code>test-refresh</code> send a notification with a pass or fail message based on the status of the unit tests that just ran. If the tests are passing, I don&apos;t have to glance at my terminal. If they are failing, I do.</p><p>I&apos;d encourage you to think about what processes you might want to get notifications from when they are done and look into how to set that up. <code>terminal-notifier</code> works great on macOS. I can&apos;t make recommendations for other operating systems since it has been years since I&apos;ve used any alternatives besides SSHing into a Linux server.</p><p>It is worth the effort to figure out how to have notifications. They remove a trivial inconvenience (having to switch programs, needing to keep a window visible on your screen) and make life a little better. By stacking small, slightly life-improving techniques, all of a sudden you find yourself much more productive.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/shipping-little-apps-anywhere-anytime/index.html</id>
    <link href="https://jakemccrary.com/blog/shipping-little-apps-anywhere-anytime/index.html"/>
    <title><![CDATA[Shipping little apps anywhere, anytime]]></title>
    <updated>2025-11-28T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>I&apos;m a big fan of making <a href="/blog/2020/10/03/go-create-silly-small-programs/">small (sometimes silly) programs</a>. As a software developer, you have a superpower: you can identify problems in your life and fix them by creating some specific software that solves for exactly what you need. When scoped small enough, creating these tiny programs takes minimal time investment.</p><p>When you develop the practice of recognizing when a bit of software would be helpful, you see opportunities all the time. But you don&apos;t control when you get inspiration for these programs. So you come up with strategies for handling these bursts of inspiration.</p><p>One strategy: Write yourself a note (paper, email to yourself, some app on your phone) and maybe get around to it later. (You occasionally manage to get around to it later.) Another strategy: Think about the inspiration and trick yourself into thinking you&apos;ll remember it later when you&apos;re at a computer. You justify this by claiming if you forget it, it must not have been important.</p><p>These workflows are fine but they leave a lot of room for never following up. With modern AI tools, we can do better.</p><p>My new strategy:</p><ol><li>Inspiration strikes!</li><li>I pull out my phone and open my web browser to <a href="https://openai.com/codex/">OpenAI&apos;s Codex web app</a>.</li><li>I translate my inspiration into a task and type (or voice-to-text) it into Codex.</li><li>I submit the task to Codex, go about my day, and check on it later.</li><li>Later: read the diff, click the Codex button to open a PR, merge the PR through GitHub&apos;s mobile interface, and let GitHub Actions deploy the changes to GitHub Pages.</li></ol><p>I started using this technique in early summer 2025. Since then, I&apos;ve been able to develop and iterate on a handful of single-page web applications this way. As models improve, it is getting even easier to knock them out. It works well for either making a new application or tweaking an existing one.</p><p>Here is my setup:</p><ul><li>I have a single repo named <a href="https://github.com/jakemcc/experiments">experiments</a><a href="#fn-1" id="fnref1"><sup>1</sup></a> on GitHub.</li><li>This repo has a subdirectory per application.</li><li>The applications are in a variety of web languages (HTML, CSS, TypeScript, JavaScript, ClojureScript).</li><li>OpenAI Codex is linked with this experiments repo.</li></ul><p>With this setup, I&apos;m able to follow the above strategy with minimal friction. If I have an idea for a new little application, I open Codex and provide a description of what I want and what it should be called, and it usually manages to start work on it. When I have an idea for tweaking an application, I open Codex, tell it what subdirectory the app is in and what tweak I want made. All of this can be done from a smartphone.</p><p>When Codex is done, I do a quick scan through the diff, click the buttons to open a PR, merge it, wait for the deploy, and then check on the deployed artifacts. The apps end up published at <a href="https://jake.in/experiments">jake.in/experiments</a>.</p><p>It isn&apos;t all smooth; sometimes a problem is introduced. Depending on the problem, I&apos;ll either revert the code and try again or give Codex more instructions and try to have it fix it. If really needed, I&apos;ll fire up my laptop and fix it myself or iterate with AI on fixing the problem there.</p><p>The bar has been seriously lowered for creating specific software. Go do it. It is fun, but in a different way than traditional programming.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>I don&apos;t know if this limitation still exists, but when I was initially setting this up my experiments repo had zero commits. This caused problems in Codex that were fixed by adding a single commit.<a href="#fnref1">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/humans-ask-computers-propose-humans-decide/index.html</id>
    <link href="https://jakemccrary.com/blog/humans-ask-computers-propose-humans-decide/index.html"/>
    <title><![CDATA[Humans ask, computers propose, humans decide]]></title>
    <updated>2025-08-17T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p><strong>Warning: There are minor spoilers of parts of <em>A Deepness in the Sky</em> in this article.</strong></p><p>I was reading Vernor Vinge&apos;s <a href="https://en.wikipedia.org/wiki/A_Deepness_in_the_Sky"><em>A Deepness in the Sky</em></a> when a paragraph made me think of today&apos;s AI tools.</p><p>In <em>A Deepness in the Sky</em>, one of the groups of humans, the Emergents, has figured out how to take advantage of a &quot;mindrot&quot; virus that was a plague on their homeland. Once a person is infected, the Emergents are able to manipulate the mindrot to force an obsession. This practically turns the infected person, colloquially called a ziphead, into a specialized appliance focused on their obsession and little else.</p><p>In the following paragraph, one of the Emergents talks about how they use a subset of the zipheads to enhance their ship&apos;s computer:</p><blockquote><p>They left the group room and started back down the central tower. “See, Pham, you—all you Qeng Ho—grew up wearing blinders. You just know certain things are impossible. I see the clichés in your literature: ‘Garbage input means garbage output’; ‘The trouble with automation is that it does exactly what you ask it’; ‘Automation can never be truly creative.’ Humankind has accepted such claims for thousands of years. But we Emergents have disproved them! With ziphead support, I can get correct performance from ambiguous inputs. I can get effective natural language translation. I can get human-quality judgment as part of the automation!”</p></blockquote><p>The zipheads see the requests made by the users of the ship, apply their human judgment to the request, and then work with the computer to fulfill their interpretation of what the user is requesting. This allows the Emergents to make ambiguous requests to their ship&apos;s computer, requests a human would understand but a computer could not, and get back quality results. There are literally humans-in-the-loop of the Emergents&apos; computer system.</p><p>This paragraph made me think about how I use the current crop of AI tools and how it&apos;s changed how I interact with computers. I can now open an app and poorly specify what I want (don&apos;t fix typos, don&apos;t bother with full sentences, be vague) and the computer still often manages to perform the task or find the information I&apos;m asking about.</p><p>I can underspecify what I&apos;m looking for and get approximately a &quot;human-quality&quot;<a href="#fn-1" id="fnref1"><sup>1</sup></a> fulfillment of that request. Not only that, but the AI response often comes back and asks about follow-up steps and offers to perform them. And all without enslaving other humans with a virus and attaching them to the computer.</p><p>This is amazing. Does it work 100% of the time? No. But wow, it works enough of the time to be a big game changer.</p><p>Here are three examples of varying degrees of specification while working with an AI:</p><h3 id="typos-barely-matter">Typos barely matter</h3><p>I rarely correct typos anymore when searching on Google or asking an AI for help with something. The computer doesn&apos;t care and still mostly does the right thing. To be fair, this has been gradually happening over time with Google&apos;s ability to make sense of garbage searches, but modern AI tools have drastically accelerated it.</p><h3 id="time-series-triage-with-o3">Time series triage with <code>o3</code></h3><p>Earlier this year, I threw a CSV of memory stats and usage metrics for a ton of JVM processes my team manages at OpenAI&apos;s <code>o3</code> model. I mentioned three services that ran out of memory on specific dates and asked it to identify other services that might be approaching memory problems. <code>o3</code> identified a pattern in the data that correlated with running out of memory and flagged a few other processes that might be approaching a problem. I looked at more metrics, agreed with <code>o3</code>, and then changed some memory settings to avoid future problems.</p><h3 id="config-migrations">Config migrations</h3><p>I recently needed to change a couple of values in about 45 config files. This wasn&apos;t something a simple <code>sed</code> could do because each file needed a unique value derived from another service&apos;s config. I provided a command to run that would tell the AI agent (or me, if I were doing this by hand) if the config values were correct and told it to run the command and fix the problems. I didn&apos;t specify what to change; it figured it out while I worked with some coworkers on figuring out a bug. Once I wrapped up with my coworkers, I reviewed the changes, agreed with them, and moved on to the next task.</p><h2 id="asks-➡-🤖-proposes-➡-🙂-decides">🙂 Asks ➡ 🤖 Proposes ➡ 🙂 Decides</h2><p>We used to have to specify specific instructions to a computer through programming. Now we can specify desires/outcomes with natural language and often the computer will do a decent job of achieving our goal or at least getting us to a reasonable starting point to take over.</p><p>The computer takes our ambiguous input, proposes a solution, and then we&apos;re able to step in and accept, change, and modify the results. This is an exciting change and thankfully we&apos;re able to achieve it without turning other humans into infected appliances.</p><pre><code class="language-bash">┌─────────────┐     ┌────────────────┐     ┌─────────────┐
│  Humans ask │ ──▶ │ Computer       │ ──▶ │ Humans      │
└─────────────┘     │  proposes      │     │   decide    │
      ▲             └────────────────┘     └─────────────┘
      │                                               │
      └───────────────────────────────────────────────┘
</code></pre><p>This feels like Vinge&apos;s ziphead-supported computer but we&apos;ve replaced infected humans with AI models<a href="#fn-2" id="fnref2"><sup>2</sup></a>.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>For some definition of human-quality<a href="#fnref1">↩</a></p></li><li class="footnote" id="fn-2"><p>How much longer until the non-infected humans are also replaced? Hopefully we&apos;re able to avoid a future dystopia.<a href="#fnref2">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/setup-emacs-to-autoformat-your-clojure-code-with-apheleia-and-zprint/index.html</id>
    <link href="https://jakemccrary.com/blog/setup-emacs-to-autoformat-your-clojure-code-with-apheleia-and-zprint/index.html"/>
    <title><![CDATA[Setup Emacs to autoformat your Clojure code with Apheleia and zprint]]></title>
    <updated>2025-04-20T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Keeping code consistently formatted is important for readability and maintainability. Once you get used to having a computer format your code for you, manually formatting code can feel tedious.</p><p>For the last few years, my team has been using <a href="https://github.com/kkinnear/zprint">zprint</a> to keep our Clojure codebase formatted to our specifications. <code>zprint</code> is great because it runs fast and is extremely customizable. This flexibility is powerful since it lets you format your code exactly how you want.</p><p>I&apos;ve recently migrated from my own custom <code>before-save-hook</code> that triggered <code>zprint</code> whenever I saved a buffer to using <a href="https://github.com/radian-software/apheleia">Apheleia</a>. Apheleia is an Emacs package that applies code formatters automatically on save. I won&apos;t quote the <a href="https://github.com/radian-software/apheleia/tree/2b491144fe157867ce1cc4538a9562edff57c891?tab=readme-ov-file#apheleia">whole introduction</a> in Apheleia&apos;s readme but it is designed to keep Emacs feeling responsive.</p><p>Here&apos;s the configuration I use in my Emacs setup:</p><pre><code class="language-lisp">(use-package apheleia
  :straight (apheleia :host github :repo &quot;radian-software/apheleia&quot;)
  :config
  (setf (alist-get &apos;zprint apheleia-formatters)
        &apos;(&quot;zprint&quot; &quot;{:style [:community] :map {:comma? false}}&quot;))
  (setf (alist-get &apos;clojure-mode apheleia-mode-alist) &apos;zprint
        (alist-get &apos;clojure-ts-mode apheleia-mode-alist) &apos;zprint)
  (apheleia-global-mode t))
</code></pre><p>This snippet shows how to install and configure using <a href="https://github.com/radian-software/straight.el">straight.el</a> and <code>use-package</code>. The <code>:config</code> section instructs <code>apheleia</code> under what modes it should run <code>zprint</code> and how to run it.<a href="#fn-1" id="fnref1"><sup>1</sup></a> I found the docstring for <a href="https://github.com/radian-software/apheleia/blob/2b491144fe157867ce1cc4538a9562edff57c891/apheleia-formatters.el#L235-L290">apheleia-formatters</a> to be crucial for figuring out how to hook <code>zprint</code> into apheleia.</p><p>With this setup, your Clojure code will be automatically formatted using zprint every time you save. No more manual formatting needed. I&apos;ve been running with this for a little while now and am enjoying it.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>I don&apos;t actually use <code>:community</code> and have my own custom formatting configuration but am using <code>:community</code> in this post so the snippet is immediately useful to readers.<a href="#fnref1">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/a-couple-tiny-elisp-functions-for-improving-living-in-emacs/index.html</id>
    <link href="https://jakemccrary.com/blog/a-couple-tiny-elisp-functions-for-improving-living-in-emacs/index.html"/>
    <title><![CDATA[A couple tiny elisp functions for improving living in Emacs]]></title>
    <updated>2025-02-17T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>I&apos;ve been using Emacs for nearly twenty years but find it challenging to craft even tiny elisp functions to enhance my workflow. Despite that, I&apos;ve written elisp to enhance my Emacs experience but I often don&apos;t bother fixing small annoyances that are solved relatively easily in other ways. Now that LLMs exist and are reasonably good, they have lowered the bar for creating small quality of life enhancements. Below are two such enhancements.</p><p>These have only been tested and used in Emacs 29.4 on macOS.</p><h2 id="quickly-change-font-sizes">Quickly change font sizes</h2><p>I find myself working on a variety of monitor sizes and resolutions. This function lets me quickly switch between font sizes.</p><pre><code class="language-lisp">(defun jm/choose-font-size ()
  &quot;Choose between three different font sizes: 16, 18, and 20.&quot;
  (interactive)
  (set-face-attribute &apos;default nil :height
                      (* 10 (string-to-number
                             (completing-read &quot;Choose font size: &quot;
                                              (mapcar #&apos;number-to-string &apos;(16 18 20)))))))
</code></pre><h2 id="change-window-split-orientation">Change window split orientation</h2><p>First some definitions from the Emacs manual.</p><blockquote><p>A frame is a screen object that contains one or more Emacs windows (see Windows). It is the kind of object called a “window” in the terminology of graphical environments; but we can’t call it a “window” here, because Emacs uses that word in a different way. - <a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Frames.html">Emacs manual</a></p></blockquote><blockquote><p>A window is an area of the screen that can be used to display a buffer (see Buffers). Windows are grouped into frames (see Frames). Each frame contains at least one window; the user can subdivide a frame into multiple, non-overlapping windows to view several buffers at once. - <a href="https://www.gnu.org/software/emacs/manual/html_node/elisp/Basic-Windows.html">Emacs manual</a></p></blockquote><p>I primarily work in a single frame split into at most two windows. I haven&apos;t found a setup that I like for working with more than two windows, so I avoid it. Often, I&apos;ll want to change the split from vertical to horizontal or horizontal to vertical. I hear a picture is worth a thousand words and a gif even more so below is a demo and and the enabling code.</p><p><img alt="Demo of toggling window orientation" src="/images/toggle-window-orientation.gif" /></p><pre><code class="language-lisp">(defun jm/toggle-window-split ()
  &quot;Toggle between horizontal and vertical split for two windows. Thanks ChatGPT.&quot;
  (interactive)
  (if (= (count-windows) 2)
      (let* ((this-win-buffer (window-buffer))
             (next-win-buffer (window-buffer (next-window)))
             (this-win-edges (window-edges (selected-window)))
             (next-win-edges (window-edges (next-window)))
             (this-win-2nd (not (and (&lt;= (car this-win-edges)
                                         (car next-win-edges))
                                     (&lt;= (cadr this-win-edges)
                                         (cadr next-win-edges)))))
             (splitter
              (if (= (car this-win-edges)
                     (car (window-edges (next-window))))
                  &apos;split-window-horizontally
                &apos;split-window-vertically)))
        (delete-other-windows)
        (let ((first-win (selected-window)))
          (funcall splitter)
          (if this-win-2nd (other-window 1))
          (set-window-buffer (selected-window) this-win-buffer)
          (set-window-buffer (next-window) next-win-buffer)
          (select-window first-win)
          (if this-win-2nd (other-window 1))))))
</code></pre><h2 id="end">End</h2><p>Could those functions be written better? I don&apos;t know, maybe. If I had to read the documentation and write these entirely on my down, these functions wouldn&apos;t exist because the return on investment just wouldn&apos;t be there. These only exist because the time to generate them<a href="#fn-1" id="fnref1"><sup>1</sup></a> is so small that it becomes worth it. AI tools drastically lower the bar for making small routine operations more efficient.</p><p><a href="https://xkcd.com/1205"><img alt="xkcd: Is It Worth The Time?" src="https://imgs.xkcd.com/comics/is_it_worth_the_time.png" /></a></p><ol class="footnotes"><li class="footnote" id="fn-1"><p>And test and make minor edits as needed.<a href="#fnref1">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/reading-in-2024/index.html</id>
    <link href="https://jakemccrary.com/blog/reading-in-2024/index.html"/>
    <title><![CDATA[Reading in 2024]]></title>
    <updated>2025-02-13T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>At the beginning of every year, I take the time to update my records of what I&apos;ve read the previous year and write up a summary.</p><p>Previous summaries: <a href="/blog/2014/01/01/using-incanter-to-review-my-2013-reading/">2013</a>, <a href="/blog/2015/01/08/reading-in-2014/">2014</a>, <a href="/blog/2016/03/13/reading-in-2015/">2015</a>, <a href="/blog/2017/01/04/reading-in-2016/">2016</a>, <a href="/blog/2018/03/03/reading-in-2017/">2017</a>, <a href="/blog/2019/01/21/reading-in-2018/">2018</a>, <a href="/blog/2020/01/11/reading-in-2019/">2019</a>, <a href="/blog/2021/01/24/reading-in-2020/">2020</a>, <a href="/blog/2022/01/02/reading-in-2021/">2021</a>, <a href="/blog/2023/01/14/reading-in-2022/">2022</a>, <a href="/blog/2024/02/18/reading-in-2023/">2023</a>.</p><p>I&apos;ve continued to keep track of my reading using <a href="http://goodreads.com">Goodreads</a>. My <a href="https://www.goodreads.com/user/show/3431614-jake-mccrary">profile</a> has nearly the full list of the books I&apos;ve read since 2010.</p><h2 id="2024-goals">2024 Goals</h2><blockquote><p>I&apos;d like to maintain a regular reading practice. I think this means having a similar number of pages read this year.</p></blockquote><p>Ehh, I don&apos;t think I succeeded in this goal, despite not being too far off the number of pages I read in 2023.</p><blockquote><p>I have a massive stack of books at home that I haven&apos;t read through yet, I&apos;d like to whittle that down some.</p></blockquote><p>I&apos;m pretty sure there is a universal law that a stack of unread books can only grow and my coffee table is proof it is true.</p><h2 id="highlights">Highlights</h2><p>Below are the highlights from 2024. Any title link will bring you to Goodreads.</p><h3 id="five-star-books">Five-star books</h3><h4 id="prophet-song-by-paul-lynch"><a href="https://www.goodreads.com/book/show/158875813-prophet-song">Prophet Song</a> by Paul Lynch</h4><p>This book was beautiful. I cried. The writing is lyrical, the subject matter is tough, and you&apos;ll probably tear up too.</p><p>You may have heard that reading fiction can help you be more empathetic. I could feel this book do that to me.</p><p>Go read it.</p><h4 id="all-fours-by-miranda-july"><a href="https://www.goodreads.com/book/show/197828937-all-fours">All Fours</a> by Miranda July</h4><p>This book is fantastic. I literally laughed out loud multiple times. Miranda July&apos;s writing tickles me in just the right way.</p><h4 id="day-by-michael-cunningham"><a href="https://www.goodreads.com/book/show/145625425-day">Day</a> by Michael Cunningham</h4><p>I guess I haven&apos;t gotten sick of books that deal with the COVID-19 pandemic. Maybe that is because I&apos;ve only read stories on the topic from really solid authors.</p><p>This book gives you a glimpse into the lives of a family on three different days (April 5 2019, 2020 and 2021). It has been nearly a year since I&apos;ve read this and unfortunately I don&apos;t remember what I enjoyed so much about this book.</p><h4 id="looking-for-alaska-by-john-green"><a href="https://www.goodreads.com/book/show/6567453-looking-for-alaska">Looking for Alaska</a> by John Green</h4><p>I don&apos;t know, sometimes you read a young adult book and it just makes an impact. I doubt others will enjoy this as much as I did at the time I read it.</p><h4 id="supercommunicators-by-charles-duhigg"><a href="https://www.goodreads.com/book/show/157998171-supercommunicators">Supercommunicators</a> by Charles Duhigg</h4><p>I&apos;m projecting but I&apos;d guess that most folks would be better off if they became better communicators. And that is exactly what this book says it will do.</p><p>I enjoyed reading this and will benefit by going back through and trying to apply the lessons found in this book.</p><h3 id="other-highlights">Other highlights</h3><h4 id="the-zen-of-climbing-by-francis-sanzaro"><a href="https://www.goodreads.com/book/show/91288912-the-zen-of-climbing">The Zen of Climbing</a> by Francis Sanzaro</h4><p>Some parts were great; some parts were so-so. Seeing Zen principles applied to climbing was thought-provoking.</p><h4 id="ada-palmer&apos;s-terra-ignota-series">Ada Palmer&apos;s <a href="https://www.goodreads.com/series/166200-terra-ignota">Terra Ignota</a> series</h4><p>The right reader will absolutely love this series. Unfortunately, I&apos;m not that reader. Still, I enjoyed the series quite a bit. The world built by Ada Palmer is fascinating. I&apos;ll admit though, I think about this world quite often. This book made an impact.</p><h4 id="small-things-like-these-by-claire-keegan"><a href="https://www.goodreads.com/book/show/59016923-small-things-like-these">Small Things Like These</a> by Claire Keegan</h4><p>A short and powerful book.</p><h4 id="jasper-fforde&apos;s-shades-of-grey-series">Jasper Fforde&apos;s <a href="https://www.goodreads.com/series/51553-shades-of-grey">Shades of Grey</a> series</h4><p>At the time of writing this, I think there are only two books in the series and those are the two that I&apos;ve read.</p><p>This series puts you into a world of rules and structure, with society being built on top of hierarchies based on how much color you can see. It is an interesting coming of age story as the main character learns to navigate this world. This is a fun and interesting world and I hope more books come out in the series.</p><h4 id="i&apos;m-starting-to-worry-about-this-black-box-of-doom-by-jason-pargin"><a href="https://www.goodreads.com/book/show/204193039-i-m-starting-to-worry-about-this-black-box-of-doom">I&apos;m Starting to Worry About This Black Box of Doom</a> by Jason Pargin</h4><p>This book calls out our modern world and our addiction to outrage-as-entertainment, addiction to screens, and the information bubbles we live in.</p><p>I really enjoyed all of that. In a way, this book takes non-fiction books, such as Hans Rosling&apos;s Factfulness (a book about how much better the world has become) and books on social media, filter bubbles, and polarization, and packages it up into a fiction tale demonstrating how it the Internet warps how we perceive each other and our modern world. Some readers might not enjoy characters going on rants and arguments about these topics but I really enjoyed it.</p><p>This book is a good reminder that we&apos;re really more similar than we are different and that modern media (social, news, etc) really divides us and both makes the world seem worse and actually be worse.</p><h4 id="rejection-by-tony-tulathimutte"><a href="https://www.goodreads.com/book/show/204079049-rejection">Rejection</a> by Tony Tulathimutte</h4><p>A collection of stories about rejection that are loosely connected. It is fun satire. I probably could have given this one five stars.</p><h2 id="stats">Stats</h2><p>I read 35 books in 2024.</p><pre><code class="language-org">| Year | # of Pages | # of Books |
|------+------------+------------|
| 2024 |      12919 |         37 |
| 2023 |      14956 |         53 |
| 2022 |      10127 |         35 |
| 2021 |      19564 |         57 |
| 2020 |      12093 |         43 |
| 2019 |      15994 |         42 |
| 2018 |      13538 |         36 |
| 2017 |      18317 |         48 |
| 2016 |      22790 |         59 |
| 2015 |      21689 |         51 |
| 2014 |      24340 |         71 |
| 2013 |      19815 |         60 |
| 2012 |      14208 |         44 |
| 2011 |       9179 |         19 |
| 2010 |      14667 |         46 |
</code></pre><p><img alt="Book and pages count by month" src="/images/reading-by-month-2024.svg" title="Number of books and pages for each month" /></p><p>Electronic books continue to dominate.</p><pre><code class="language-org">|           | 2024 | 2023 | 2022 | 2021 | 2020 | 2019 | 2018 | 2017 | 2016 | 2015 |
|-----------+------+------+------+------+------+------+------+------+------+------|
| audiobook |    0 |    0 |    0 |    0 |    1 |    0 |    0 |    0 |    0 |    0 |
| ebook     |   35 |   51 |   34 |   56 |   41 |   43 |   37 |   37 |   56 |   47 |
| hardcover |    1 |    0 |    1 |    0 |    0 |    1 |    1 |    7 |    0 |    1 |
| paperback |    1 |    2 |    0 |    1 |    1 |    7 |    5 |    5 |    3 |    3 |
</code></pre><p>I did not read many non-fiction books last year.</p><pre><code class="language-org">|                           |   2024 |  2023 | 2022 |   2021 |   2020 |   2019 |   2018 |
|---------------------------+--------+-------+------+--------+--------+--------+--------|
| fiction                   |     33 |    47 |   28 |     46 |     26 |     28 |     29 |
| non-fiction               |      4 |     6 |    7 |     11 |     17 |     23 |     14 |
| fiction:non-fiction ratio | 8.25:1 | 7.8:1 |  4:1 | 4.18:1 | 1.53:1 | 1.22:1 | 2.07:1 |

</code></pre><p>Here is the star rating distribution.</p><pre><code class="language-org">|             | 3 stars | 4 stars | 5 stars |
|-------------+---------+---------+---------|
| fiction     |      17 |      12 |       4 |
| non-fiction |       1 |       2 |       1 |
</code></pre><h2 id="2025-goals">2025 goals</h2><p>For the last month or so, I&apos;ve been better about reading with a regular cadence. I&apos;d like to keep that up.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2024/06/16/scheduling-cron-tasks-in-mixed-time-zones/index.html</id>
    <link href="https://jakemccrary.com/blog/2024/06/16/scheduling-cron-tasks-in-mixed-time-zones/index.html"/>
    <title><![CDATA[Scheduling cron tasks in mixed time zones]]></title>
    <updated>2024-06-16T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Have you ever needed to schedule a repeating task on a Linux host? If so, you&apos;ve probably reached for cron. cron is widely available and reliable; it is a great choice for scheduling tasks.</p><p>Sometimes you find yourself scheduling a task and, ideally, you&apos;d be scheduling that task referencing a different time zone. This is a common need if your programs are interacting with systems hosted in different areas of the world. If one system you interact with starts up at 7 AM <code>Europe/London</code> and another at 8 AM <code>America/New_York</code>, it would be much better to schedule your program to run using times specified in those time zones.</p><p>Why is that preferred?</p><ul><li>If you schedule in your host time zone, you have to convert from the other time zone to your own. This is error prone.</li><li>Different time zones have different Daylights savings shifts. Having to adjust your schedule when your host or target time zone shifts is error prone.</li></ul><p>Luckily, you can do this with cron! At least, with the cronie implementation.</p><p>You do this by specifying the time zone in the crontab with the <code>CRON_TZ</code> variable. Any line after a <code>CRON_TZ</code> specification is scheduled in the specified time zone. This persists until the next <code>CRON_TZ</code> value is specified.</p><p>Below is a sample crontab that schedules four tasks. One is scheduled in the host time zone, two in <code>America/New_York</code>, and one in <code>Europe/London</code>.</p><pre><code>0 7 * * * echo &quot;run at 7 AM in the host time zone&quot;

CRON_TZ=America/New_York
0 7 * * * echo &quot;Run at 7 AM New York&quot;
10 7 * * * echo &quot;Run at 7:10 AM New York&quot;

CRON_TZ=Europe/London
* 8 * * * echo &quot;Run at 8 AM London&quot;
</code></pre><p>The one gotcha with this is that cronie&apos;s behavior is unspecified if the scheduled time ends up in the daylights savings shift of the host machine<a href="#fn-1" id="fnref1"><sup>1</sup></a>. So make sure you don&apos;t do that.</p><p>My team at work has been taking advantage of this feature since early 2023 for scheduling all of our processes start and end times. It has been working great. Prior to figuring<a href="#fn-2" id="fnref2"><sup>2</sup></a> this out, the fall and spring time shifts were sources of issues as various countries shifted on different days. That entire source of problems has been solved through scheduling tasks in the proper time zone.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>We have unit tests that confirm someone hasn&apos;t configured a task to run within one of these periods.<a href="#fnref1">↩</a></p></li><li class="footnote" id="fn-2"><p>Figuring this out was a bit of a chore. Even the Linux experts I talked to weren&apos;t aware of being able to do this. Digging through the source of cronie was how I figured this out. Hopefully this article makes it easier for the next person. Though, now that I know the <code>CRON_TZ</code> solution, it is pretty easy to search and find other folks talking about this.<a href="#fnref2">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2024/02/18/reading-in-2023/index.html</id>
    <link href="https://jakemccrary.com/blog/2024/02/18/reading-in-2023/index.html"/>
    <title><![CDATA[Reading in 2023]]></title>
    <updated>2024-02-18T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>At the beginning of every year (not so much the beginning this year), I take the time to update my records of what I&apos;ve read the previous year and write up a summary.</p><p>Previous summaries: <a href="/blog/2014/01/01/using-incanter-to-review-my-2013-reading/">2013</a>, <a href="/blog/2015/01/08/reading-in-2014/">2014</a>, <a href="/blog/2016/03/13/reading-in-2015/">2015</a>, <a href="/blog/2017/01/04/reading-in-2016/">2016</a>, <a href="/blog/2018/03/03/reading-in-2017/">2017</a>, <a href="/blog/2019/01/21/reading-in-2018/">2018</a>, <a href="/blog/2020/01/11/reading-in-2019/">2019</a>, <a href="/blog/2021/01/24/reading-in-2020/">2020</a>, <a href="/blog/2022/01/02/reading-in-2021/">2021</a>, <a href="/blog/2023/01/14/reading-in-2022/">2022</a>.</p><p>I&apos;ve continued to keep track of my reading using <a href="http://goodreads.com">Goodreads</a>. My <a href="https://www.goodreads.com/user/show/3431614-jake-mccrary">profile</a> has nearly the full list of the books I&apos;ve read since 2010.</p><h2 id="2023-goals">2023 Goals</h2><p>Last year I wrote:</p><blockquote><p>I’d like to do a better job of keeping track of my reading. This should be pretty easy to do.</p></blockquote><blockquote><p>I don’t feel too bad about the reduction in reading but I’d like to read more this year. Some of my reading time has been replaced with worthwhile endeavors but not always.</p></blockquote><blockquote><p>Reading rejuvenates me. I need to keep it a regular part of my life.</p></blockquote><p>Well, I don&apos;t remember how poorly I did in 2022 of keeping tack of reading but I don&apos;t think I did a great job in 2023. Was it better than 2022? Perhaps.</p><p>I didn&apos;t write many reviews for specific books nor did I send out any emails about what I was reading throughout the year. But I did do fewer corrections of the data I had in Goodreads.</p><p>I did read more than I did last year.</p><p>If I were grading myself on how well I achieved my goal, I&apos;d give myself a B.</p><h2 id="highlights">Highlights</h2><p>Below are the highlights from 2023. Any title link will bring you to Goodreads.</p><h3 id="five-star-books">Five-star books</h3><h4 id="sea-of-tranquility-by-emily-st.-john-mandel"><a href="https://www.goodreads.com/book/show/58601245-sea-of-tranquility">Sea of Tranquility</a> by Emily St. John Mandel</h4><p>I started off the year with this book and really enjoyed it. It is a pandemic story that has characters spanning hundreds of years.</p><p>It is a little weird and beautiful. It did an excellent job of conveying feeling.</p><p>This is one of the few books that I did a mini-review of when I finished it. That review:</p><blockquote><p>A beautiful novel. While reading, I found myself rereading parts. Not because the sentences were confusing but because they expressed such a clear feeling.</p></blockquote><p>I highly recommend this book.</p><h4 id="the-first-bad-man-by-miranda-july"><a href="https://www.goodreads.com/book/show/53440834-the-first-bad-man">The First Bad Man</a> by Miranda July</h4><p>This book is real weird and fantastic. The main character is extremely interesting. I had a blast reading this.</p><h4 id="network-effect-by-martha-wells"><a href="https://www.goodreads.com/book/show/52381770-network-effect">Network Effect</a> by Martha Wells</h4><p>I really enjoy this series and thought this as an excellent addition to it.</p><h4 id="tomorrow,-and-tomorrow,-and-tomorrow-by-gabrielle-zevin"><a href="https://www.goodreads.com/book/show/58916147-tomorrow-and-tomorrow-and-tomorrow">Tomorrow, and Tomorrow, and Tomorrow</a> by Gabrielle Zevin</h4><p>This was an excellent book about friendship and creative, collaborative pursuits.</p><h4 id="how-to-live:-27-conflicting-answers-and-one-weird-conclusion-by-derek-sivers"><a href="https://www.goodreads.com/book/show/61351606-how-to-live">How to Live: 27 conflicting answers and one weird conclusion</a> by Derek Sivers</h4><p>A friend offered to send copies of this books to anyone who was up to reading it and I&apos;m glad I spoke up and asked. Each chapter presents a philosophy. And the next chapter usually presents a different, conflicting philosophy. Every chapter is small, so it is easy to read a bit and take some time to reflect.</p><h4 id="the-little-book-of-talent:-52-tips-for-improving-your-skills-by-daniel-coyle"><a href="https://www.goodreads.com/book/show/18652778-the-little-book-of-talent">The Little Book of Talent: 52 Tips for Improving Your Skills</a> by Daniel Coyle</h4><p>A bunch of distilled ideas around improvement. No fluff, just tips.</p><h4 id="wool-omnibus-by-hugh-howey"><a href="https://www.goodreads.com/book/show/13453029-wool-omnibus">Wool Omnibus</a> by Hugh Howey</h4><p>I have mixed feelings about including this book on the list. I reread this series when I was debating watching the television show. Even on a second read, I still enjoyed this story.</p><p>Is it complex and view shattering science fiction? Nahh, not really.</p><p>Is it entertaining? Yep.</p><h3 id="other-highlights">Other highlights</h3><h4 id="chain-gang-all-stars-by-nana-kwame-adjei-brenyah"><a href="https://www.goodreads.com/book/show/61618096-chain-gang-all-stars">Chain-Gang All-Stars</a> by Nana Kwame Adjei-Brenyah</h4><p>This is a really well done book that explores a modern gladiator system of punishment for criminals. It is dark and creative and a solid commentary on modern society.</p><p>Maybe this should have been five-stars.</p><h4 id="the-sparrow-by-mary-doria-russell"><a href="https://www.goodreads.com/book/show/35162756-the-sparrow">The Sparrow</a> by Mary Doria Russell</h4><p>I knew nothing about it going in and thought this was a interesting take on first contact with aliens.</p><h2 id="stats">Stats</h2><p>I read 53 books in 2023.</p><pre><code class="language-org">| Year | # of Pages | # of Books |
|------+------------+------------|
| 2023 |      14956 |         53 |
| 2022 |      10127 |         35 |
| 2021 |      19564 |         57 |
| 2020 |      12093 |         43 |
| 2019 |      15994 |         42 |
| 2018 |      13538 |         36 |
| 2017 |      18317 |         48 |
| 2016 |      22790 |         59 |
| 2015 |      21689 |         51 |
| 2014 |      24340 |         71 |
| 2013 |      19815 |         60 |
| 2012 |      14208 |         44 |
| 2011 |       9179 |         19 |
| 2010 |      14667 |         46 |
</code></pre><p>There are definitely some spiky months in the data.</p><p><img alt="Book and pages count by month" src="/images/reading-by-month-2023.svg" title="Number of books and pages for each month" /></p><p>Unsurprisingly, electronic books are still the dominate format.</p><pre><code class="language-org">|           | 2023 | 2022 | 2021 | 2020 | 2019 | 2018 | 2017 | 2016 | 2015 |
|-----------+------+------+------+------+------+------+------+------+------|
| audiobook |    0 |    0 |    0 |    1 |    0 |    0 |    0 |    0 |    0 |
| ebook     |   51 |   34 |   56 |   41 |   43 |   37 |   37 |   56 |   47 |
| hardcover |    0 |    1 |    0 |    0 |    1 |    1 |    7 |    0 |    1 |
| paperback |    2 |    0 |    1 |    1 |    7 |    5 |    5 |    3 |    3 |
</code></pre><p>Fiction continued to dominate the book count this year. If I did this by page count I think it would tell a different story, as I read quite a few short stories published as Kindle books and this skewed my fiction book count high.</p><pre><code class="language-org">|                           |  2023 | 2022 |   2021 |   2020 |   2019 |   2018 |
|---------------------------+-------+------+--------+--------+--------+--------|
| fiction                   |    47 |   28 |     46 |     26 |     28 |     29 |
| non-fiction               |     6 |    7 |     11 |     17 |     23 |     14 |
| fiction:non-fiction ratio | 7.8:1 |  4:1 | 4.18:1 | 1.53:1 | 1.22:1 | 2.07:1 |

</code></pre><p>Here is the star rating distribution.</p><pre><code class="language-org">|             | 2 stars | 3 stars | 4 stars | 5 stars |
|-------------+---------+---------+---------+---------|
| fiction     |       0 |      21 |      22 |       4 |
| non-fiction |       0 |       1 |       3 |       2 |
</code></pre><h2 id="2024-goals">2024 goals</h2><p>I&apos;d like to maintain a regular reading practice. I think this means having a similar number of pages read this year.</p><p>I have a massive stack of books at home that I haven&apos;t read through yet, I&apos;d like to whittle that down some.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2023/01/14/reading-in-2022/index.html</id>
    <link href="https://jakemccrary.com/blog/2023/01/14/reading-in-2022/index.html"/>
    <title><![CDATA[Reading in 2022]]></title>
    <updated>2023-01-14T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>At the beginning of every year, I look back at my records and reflect on the books I read the previous year.</p><p>Previous years: <a href="/blog/2014/01/01/using-incanter-to-review-my-2013-reading/">2013</a>, <a href="/blog/2015/01/08/reading-in-2014/">2014</a>, <a href="/blog/2016/03/13/reading-in-2015/">2015</a>, <a href="/blog/2017/01/04/reading-in-2016/">2016</a>, <a href="/blog/2018/03/03/reading-in-2017/">2017</a>, <a href="/blog/2019/01/21/reading-in-2018/">2018</a>, <a href="/blog/2020/01/11/reading-in-2019/">2019</a>, <a href="/blog/2021/01/24/reading-in-2020/">2020</a>, <a href="/blog/2022/01/02/reading-in-2021/">2021</a>.</p><p>I&apos;ve continued to keep track of my reading using <a href="http://goodreads.com">Goodreads</a>. My <a href="https://www.goodreads.com/user/show/3431614-jake-mccrary">profile</a> has nearly the full list of the books I&apos;ve read since 2010.</p><p>This year I did a poor job of keeping Goodreads updated. I had somewhat stopped updating Goodreads, or at least caring if I did it accurately, because I thought they were killing the ability to export your data. Luckily, that feature hasn&apos;t been removed so I&apos;m going to continue using the service.</p><h2 id="2022-goals">2022 goals</h2><p>Last year I wrote:</p><blockquote><p>I used to be pretty good at capturing some thoughts upon completion of a book. I haven&apos;t been doing a great job of that. I&apos;d like to do better this year.</p></blockquote><blockquote><p>We&apos;ll see what that entails but it might take the form of having more discipline around sending out some thoughts in the [newsletter]({{ site.subscribe_email }}).</p></blockquote><p>I accomplished absolutely nothing related to the goals I wrote at the beginning of 2022.</p><p>I didn&apos;t write down thoughts on books closer to when I finished them. I didn&apos;t keep my Goodreads data updated. I didn&apos;t send out updates to my newsletter.</p><h2 id="highlights">Highlights</h2><p>Below are some highlights from 2022. The titles link to Goodreads.</p><p>I didn&apos;t write many reviews on Goodreads this year and did not write detailed reviews in this article. I&apos;d encourage you to click the links and read reviews on Goodreads.</p><h3 id="five-star-books">Five-star books</h3><h4 id="liberation-day:-stories-by-george-saunders"><a href="https://www.goodreads.com/book/show/61033655-liberation-day">Liberation Day: Stories</a> by George Saunders</h4><p>I love George Saunders&apos; writing and especially enjoy his short stories. This collection delivered.</p><h4 id="the-lathe-of-heaven-by-ursula-k.-le-guin"><a href="https://www.goodreads.com/book/show/53513520-the-lathe-of-heaven">The Lathe of Heaven</a> by Ursula K. Le Guin</h4><p>A science fiction classic that deserves to be read.</p><h4 id="four-thousand-weeks:-time-management-for-mortals-by-oliver-burkeman"><a href="https://www.goodreads.com/book/show/55742688-four-thousand-weeks">Four Thousand Weeks: Time Management for Mortals</a> by Oliver Burkeman</h4><p>Gist of the book: Life is short and you won&apos;t be able to accomplish everything you desire.</p><p>I enjoyed reading this and would benefit from reading it (or at least the Goodread&apos;s reviews) again. This <a href="https://www.goodreads.com/review/show/4224854256?book_show_action=true">review</a> on Goodreads is excellent.</p><h4 id="breath:-the-new-science-of-a-lost-art-by-james-nestor"><a href="https://www.goodreads.com/book/show/51202930-breath">Breath: The New Science of a Lost Art</a> by James Nestor</h4><p>I found this book fascinating and it made me interested in breathing. It was a nice mix of self-experimentation and reporting on studies. The history of the shape of our faces and how it affects breathing really hooked me into this book.</p><p>I enjoyed the book enough that I also listened to a significant portion of it while on a road trip so my partner could also consume it.</p><h3 id="other-highlights">Other Highlights</h3><h4 id="mount-chicago:-a-novel-by-adam-levin"><a href="https://www.goodreads.com/book/show/61741867-mount-chicago">Mount Chicago: A Novel</a> by Adam Levin</h4><p>This probably could have been rated five stars. It is a ridiculous and long novel that rewards the reader for paying attention.</p><p>Probably the highlights for me were the chapters from the perspective of a pet.</p><p>I thought a couple sections dragged a bit and are what held me back from five stars.</p><h4 id="novelist-as-a-vocation-by-haruki-murakami"><a href="https://www.goodreads.com/book/show/60387334-novelist-as-a-vocation">Novelist as a Vocation</a> by Haruki Murakami</h4><p>To say I like Murakami&apos;s writing would be an understatement, so reading a collection of essays by him about his writing was a pleasure. I learned a bunch about Murakami and his journey to becoming an internationally renowned author. At least as presented by Murakami, aspects of his life seem as surreal as some of his books.</p><p>This didn&apos;t earn five stars because I found myself just not caring about some of the topics. But overall, solid book, especially for someone that enjoys Murakami and enjoys reading about writing.</p><h3 id="octavia-butler&apos;s-patternmaster-series.">Octavia Butler&apos;s <a href="https://www.goodreads.com/series/55489-patternmaster">Patternmaster</a> series.</h3><p>Octavia Butler is one of my favorite authors and I devoured the first three books in this series. I enjoyed the first book, Wild Seed, the best.</p><p>I didn&apos;t realize until writing this up that there is a fourth book in the series and I&apos;ve immediately queued up reading it.</p><h2 id="stats">Stats</h2><p>Compared to my usual number of books and pages read, this was a low reading year. I read 35 books in 2022.</p><pre><code class="language-org">| Year | # of Pages | # of Books |
|------+------------+------------|
| 2022 |      10127 |         35 |
| 2021 |      19564 |         57 |
| 2020 |      12093 |         43 |
| 2019 |      15994 |         42 |
| 2018 |      13538 |         36 |
| 2017 |      18317 |         48 |
| 2016 |      22790 |         59 |
| 2015 |      21689 |         51 |
| 2014 |      24340 |         71 |
| 2013 |      19815 |         60 |
| 2012 |      14208 |         44 |
| 2011 |       9179 |         19 |
| 2010 |      14667 |         46 |
</code></pre><p>Here is a breakdown of books finished by month. There were even a couple months where I allegedly didn&apos;t finish a single book, though I wonder if that is actually true or of it is a data issue.</p><p><img alt="Book and pages count by month" src="/images/reading-by-month-2022.svg" title="Number of books and pages for each month" /></p><p>Electronic books continue to be the dominant format. Audio book could be 0.5 for how much I listened to Breath while on a road trip.</p><pre><code class="language-org">|           | 2022 | 2021 | 2020 | 2019 | 2018 | 2017 | 2016 | 2015 |
|-----------+------+------+------+------+------+------+------+------|
| audiobook |    0 |    0 |    1 |    0 |    0 |    0 |    0 |    0 |
| ebook     |   34 |   56 |   41 |   43 |   37 |   37 |   56 |   47 |
| hardcover |    1 |    0 |    0 |    1 |    1 |    7 |    0 |    1 |
| paperback |    0 |    1 |    1 |    7 |    5 |    5 |    3 |    3 |
</code></pre><p>Fiction dominated this year.</p><pre><code class="language-org">|                           | 2022 |   2021 |   2020 |   2019 |   2018 |
|---------------------------+------+--------+--------+--------+--------|
| fiction                   |   28 |     46 |     26 |     28 |     29 |
| non-fiction               |    7 |     11 |     17 |     23 |     14 |
| fiction:non-fiction ratio |  4:1 | 4.18:1 | 1.53:1 | 1.22:1 | 2.07:1 |

</code></pre><p>Here is the star rating distribution.</p><pre><code class="language-org">|             | 2 stars | 3 stars | 4 stars | 5 stars |
|-------------+---------+---------+---------+---------|
| fiction     |       2 |      10 |      14 |       2 |
| non-fiction |       0 |       0 |       5 |       2 |
</code></pre><h2 id="2023-goals">2023 goals</h2><p>I&apos;d like to do a better job of keeping track of my reading. This should be pretty easy to do.</p><p>I don&apos;t feel too bad about the reduction in reading but I&apos;d like to read more this year. Some of my reading time has been replaced with worthwhile endeavors but not always.</p><p>Reading rejuvenates me. I need to keep it a regular part of my life.</p><p>Have any book recommendations? Please shoot me an email or leave a comment.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2022/11/13/bookmarklets-on-mobile-are-useful/index.html</id>
    <link href="https://jakemccrary.com/blog/2022/11/13/bookmarklets-on-mobile-are-useful/index.html"/>
    <title><![CDATA[Bookmarklets on mobile are useful]]></title>
    <updated>2022-11-13T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Bookmarklets, little snippets of JavaScript that you keep around as a bookmark, are useful. They let you execute some JavaScript to perform almost any action you want on a website.</p><p>Some bookmarklets I use on my desktop browser include:</p><ol><li>A collection of bookmarklets that let you change the playback speed of most embedded videos.</li><li>A bookmarklet to manipulate the URL of the page you&apos;re visiting.</li><li>A <a href="https://pinboard.in/howto/">bookmarklet</a> to save the current page&apos;s URL to pinboard.in.</li></ol><p>For years, I thought I was restricted to only using bookmarklets in my desktop web browser. I hadn&apos;t effectively used mobile bookmarks before and thought that clicking them would be a huge pain.</p><p>It turns out, I was wrong! I recently learned that if you start typing a bookmark&apos;s title into your mobile browser&apos;s location bar, it will let you select the bookmark. This means you can easily execute a bookmarklet just by starting to type its name and clicking it when it appears. This &quot;search for bookmark in location bar&quot; technique works with at least Google Chrome and Brave running in Android.</p><p>Below are the two bookmarklets I use regularly on my phone. They exist to bypass paywalls.</p><p>This one prepends <code>http://archive.is/</code> to the current URL:</p><pre><code class="language-javascript">javascript:(function() {window.location=&quot;http://archive.is/&quot;+window.location.toString();}())
</code></pre><p>This one changes <code>theatlantic.com</code> to <code>theatlantic.com.</code> (though it no longer gets around their paywall):</p><pre><code class="language-javascript">javascript:(function() {window.location=window.location.href.replace(/theatlantic.com/, &apos;theatlantic.com.&apos;);}())
</code></pre><p>To get them onto my phone, I added them a bookmarks on my laptop&apos;s Chrome and synced them to my mobile phone. Once in my mobile Chrome, I edited the bookmark in mobile Chrome, copied the code, and pasted it into a bookmark in Brave.</p><p>I type three characters into my mobile browser&apos;s location bar before I can select either of these bookmarklets. That is quicker than editing the URLs by hand and has improved the experience of reading articles on my phone.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2022/01/02/reading-in-2021/index.html</id>
    <link href="https://jakemccrary.com/blog/2022/01/02/reading-in-2021/index.html"/>
    <title><![CDATA[Reading in 2021]]></title>
    <updated>2022-01-02T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>At the beginning of every year, I reflect on books I&apos;ve read in the previous year. I take a look at my records, fix errors, and think about reading goals for the upcoming year.</p><p>Here are links to my previous end-of-year reflections: <a href="/blog/2014/01/01/using-incanter-to-review-my-2013-reading/">2013</a>, <a href="/blog/2015/01/08/reading-in-2014/">2014</a>, <a href="/blog/2016/03/13/reading-in-2015/">2015</a>, <a href="/blog/2017/01/04/reading-in-2016/">2016</a>, <a href="/blog/2018/03/03/reading-in-2017/">2017</a>, <a href="/blog/2019/01/21/reading-in-2018/">2018</a>, <a href="/blog/2020/01/11/reading-in-2019/">2019</a>, and <a href="/blog/2021/01/24/reading-in-2020/">2020</a>.</p><p>I&apos;ve continued to keep track of my reading using <a href="http://goodreads.com">Goodreads</a>. My <a href="https://www.goodreads.com/user/show/3431614-jake-mccrary">profile</a> has nearly the full list of the books I&apos;ve read since 2010. <a href="https://www.goodreads.com/review/list/3431614-jake-mccrary?shelf=read_2021">Here</a> is my 2021.</p><h2 id="2021-goals">2021 Goals</h2><p>Last year I wrote:</p><blockquote><p>I have quite a few unread books sitting on my virtual and physical bookshelf. This feels like setting a really low-bar but this year I&apos;d like to read some of these unread-but-owned books.</p></blockquote><blockquote><p>I&apos;m also planning on reading at least one book on writing and one book on climbing. This goal is almost a subset of the above goal as I have books on both these topics sitting on my shelf.</p></blockquote><p>Did I achieve those goals? No.</p><p>Looking through my list of read books, I think only one of those a book I owned at the beginning of 2021. I did not read <em>some</em> already owned books; I read a single already owned book.</p><p>I did read a book on writing, George Saunders&apos; <em>A Swim in a Pond in the Rain</em>, and multiple climbing books, <em>Rock Climbing Technique</em> by John Kettle and <em><a href="https://www.goodreads.com/book/show/57691814-rock-climbing-in-kentucky-s-red-river-gorge">Rock Climbing in Kentucky&apos;s Red River Gorge</a></em> by James Maples.</p><p>Early in 2020, at the request of some readers of this site, I started a [mailing list]({{ site.subscribe_email }}). During 2020 I used this newsletter as a way to notify subscribers of new articles posted to this website and write up short blurbs on books I had finished. Except for a single email, I also didn&apos;t send out any updates to the newsletter.</p><p>This is partially because I didn&apos;t write many articles last year. I generally try to write about one article a month but I did not do that in 2021. We&apos;ll see if I pick back up this habit in 2022.</p><h2 id="highlights">Highlights</h2><p>Below are some highlights from 2021. The titles link to Goodreads.</p><p>I didn&apos;t write many reviews on Goodreads this year and did not write detailed reviews in this article. I&apos;d encourage you to click the links and read reviews on Goodreads.</p><h3 id="five-star-books">Five-star books</h3><h4 id="dune-by-frank-herbert"><a href="https://www.goodreads.com/book/show/44767458-dune">Dune</a> by Frank Herbert</h4><p>This was my third time reading Dune. I read it in preparation for seeing the 2021 film.</p><p>I&apos;m a huge fan of Dune but parts of it definitely haven&apos;t aged well. I think the story manages to be complex and have plenty of movement but somehow isn&apos;t overwhelming. I thought the film did a pretty good job of capturing that.</p><p>The first time I read Dune, I also made the very first <a href="https://jakemcc.gumroad.com/l/dune-dictionary">Kindle Dune Dictionary</a>. If you are reading on a Kindle and haven&apos;t read the book before, I&apos;d recommend purchasing the dictionary. I didn&apos;t find it useful  on my third time through the book but it made my first read through better.</p><h4 id="memory-by-lois-mcmaster-bujold"><a href="https://www.goodreads.com/book/show/12621704-memory">Memory</a> by Lois McMaster Bujold</h4><p>In August and September, I pretty much devoured the entire <a href="https://www.goodreads.com/series/98250-vorkosigan-saga-publication-order">Vorkosigan Saga</a>. I read all these books pretty much back to back and can barely distinguish them.</p><p>I&apos;d recommend the series. It was a fun series and there are quite a few books in it. For some reason, this book stood out and is the only one I gave five stars.</p><h4 id="a-memory-called-empire-and-a-desolation-called-peace-by-arkady-martine"><a href="https://www.goodreads.com/book/show/39873472-a-memory-called-empire">A Memory Called Empire</a> and <a href="https://www.goodreads.com/book/show/45154547-a-desolation-called-peace">A Desolation Called Peace</a> by Arkady Martine</h4><p>These are two incredible books. Arkady Martine built a great world. Politics, aliens, and poetry all show up in these books and it is great.</p><p>Go read some reviews (perhaps <a href="https://www.goodreads.com/review/show/2754542016?book_show_action=true">this</a> and <a href="https://www.goodreads.com/review/show/3361651782?book_show_action=true">this</a>) and then go start this series.</p><h4 id="the-overstory-by-richard-powers"><a href="https://www.goodreads.com/book/show/36075657-the-overstory">The Overstory</a> by Richard Powers</h4><p>The book starts with what feels like a collection of short stories and grows into a story of struggle, triumph, and failure. I can&apos;t point to what made this book stand out to me but a I really enjoyed it. It might have had some moments that dragged a bit but I still loved it.</p><h4 id="this-is-how-you-lose-the-time-war-by-amal-el-mohtar"><a href="https://www.goodreads.com/book/show/55710260-this-is-how-you-lose-the-time-war">This Is How You Lose the Time War</a> by Amal El-Mohtar</h4><p>I went into this book knowing nothing about it and found it beautiful. The writing is lyrical and the way the story is told worked really well.</p><p>The book isn&apos;t that long and I absolutely devoured it.</p><h4 id="zikora-by-chimamanda-ngozi-adichie"><a href="https://www.goodreads.com/book/show/55612579-zikora">Zikora</a> by Chimamanda Ngozi Adichie</h4><p>This is a 35 page short story. Go spend the short time it takes to read it. And then go read the rest of Adichie&apos;s writing.</p><h4 id="a-swim-in-a-pond-in-the-rain:-in-which-four-russians-give-a-master-class-on-writing,-reading,-and-life-by-george-saunders"><a href="https://www.goodreads.com/book/show/53487237-a-swim-in-a-pond-in-the-rain">A Swim in a Pond in the Rain: In Which Four Russians Give a Master Class on Writing, Reading, and Life</a> by George Saunders</h4><p>This book is fantastic. Generally, the format of the book is that you read a short story and then read George Saunders&apos; thoughts about that story.</p><p>This book slightly changed how I think about stories.</p><h4 id="dept.-of-speculation-by-jenny-offill"><a href="https://www.goodreads.com/book/show/19635587-dept-of-speculation">Dept. of Speculation</a> by Jenny Offill</h4><p>This is a relatively short, beautiful book. I experienced a full range of emotions while reading it.</p><h4 id="think-again:-the-power-of-knowing-what-you-don&apos;t-know-by-adam-m.-grant"><a href="https://www.goodreads.com/book/show/55614248-think-again">Think Again: The Power of Knowing What You Don&apos;t Know</a> by Adam M. Grant</h4><p>A solid book on the benefits of rethinking your positions. A very short summary is that it is good to update your beliefs and be curious. There is little benefit to being wrong longer. Learn how to rejoice in correcting your beliefs and embrace updating your viewpoints.</p><p>There is a some overlap of concepts in this book and the book Moral Tribes. If you are also well-versed in cognitive biases, parts of this book will be a repeat.</p><h4 id="the-devops-handbook:-how-to-create-world-class-agility,-reliability,-and-security-in-technology-organizations-by-gene-kim"><a href="https://www.goodreads.com/book/show/32605769-the-devops-handbook">The DevOps Handbook: How to Create World-Class Agility, Reliability, and Security in Technology Organizations</a> by Gene Kim</h4><p>I think a second edition came out immediately after I finished reading the first edition. Hopefully the second edition has high quality updates as the original edition of this book is pretty solid. I&apos;ve lived the vision promoted by this book and it is a good place to be.</p><h4 id="rock-climbing-technique:-the-practical-guide-to-movement-mastery-by-john-kettle"><a href="https://www.goodreads.com/book/show/40526042-rock-climbing-technique">Rock Climbing Technique: The Practical Guide to Movement Mastery</a> by John Kettle</h4><p>A short book full of specific drills that are intended to improve your skill in climbing. This book isn&apos;t about improving your strength, flexibility, or endurance. It is all about getting better at movement and paying attention to your movement patterns.</p><p>I&apos;ve taken some of these drills and incorporated them into my climbing practice. I plan on digging back into this book and incorporating more of them.</p><h4 id="drug-use-for-grown-ups:-chasing-liberty-in-the-land-of-fear-by-carl-l.-hart"><a href="https://www.goodreads.com/book/show/54965614-drug-use-for-grown-ups">Drug Use for Grown-Ups: Chasing Liberty in the Land of Fear</a> by Carl L. Hart</h4><p>This book is a mix of the author&apos;s personal experience, policy, and science and makes the argument that drugs should be legal. Probably worth reading if you have any sort of reaction to that last sentence.</p><p>Reading some reviews on Goodreads gives a fairly balanced view of what this book is about. Even if I personally gave this book five stars, I find myself agreeing with a wide range of reviews by others.</p><h3 id="other-highlights">Other Highlights</h3><h4 id="a-psalm-for-the-wild-built-by-becky-chambers"><a href="https://www.goodreads.com/book/show/40864002-a-psalm-for-the-wild-built">A Psalm for the Wild-Built</a> by Becky Chambers</h4><p>A couple other reviews (<a href="https://www.goodreads.com/review/show/3567191782?book_show_action=true">one</a>, <a href="https://www.goodreads.com/review/show/3762535675?book_show_action=true">two</a>) described this book as comforting science fiction. I think that is a great description.</p><p>The world feels cozy. It is full of generally nice folks going about their lives and interacting with each other over tea. This main character in this book is non-binary and you get to hang out with them as they live their life. It feels like a nice place to be with reasonable folks and respect between humans, nature, and robots.</p><p>It is a relaxing read that feels like a gentle fable that muses on life and what it means to exist.</p><h4 id="rock-climbing-in-kentucky&apos;s-red-river-gorge-by-james-maples"><a href="https://www.goodreads.com/book/show/57691814-rock-climbing-in-kentucky-s-red-river-gorge">Rock Climbing in Kentucky&apos;s Red River Gorge</a> by James Maples</h4><p>This book has a very narrow audience. If you have heard about the Red River Gorge, rock climb, and are interested in the history of the area you should read this book.</p><p>I learned a lot about one of my favorite places to rock climb.</p><h4 id="crossroads-by-jonathan-franzen"><a href="https://www.goodreads.com/book/show/56247202-crossroads">Crossroads</a> by Jonathan Franzen</h4><p>There is a really good chance I should have given this book five stars. It was great. This book tells the story of the Hildebrandt family through interweaving perspectives of the family&apos;s members. The characters are complex and the perspectives are interesting. I hope Franzen can continue to deliver this level of story in the sequels.</p><p>This <a href="https://www.goodreads.com/review/show/4264230041?book_show_action=true">goodreads review</a> does a great job of selling the book.</p><h4 id="the-art-of-gathering-by-priya-parker"><a href="https://www.goodreads.com/book/show/38466356-the-art-of-gathering">The Art of Gathering</a> by Priya Parker</h4><p>This book does a wonderful job of describing what makes a gathering great. It was a bit hopeful to read this book early in 2021.</p><h4 id="all-systems-red-by-martha-wells"><a href="https://www.goodreads.com/book/show/32758901-all-systems-red">All Systems Red</a> by Martha Wells</h4><p>A fun novella told from the perspective of an android.</p><h4 id="klara-and-the-sun-by-kazuo-ishiguro"><a href="https://www.goodreads.com/book/show/54250259-klara-and-the-sun">Klara and the Sun</a> by Kazuo Ishiguro</h4><p>This book was beautiful. It manages to feel slightly off and this is completely appropriate given the narrator. This leads to some really amusing bits of writing.</p><h2 id="stats">Stats</h2><p>I thought I had read less this year than I had in recent years but I was wrong. I read 57 books and 19,564 pages in 2021.</p><pre><code class="language-org">| Year | # of Pages | # of Books |
|------+------------+------------|
| 2021 |      19564 |         57 |
| 2020 |      12093 |         43 |
| 2019 |      15994 |         42 |
| 2018 |      13538 |         36 |
| 2017 |      18317 |         48 |
| 2016 |      22790 |         59 |
| 2015 |      21689 |         51 |
| 2014 |      24340 |         71 |
| 2013 |      19815 |         60 |
| 2012 |      14208 |         44 |
| 2011 |       9179 |         19 |
| 2010 |      14667 |         46 |
</code></pre><p>Here is a breakdown of books finished by month. I can tell from looking at August and September that I started and finished the Vorkosigan series during those months.</p><p><img alt="Book and pages count by month" src="/images/reading-by-month-2021.png" title="Number of books and pages for each month" /></p><p>Electronic books continue to be the dominant format.</p><pre><code class="language-org">|           | 2021 | 2020 | 2019 | 2018 | 2017 | 2016 | 2015 |
|-----------+------+------+------+------+------+------+------|
| audiobook |    0 |    1 |    0 |    0 |    0 |    0 |    0 |
| ebook     |   56 |   41 |   43 |   37 |   37 |   56 |   47 |
| hardcover |    0 |    0 |    1 |    1 |    7 |    0 |    1 |
| paperback |    1 |    1 |    7 |    5 |    5 |    3 |    3 |
</code></pre><p>Fiction dominated this year.</p><pre><code class="language-org">|                           |   2021 |   2020 |   2019 |   2018 |
|---------------------------+--------+--------+--------+--------|
| fiction                   |     46 |     26 |     28 |     29 |
| non-fiction               |     11 |     17 |     23 |     14 |
| fiction:non-fiction ratio | 4.18:1 | 1.53:1 | 1.22:1 | 2.07:1 |

</code></pre><p>Here is the star rating distribution.</p><pre><code class="language-org">|             | 2 stars | 3 stars | 4 stars | 5 stars |
|-------------+---------+---------+---------+---------|
| fiction     |       2 |      17 |      19 |       8 |
| non-fiction |       1 |       2 |       3 |       5 |
</code></pre><h2 id="2022-goals">2022 goals</h2><p>I used to be pretty good at capturing some thoughts upon completion of a book. I haven&apos;t been doing a great job of that. I&apos;d like to do better this year.</p><p>We&apos;ll see what that entails but it might take the form of having more discipline around sending out some thoughts in the [newsletter]({{ site.subscribe_email }}).</p><p>Have any book recommendations? Please shoot me an email or leave a comment.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2021/09/11/tests-can-act-as-living-documentation/index.html</id>
    <link href="https://jakemccrary.com/blog/2021/09/11/tests-can-act-as-living-documentation/index.html"/>
    <title><![CDATA[Tests are living documentation]]></title>
    <updated>2021-09-11T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Tests can serve many purposes.</p><p>You might write tests as a way of driving the design of your software. Other tests might be written in response to a discovered bug and, if written first, those tests you know when you&apos;ve fixed the bug and act as guardrails preventing the reintroduction of that bug. Tests can also be used to confirm you haven&apos;t changed behavior while refactoring.</p><p>Tests can also be used as documentation. Unlike non-executable documentation, tests will always match the implementation&apos;s behavior.</p><p>An example in a comment or other documentation deserves to be in a test. Take the following sketch of a Clojure function:</p><pre><code class="language-clojure">(defn confobulate
  &quot;Takes a string and transforms it to the confobulated form. Examples:
  - \&quot;alice\&quot; -&gt; \&quot;EcilA\&quot;
  - \&quot;//yolo1\&quot; -&gt; \&quot;//oneOloY\&quot;
  &quot;
  [s]
  (-&gt; s
      ;; insert some work here, not going to implement this
      ))
</code></pre><p>The docstring has examples in it to aid humans in understanding its behavior. These examples are useful! But they stop being useful and start being dangerous when they stop being accurate.</p><p>We can use unit tests to keep examples like this correct. You can write comments near the assertions letting future readers know about the documentation that needs to be updated if behavior changes.</p><pre><code class="language-clojure">(deftest confobulate-should-ignore-slashes
  ;; If this assertion changes the docstring needs to be updated
  (is (= &quot;//oneOloY&quot; (confobulate &quot;//yolo1&quot;))))

(deftest confobulate-reverses-and-capitalizes
  ;; If this assertion changes the docstring needs to be updated
  (is (= &quot;alice&quot; (confobulate &quot;EcilA&quot;))))
</code></pre><p>Any example in a comment or other non-executable documentation should be an assertion in a unit test. You&apos;ve already taken the time to document the behavior; take the time to figure out how to document it in a way that will fail if the behavior changes.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2021/08/07/improve-your-tests-by-picking-better-constants/index.html</id>
    <link href="https://jakemccrary.com/blog/2021/08/07/improve-your-tests-by-picking-better-constants/index.html"/>
    <title><![CDATA[Improve your tests by picking better constants]]></title>
    <updated>2021-08-07T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>The constants you use in unit tests matter. Like test and variable names, they can improve the readability of your code and make it easier to understand test failures.</p><p>Imagine the following.</p><p>A new developer joins your team and asks a question about how the code resolves config values. You are unsure of the details so you pair up with the new teammate to dig into the code.</p><p>You know the codebase uses a relatively simple key-value pair concept for configuration. It reads keys and values from a known files and, based on some rules, either ignores or overrides values when keys are duplicated across files.</p><p><code>config-value</code> is the function that looks up the value for a particular configuration key, represented as a <code>string</code>. This function takes three arguments: an in-memory representation of the configuration files, the key to lookup, and the mode to operate in. You know the mode is important in influencing how config resolution works but you don&apos;t remember the details.</p><p>Luckily for you and your pair, the codebase has plenty of unit tests. The two of you dive in and look at some tests, hoping to understand how config resolution works.</p><pre><code class="language-clojure">(def config {&quot;scratch.conf&quot; {&quot;a&quot; &quot;1&quot;}

             &quot;development.conf&quot; {&quot;a&quot; &quot;2&quot;
                                 &quot;b&quot; &quot;2&quot;}

             &quot;application.conf&quot; {&quot;a&quot; &quot;3&quot;
                                 &quot;b&quot; &quot;3&quot;
                                 &quot;c&quot; &quot;3&quot;}})

(deftest handles-overrides-in-dev-mode
  (is (= &quot;1&quot; (config-value config &quot;a&quot; :dev)))
  (is (= &quot;2&quot; (config-value config &quot;b&quot; :dev)))
  (is (= &quot;3&quot; (config-value config &quot;c&quot; :dev))))

(deftest handles-overrides-in-prod-mode
  (is (= &quot;3&quot; (config-value config &quot;a&quot; :prod)))
  (is (= &quot;3&quot; (config-value config &quot;b&quot; :prod)))
  (is (= &quot;3&quot; (config-value config &quot;c&quot; :prod))))
</code></pre><p>It is great that these tests exist but they could be clearer. They aren&apos;t terrible but you have to work a bit understand what is happening.</p><p>When reading <code>(= &quot;2&quot; (config-value config &quot;b&quot; :dev))</code>, what does <code>&quot;2&quot;</code> represent? What does <code>&quot;b&quot;</code> mean? You have to either keep the value of <code>config</code> in your brain or keep glancing up in the file to recall what it is.</p><p>This isn&apos;t great. This adds cognitive overhead that doesn&apos;t need to be there.</p><p>There are a few ways these tests could be improved One way is through using better constants. Let&apos;s do a quick rewrite.</p><pre><code class="language-clojure">(def config {&quot;scratch.conf&quot; {&quot;in dev+app+scratch&quot; &quot;from scratch&quot;}

             &quot;development.conf&quot; {&quot;in dev+app+scratch&quot; &quot;from development&quot;
                                 &quot;in dev+app&quot; &quot;from development&quot;}

             &quot;application.conf&quot; {&quot;in dev+app+scratch&quot; &quot;from application&quot;
                                 &quot;in dev+app&quot; &quot;from application&quot;
                                 &quot;in app&quot; &quot;from application&quot;}})

(deftest handles-overrides-in-dev-mode
  (is (= &quot;from scratch&quot; (config-value config &quot;in dev+app+scratch&quot; :dev)))
  (is (= &quot;from development&quot; (config-value config &quot;in dev+app&quot; :dev)))
  (is (= &quot;from application&quot; (config-value config &quot;in app&quot; :dev))))

(deftest handles-overrides-in-prod-mode
  (is (= &quot;from application&quot; (config-value config &quot;in dev+app+scratch&quot; :prod)))
  (is (= &quot;from application&quot; (config-value config &quot;in dev+app&quot; :prod)))
  (is (= &quot;from application&quot; (config-value config &quot;in app&quot; :prod))))
</code></pre><p>These are the same tests but with different constants. Those constants make a huge difference. This change has made the tests more legible. You no longer need to remember the value of <code>config</code> or keep glancing up at it to understand the assertions in a test.</p><p>You can read <code>(= &quot;from development&quot; (config-value config &quot;in dev+app&quot; :dev))</code> and have a pretty solid idea that you are looking up a key found in both <code>development.conf</code> and <code>application.conf</code> and while in <code>:dev</code> mode expect the value from <code>development.conf</code>.</p><p>The new constants provide clues about what the test expects. You can read and understand the assertions without keeping much state in your head.</p><p>This increases the legibility of the tests and is useful when a test fails. Which of the following is clearer?</p><pre><code>FAIL in (handles-overrides-in-dev-mode)
expected: &quot;2&quot;
  actual: &quot;3&quot;
    diff: - &quot;2&quot;
          + &quot;3&quot;
</code></pre><pre><code>FAIL in (handles-overrides-in-dev-mode)
expected: &quot;from development&quot;
  actual: &quot;from application&quot;
    diff: - &quot;from development&quot;
          + &quot;from application&quot;
</code></pre><p>The second one is clearer. You can read it and form a hypothesis about what might be broken.</p><p>Well chosen constants reduce the state a person needs to keep in their head. This makes tests easier to understand. Good constants also make test failures easier to understand. Just like good variable names, good constants increase the readability of our tests.</p><p>It is well worth placing some extra thought into the constants found in your tests.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2021/01/24/reading-in-2020/index.html</id>
    <link href="https://jakemccrary.com/blog/2021/01/24/reading-in-2020/index.html"/>
    <title><![CDATA[Reading in 2020]]></title>
    <updated>2021-01-24T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>At the beginning of every year I reflect on my reading from the previous year. I take a look at my records, fix errors, and think about reading goals for the upcoming year.</p><p>Here are links to my previous end-of-year reflections: <a href="/blog/2014/01/01/using-incanter-to-review-my-2013-reading/">2013</a>, <a href="/blog/2015/01/08/reading-in-2014/">2014</a>, <a href="/blog/2016/03/13/reading-in-2015/">2015</a>, <a href="/blog/2017/01/04/reading-in-2016/">2016</a>, <a href="/blog/2018/03/03/reading-in-2017/">2017</a>, <a href="/blog/2019/01/21/reading-in-2018/">2018</a>, and <a href="/blog/2020/01/11/reading-in-2019/">2019</a>.</p><p>I&apos;ve continued to keep track of my reading using <a href="http://goodreads.com">Goodreads</a>. My <a href="https://www.goodreads.com/user/show/3431614-jake-mccrary">profile</a> has nearly the full list of the books I&apos;ve read since 2010. <a href="https://www.goodreads.com/review/list/3431614-jake-mccrary?shelf=read_2020">Here</a> is my 2020.</p><h2 id="2020-goals">2020 Goals</h2><p>Last year I wrote:</p><blockquote><p>I was encouraged by how many non-fiction books I read this year and how many of them ended up earning a five star rating. I&apos;d like to continue that trend of reading high-quality non-fiction books.</p></blockquote><blockquote><p>I&apos;ve also been reading a lot of books but I haven&apos;t always been the best at trying to consciously apply the lessons from those books. I&apos;m going to try to improve that this year.</p></blockquote><blockquote><p>Those are pretty fuzzy goals but I&apos;m alright with that.</p></blockquote><p>I&apos;ll come back at the end of this article and reflect on if I hit it or not.</p><h2 id="highlights">Highlights</h2><p>Here are my five star books from 2020. The titles are Amazon.</p><p>If I wrote a review on Goodreads then the <code>my review</code> link will take you there. In the last couple of years, I&apos;ve been writing fewer reviews on Goodreads than in the past so many books do not have a review there.</p><p>If you&apos;re missing these reviews, I have started sending out an email every month or two and it frequently includes small reviews of what I&apos;ve read since the previous email. You can subscribe to that [here]({{ site.subscribe_email }}).</p><p>Here are the 2020 five star books:</p><ul><li><a href="https://www.amazon.com/Hard-Truth-Simple-Become-Climber/dp/1734103604">The Hard Truth: Simple Ways to Become a Better Climber</a> by Kris Hampton (<a href="https://www.goodreads.com/review/show/3315020265">my review</a>)</li><li><a href="https://www.amazon.com/Body-Keeps-Score-Healing-Trauma/dp/0143127748">The Body Keeps the Score: Brain, Mind, and Body in the Healing of Trauma</a> by Bessel A. van der Kolk</li><li><a href="https://www.amazon.com/Come-You-Are-Surprising-Transform/dp/1476762090">Come as You Are: The Surprising New Science that Will Transform Your Sex Life</a> by Emily Nagoski</li><li><a href="https://www.amazon.com/Change-Your-Mind-Consciousness-Transcendence/dp/0735224153">How to Change Your Mind: What the New Science of Psychedelics Teaches Us About Consciousness, Dying, Addiction, Depression, and Transcendence</a> by Michael Pollan</li><li><a href="https://www.amazon.com/Longer-Talking-White-People-About-ebook/dp/B06WWPX2YF">Why I’m No Longer Talking to White People About Race</a> by Reni Eddo-Lodge</li><li><a href="https://www.amazon.com/Ijeawele-Feminist-Manifesto-Fifteen-Suggestions/dp/0525434801">Dear Ijeawele; or, A Feminist Manifesto in Fifteen Suggestions</a> by Chimamanda Ngozi Adichie</li><li><a href="https://www.amazon.com/Americanah-Ala-Notable-Books-Adults-ebook/dp/B00A9ET4MC">Americanah</a> by Chimamanda Ngozi Adichie</li><li><a href="https://www.amazon.com/Half-Yellow-Chimamanda-Ngozi-Adichie/dp/1400095204">Half of a Yellow Sun</a> by Chimamanda Ngozi Adichie</li><li><a href="https://www.amazon.com/Diamond-Age-Illustrated-Primer-Spectra-ebook/dp/B000FBJCKI">The Diamond Age</a> by Neal Stephenson (<a href="https://www.goodreads.com/review/show/94072683">my review</a>)</li><li>Parable of the Sower (Earthseed, #1) by Octavia E. Butler</li><li><a href="https://www.amazon.com/Parable-Sower-Octavia-Butler/dp/1538732181">Parable of the Talents (Earthseed, #2)</a> by Octavia E. Butler</li></ul><h4 id="the-hard-truth:-simple-ways-to-become-a-better-climber-by-kris-hampton">The Hard Truth: Simple Ways to Become a Better Climber by Kris Hampton</h4><p>This is an excellent dose of wisdom about climbing and improving your performance. It does this through suggestions of how to change your mental relationship with climbing. Improving is about putting in the work, reflecting, and trying hard.</p><h4 id="the-body-keeps-the-score:-brain,-mind,-and-body-in-the-healing-of-trauma-by-bessel-a.-van-der-kolk">The Body Keeps the Score: Brain, Mind, and Body in the Healing of Trauma by Bessel A. van der Kolk</h4><p>I really enjoyed this book and made hundreds of highlights while reading it on my Kindle. I&apos;d suggest reading reviews on <a href="https://www.goodreads.com/book/show/22268254-the-body-keeps-the-score">Goodreads</a> and seeing if it is something that would be interesting for you.</p><h4 id="come-as-you-are:-the-surprising-new-science-that-will-transform-your-sex-life-by-emily-nagoski">Come as You Are: The Surprising New Science that Will Transform Your Sex Life by Emily Nagoski</h4><p>This was a good book that, unlike what the subtitle claims, did not transform my sex life. But I didn&apos;t go into it expecting that. I&apos;m not the main audience for this book but still got some value from it. I particularly enjoyed the parts that talked about stress, responses to stress, and emotional systems.</p><h4 id="how-to-change-your-mind-by-michael-pollan">How to Change Your Mind by Michael Pollan</h4><p>This book is about psychedelics, such as LSD and psilocybin. It combines the history of these substances, old and new research being done with them, and sort of a travelogue of Michael Pollan&apos;s growing experience with these substances.</p><h4 id="why-i’m-no-longer-talking-to-white-people-about-race-by-reni-eddo-lodge">Why I’m No Longer Talking to White People About Race by Reni Eddo-Lodge</h4><p>I&apos;ll just link to a <a href="https://www.goodreads.com/review/show/2697985490?book_show_action=false">friend&apos;s review</a> of this book.</p><h4 id="the-author-chimamanda-ngozi-adichie">The author Chimamanda Ngozi Adichie</h4><p>I devoured all of her writing this year, both fiction and non-fiction, and the highlights are above. None of her writing earned less than four stars.</p><p>Between starting and finishing writing this article, I learned she published a new short story, <em>Zikora</em>, and immediately read it. It was pretty great.</p><p>One of the reasons I enjoy reading fiction is that it provides a window into the experiences of others. Chimamanda Ngozi Adichie&apos;s writing does exactly this and does it with beautiful prose and compelling stories.</p><h4 id="the-diamond-age-by-neal-stephenson">The Diamond Age by Neal Stephenson</h4><p>This was a reread of the first Neal Stephenson book I read. I wanted to reread this book as I had been recommending it as a relatively short introduction to Neal Stephenson&apos;s writing but I was second guessing how much I enjoyed it.</p><p>I was wrong to second guess that. This story was still great the second time through. This book covers so much and feels prescient despite being read 25 years after it was originally published (February 1995).</p><h4 id="parable-of-the-sower-and-parable-of-the-talents-by-octavia-e.-butler">Parable of the Sower and Parable of the Talents by Octavia E. Butler</h4><p>Octavia Butler builds a new religion in this series and, honestly, that religion is tempting. These are fantastic science fiction reads that explore human connections and what we could be as a species.</p><h3 id="other-notable-reads">Other notable reads</h3><p>These are books that for some reason I didn&apos;t give five stars but I still think they are worth recommending. All links below are to Goodreads.</p><h4 id="piranesi-by-susanna-clark"><a href="https://www.goodreads.com/book/show/52702097-piranesi">Piranesi</a> by Susanna Clark</h4><p>This book was weird and I enjoyed it. You follow a character that lives in a weird, infinite building made of corridors lined with statues.</p><h4 id="why-we&apos;re-polarized-by-ezra-klein"><a href="https://www.goodreads.com/book/show/52655150-why-we-re-polarized">Why We&apos;re Polarized</a> by Ezra Klein</h4><p>This covers the American political system and how we got to our modern form with deeply polarized parties. I thought it was a pretty interesting read.</p><h4 id="the-will-to-change:-men,-masculinity,-and-love-by-bell-hooks"><a href="https://www.goodreads.com/book/show/6656720-the-will-to-change">The Will to Change: Men, Masculinity, and Love</a> by bell hooks</h4><p>I read this at the very beginning of 2020 and think that everyone should read it. I highlighted a ton of passages and plan on going back and reviewing those passages.</p><h4 id="you&apos;re-not-listening:-what-you&apos;re-missing-and-why-it-matters-by-kate-murphy"><a href="https://www.goodreads.com/book/show/52355815-you-re-not-listening">You&apos;re Not Listening: What You&apos;re Missing and Why It Matters</a> by Kate Murphy</h4><p>I read this at the very beginning of 2020 and highlighted a ton of passages. The book is about listening and how we do a bad job at it. It includes suggestions about how to get better.</p><p>Pair this book with <a href="https://www.goodreads.com/book/show/35522033-i-hear-you">I hear you</a>, a book I read last year, and you&apos;ll have the tools to become a better listener.</p><h4 id="diaspora-by-greg-egan"><a href="https://www.goodreads.com/book/show/18886790-diaspora">Diaspora</a> by Greg Egan</h4><p>This was a stupendous science fiction read. It takes you on a wild journey into a far future where sentient beings can exist in software.</p><p>This was very close to receiving five stars but I kept getting bogged down in some of the explanations. I know this is why some folks enjoy hard science fiction but that isn&apos;t why I&apos;m reading these stories. This book delivers an interesting, complex, and very speculative far future. If the blurb sounds interesting to you and you&apos;re willing to put up some with advanced theoretical (real? fake? I don&apos;t know) physics then pick this book up.</p><h2 id="stats">Stats</h2><p>I read 43 books and 12,093 pages in 2020. The data also doesn&apos;t capture three books that I&apos;ve started but have yet to finish.</p><pre><code class="language-org">| Year | # of Pages | # of Books |
|------+------------+------------|
| 2020 |      12093 |         43 |
| 2019 |      15994 |         42 |
| 2018 |      13538 |         36 |
| 2017 |      18317 |         48 |
| 2016 |      22790 |         59 |
| 2015 |      21689 |         51 |
| 2014 |      24340 |         71 |
| 2013 |      19815 |         60 |
| 2012 |      14208 |         44 |
| 2011 |       9179 |         19 |
| 2010 |      14667 |         46 |
</code></pre><p>Last year marks a decade of me tracking my reading and it was the second lowest page count in that decade. For many reasons 2020 was an unforgettable year and one where I spent a lot of time at home.</p><p>I would have thought that would have lead to a large number of pages read but I think much of my time ended up being taken up by non-book reading activities. For better or worse (probably worse), a lot of my time was spent reading articles about the on-going global pandemic, the USA election, and the other non-stop news cycle of 2020. Between that and the increase in newsletters and podcasts I&apos;m consuming, I&apos;m not that surprised my book reading has taken a hit.</p><p>Here is a breakdown of books finished by month.</p><p><img alt="Book and pages count by month" src="/images/reading-2020/books-by-month-2020.png" title="Number of books and pages for each month" /></p><p>This graph tells a slightly different story than the one I presented above. I did not finish many pages in January through March, the pre-pandemic time period in the United States.</p><p>Those months I was extremely dedicated to training for climbing and was starting a new relationship. I&apos;m very happy both of those took up my non-working hours during those months.</p><p>I was still commuting to an office from January till mid-March and would have expected more pages finished on the train. I&apos;ll blame podcasts for that as this year I did start listening to those while commuting, since I can enjoy those while walking to and from the train as well.</p><p>The number of books read in February is high because I read a short story collection where each story was published individually on Amazon.</p><p>Unsurprisingly, electronic books continue to be the dominant format.</p><pre><code class="language-org">|           | 2020 | 2019 | 2018 | 2017 | 2016 | 2015 |
|-----------+------+------+------+------+------+------|
| audiobook |    1 |    0 |    0 |    0 |    0 |    0 |
| ebook     |   41 |   43 |   37 |   37 |   56 |   47 |
| hardcover |    0 |    1 |    1 |    7 |    0 |    1 |
| paperback |    1 |    7 |    5 |    5 |    3 |    3 |
</code></pre><p>Below is the breakdown of fiction vs non-fiction books. Fiction started to regain its dominance after having non-fiction catch up in 2019.</p><pre><code class="language-org">|                           |   2020 |   2019 |   2018 |
|---------------------------+--------+--------+--------|
| fiction                   |     26 |     28 |     29 |
| non-fiction               |     17 |     23 |     14 |
| fiction:non-fiction ratio | 1.53:1 | 1.22:1 | 2.07:1 |

</code></pre><p>Here is the star rating distribution.</p><pre><code class="language-org">|             | 2 stars | 3 stars | 4 stars | 5 stars |
|-------------+---------+---------+---------+---------|
| fiction     |       1 |       8 |      12 |       5 |
| non-fiction |       0 |       4 |       7 |       6 |
</code></pre><h2 id="did-i-hit-my-2020-goals?">Did I hit my 2020 goals?</h2><p>I succeeded in reading a solid number of non-fiction books that earned a high rating. I read fewer non-fiction books than fiction but managed to have more 5 star ratings. I&apos;m going to count this as successfully hitting the non-fiction part of my 2020 goal.</p><p>Did I get better at applying the lessons from books? Not at all and I barely even tried to do so. Definite failure here.</p><h2 id="2021-goals">2021 goals</h2><p>I have quite a few unread books sitting on my virtual and physical bookshelf. This feels like setting a really low-bar but this year I&apos;d like to read some of these unread-but-owned books.</p><p>I&apos;m also planning on reading at least one book on writing and one book on climbing. This goal is almost a subset of the above goal as I have books on both these topics sitting on my shelf.</p><p>It is interesting to have been collecting this data for a decade now. I haven&apos;t done much in the way around looking at multi-year trends but I think it might be interesting to do so.</p><p>If you have a book recommendation, feel free to reach out and contact me.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2020/12/30/speeding-up-magit-with-the-native-comp-branch-of-emacs/index.html</id>
    <link href="https://jakemccrary.com/blog/2020/12/30/speeding-up-magit-with-the-native-comp-branch-of-emacs/index.html"/>
    <title><![CDATA[Speeding up Magit with the native-comp branch of Emacs]]></title>
    <updated>2020-12-30T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>In my last article, <a href="/blog/2020/11/14/speeding-up-magit/">Speeding up Magit</a>, I showed how removing elements from Magit&apos;s status buffer drastically reduces the time it takes to refresh this buffer when working in a large repository (from 4 seconds to around 0.348 seconds). In a <a href="https://www.reddit.com/r/emacs/comments/k3xfa1/speeding_up_magit/ge5o0e0/?utm_source=reddit&amp;utm_medium=web2x&amp;context=3">comment on r/emacs</a>, someone wondered if the <code>native-comp</code> feature of Emacs might improve the Magit status refresh time.</p><p>This reddit thread was the first time I had heard of the <code>native-comp</code> feature. This feature lives on the <code>feature/native-comp</code> branch of the Emacs repository and it compiles Elisp code into native code. Many users have reported noticeable speed improvements using it. The <a href="http://akrl.sdf.org/gccemacs.html">official development log</a> and <a href="https://www.emacswiki.org/emacs/GccEmacs">Emacs Wiki</a> have more information about it.</p><p>I&apos;ll provide more information about getting <code>native-comp</code> working on macOS after I answer the Magit speed question.</p><h2 id="how-did-it-change-refresh-times-of-the-magit-status-buffer?">How did it change refresh times of the Magit status buffer?</h2><p>The quick answer is that running Emacs with <code>native-comp</code> improved the refresh times of the Magit status buffer. Below is a table of the various times.</p><pre><code>| Experiment                              | magit-status refresh time |
|-----------------------------------------+---------------------------|
| full magit-status with native-comp      | 3.152 seconds             |
| full magit-status without native-comp   | 4.003 seconds             |
| magit-status with many sections removed | 0.348 seconds             |
</code></pre><p>Using <code>native-comp</code>, we&apos;ve cut off about 0.85 seconds. That is a pretty solid improvement. Even still, that isn&apos;t fast enough for how often I use Magit so I&apos;ll be sticking with my <a href="/blog/2020/11/14/speeding-up-magit/">Magit setup</a> with many sections removed.</p><p>As a caveat, the timing with <code>native-comp</code> also includes upgrading Emacs from <code>26.3</code> to <code>28.0.50</code> (so I could have <code>native-comp</code>) and Magit from <code>20201111.1436</code> to <code>20201212.929</code>. As a result, the comparison to <code>full magit-status without native-comp</code> isn&apos;t entirely fair as multiple variables have changed. The comparison to time with sections removed is fair as I&apos;m still using that setup (but with native-comp) and the timing is pretty much the same.</p><h2 id="getting-native-comp-on-macos">Getting <code>native-comp</code> on macOS</h2><p>To enable <code>native-comp</code> you need to build Emacs from source. I&apos;ve done this before on Linux systems but this was the first time I&apos;ve done this on macOS.</p><p>When browsing reddit, I found the <a href="https://github.com/jimeh/build-emacs-for-macos">build-emacs-for-macos</a> project which has some helpful instructions for doing this. I followed the instructions from the readme and picked the latest known good commit from <a href="https://github.com/jimeh/build-emacs-for-macos/issues/6">this issue</a> (at the time I did this <code>be907b0ba82c2a65e0468d50653cae8a7cf5f16b</code>). I then updated my <a href="https://github.com/jakemcc/emacs.d/commit/72cf37a497b72b8990956395e2eaa87285ea7c81">init.el</a> based on instructions from in the <code>build-emacs-for-macos</code> project.</p><p>I haven&apos;t had any issues since switching to this very new Emacs. I don&apos;t have numbers to back this up but it does feel faster.</p><h2 id="recommendation">Recommendation</h2><p>I&apos;d recommend giving the <code>native-comp</code> feature of Emacs a shot. It wasn&apos;t terribly challenging to get setup and it is nice to get a glimpse of what the future of Emacs might be. That future is a bit snappier.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2020/11/14/speeding-up-magit/index.html</id>
    <link href="https://jakemccrary.com/blog/2020/11/14/speeding-up-magit/index.html"/>
    <title><![CDATA[Speeding up magit]]></title>
    <updated>2020-11-14T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p><a href="https://github.com/magit/magit">Magit</a> is a great Emacs tool and by far my favorite way of interacting with git repositories. I use Magit nearly every day.</p><p>Unfortunately, refreshing the <code>magit-status</code> buffer is sluggish when you are working in a large repository.</p><p>A few months ago, I became sick of waiting and investigated how to speed up refreshing the status buffer. After doing some research, I learned about the <code>magit-refresh-verbose</code> variable.</p><p>Setting <code>magit-refresh-verbose</code> to true causes Magit to print some very useful output to your <code>*Messages*</code> buffer. This output shows how many seconds each step of <code>magit-status</code> takes.</p><p>Here is the output for the large repo that caused me to look into this.</p><pre><code>Refreshing buffer ‘magit: example-repo’...
  magit-insert-error-header                          1e-06
  magit-insert-diff-filter-header                    2.3e-05
  magit-insert-head-branch-header                    0.026227
  magit-insert-upstream-branch-header                0.014285
  magit-insert-push-branch-header                    0.005662
  magit-insert-tags-header                           1.7119309999999999
  magit-insert-status-headers                        1.767466
  magit-insert-merge-log                             0.005947
  magit-insert-rebase-sequence                       0.000115
  magit-insert-am-sequence                           5.1e-05
  magit-insert-sequencer-sequence                    0.000105
  magit-insert-bisect-output                         5.3e-05
  magit-insert-bisect-rest                           1.1e-05
  magit-insert-bisect-log                            1e-05
  magit-insert-untracked-files                       0.259485
  magit-insert-unstaged-changes                      0.031528
  magit-insert-staged-changes                        0.017763
  magit-insert-stashes                               0.028514
  magit-insert-unpushed-to-pushremote                0.911193
  magit-insert-unpushed-to-upstream-or-recent        0.497709
  magit-insert-unpulled-from-pushremote              7.2e-05
  magit-insert-unpulled-from-upstream                0.446168
Refreshing buffer ‘magit: example-repo’...done (4.003s)
</code></pre><p>The total time is found in the last line and we can see it took four seconds. Four seconds is an incredibly long time to wait before interacting with Magit.</p><p>You can change how much work Magit does by removing functions from the <code>magit-status-sections-hook</code> with <code>remove-hook</code>. I looked at the timings and and tried removing anything I decided was slow and something I didn&apos;t think I&apos;d miss. For me, that list includes <code>magit-insert-tags-header</code>, <code>magit-insert-status-headers</code>, <code>magit-insert-unpushed-to-pushremote</code>, <code>magit-insert-unpushed-to-upstream-or-recent</code>, and <code>magit-insert-unpulled-from-upstream</code>. I also removed <code>magit-insert-unpulled-from-pushremote</code>.</p><p>You remove a function from a hook by adding elisp similar to <code>(remove-hook &apos;magit-status-sections-hook &apos;magit-insert-tags-header)</code> to your Emacs configuration.</p><p>I use <a href="https://github.com/jwiegley/use-package">use-package</a> to configure mine and below is what my <code>magit</code> section looks like.</p><p>Lines 20-25 remove the hooks. I also hard-code <code>magit-git-executable</code> to be the full path of the <code>git</code> executable on line 5 because folks said this made a difference on macOS.</p><pre><code class="language-lisp">(use-package magit
  :ensure t
  :bind (&quot;C-c g&quot; . magit-status)
  :custom
  (magit-git-executable &quot;/usr/local/bin/git&quot;)
  :init
  (use-package with-editor :ensure t)

  ;; Have magit-status go full screen and quit to previous
  ;; configuration.  Taken from
  ;; http://whattheemacsd.com/setup-magit.el-01.html#comment-748135498
  ;; and http://irreal.org/blog/?p=2253
  (defadvice magit-status (around magit-fullscreen activate)
    (window-configuration-to-register :magit-fullscreen)
    ad-do-it
    (delete-other-windows))
  (defadvice magit-quit-window (after magit-restore-screen activate)
    (jump-to-register :magit-fullscreen))
  :config
  (remove-hook &apos;magit-status-sections-hook &apos;magit-insert-tags-header)
  (remove-hook &apos;magit-status-sections-hook &apos;magit-insert-status-headers)
  (remove-hook &apos;magit-status-sections-hook &apos;magit-insert-unpushed-to-pushremote)
  (remove-hook &apos;magit-status-sections-hook &apos;magit-insert-unpulled-from-pushremote)
  (remove-hook &apos;magit-status-sections-hook &apos;magit-insert-unpulled-from-upstream)
  (remove-hook &apos;magit-status-sections-hook &apos;magit-insert-unpushed-to-upstream-or-recent))
</code></pre><p>After this change, my <code>magit-status</code> buffer refreshes in under half a second.</p><pre><code>Refreshing buffer ‘magit: example-repo’...
  magit-insert-merge-log                             0.005771
  magit-insert-rebase-sequence                       0.000118
  magit-insert-am-sequence                           5.3e-05
  magit-insert-sequencer-sequence                    0.0001
  magit-insert-bisect-output                         5.5e-05
  magit-insert-bisect-rest                           1.1e-05
  magit-insert-bisect-log                            1.1e-05
  magit-insert-untracked-files                       0.247723
  magit-insert-unstaged-changes                      0.024989
  magit-insert-staged-changes                        0.018397
  magit-insert-stashes                               0.026055
Refreshing buffer ‘magit: example-repo’...done (0.348s)
</code></pre><p>What did I lose from the <code>magit-status</code> buffer as a result of these changes? Here is screenshot of the original buffer.</p><p><img alt="Buffer before changes" src="/images/magit-speed/magit-before.png" /></p><p>And here is the buffer after.</p><p><img alt="Buffer after changes" src="/images/magit-speed/magit-after.png" /></p><p>The difference is drastic<a href="#fn-1" id="fnref1"><sup>1</sup></a>. And so is the speed difference.</p><p>The increased speed is worth losing the additional information. I interact with <code>git</code> very often and much prefer using Magit to do so. Before these changes, I found myself regressing to using <code>git</code> at the command line and I don&apos;t find that to be nearly as enjoyable. Since I&apos;ve made these changes, I&apos;m back to doing 99% of my <code>git</code> interactions through Magit.</p><p>Don&apos;t settle for slow interactions with your computer. Aggressively shorten your feedback cycles and you&apos;ll change how you interact with the machine.</p><h4 id="versions-used-when-writing-this-article">Versions used when writing this article</h4><p>This post was written with Magit version <code>20201111.1436</code> and Emacs <code>26.3</code> on macOS <code>10.15.7</code>. I&apos;ve been using these changes for a few months but do not remember or have a record of what Magit version I was using at the time I originally made these changes.</p><p><strong>edit on 2020/12/15</strong>: I recently upgraded Emacs to tryout the native-comp work and can report this still works with with Emacs <code>28.0.50</code>, Magit <code>20201212.929</code>, and Git <code>2.29.2</code> running in macOS <code>11.0.1</code>.</p><p><strong>Warning</strong>: This reduces the information Magit shows you. The status buffer will be blank if you have no changes. I find this tradeoff to be worth it.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>The before image is even missing some sections that would have gone missing in the after shot since I didn&apos;t want to put the effort.<a href="#fnref1">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2020/11/11/creating-a-custom-kindle-dictionary/index.html</id>
    <link href="https://jakemccrary.com/blog/2020/11/11/creating-a-custom-kindle-dictionary/index.html"/>
    <title><![CDATA[Creating a custom Kindle dictionary]]></title>
    <updated>2020-11-11T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Back in April 2013, I created and published a <a href="https://gumroad.com/l/dune-dictionary">custom Kindle dictionary</a> for the book <a href="http://www.amazon.com/gp/product/B00B7NPRY8">Dune</a>. As far as I can tell, <a href="https://gumroad.com/l/dune-dictionary">my Dune dictionary</a> was the very first custom Kindle dictionary for a fiction book.</p><p>I created it because I was reading Dune for the first time and there were many unfamiliar words. These words could not be looked up by my Kindle because they were not found in any of on-device dictionaries. These words were in Dune&apos;s glossary but flipping back-and-forth to that on a Kindle was a huge pain.</p><p>I initially worked around this by printing a word list from Wikipedia and carrying it with me. This was better but it was still annoying.</p><p>I was so annoyed that I took a break from reading to figure out how to create a custom Kindle dictionary. At the time, there wasn&apos;t a ton of great information online about how to do this.</p><p>Eventually, I found Amazon&apos;s <a href="https://s3.amazonaws.com/kindlegen/AmazonKindlePublishingGuidelines.pdf">Kindle Publishing Guidelines</a> and, referencing it, managed to figure out something that worked. The link in the previous sentence is to the <strong>current</strong> documentation which is much nicer than the <a href="https://web.archive.org/web/20130408183149/http://s3.amazonaws.com/kindlegen/AmazonKindlePublishingGuidelines.pdf">mid-2013 documentation</a>. The earlier documentation left me with questions and required quite a bit of experimentation.</p><p>Using the mid-2013 documentation, I developed some Clojure code to generate my <a href="https://gumroad.com/l/dune-dictionary">dictionary</a>. Doing this in 2013 was annoying. The documentation was not good.</p><p>I recently read <a href="https://www.gregegan.net/DIASPORA/DIASPORA.html">Greg Egan&apos;s Diaspora</a> and found myself wishing I had a custom dictionary. I took a break from reading and packaged up Diaspora&apos;s glossary into a dictionary. I could have stuck with my 2013 generator but I decided to update it and write this article about creating a Kindle dictionary in late 2020.</p><p>The new documentation is a bit better but it still isn&apos;t great. Here is what you need to do.</p><h2 id="making-a-dictionary">Making a dictionary</h2><p>Below are the steps to building a dictionary.</p><ol><li>Construct your list of words and definitions.</li><li>Convert the list into the format specified by Amazon.</li><li>Create a cover page.</li><li>Create a copyright page.</li><li>Create a usage page (definitely optional).</li><li>Make an <code>.opf</code> file.</li><li>Combine the files together.</li><li>Put it onto your device.</li></ol><h3 id="1.-construct-your-list-of-words-and-definitions">1. Construct your list of words and definitions</h3><p>There really are no set instructions for this. Source your words and definitions and store them in some format that you&apos;ll be able to manipulate in a programming language.</p><p>I&apos;ve sourced words a few different ways. I&apos;ve taken them straight from a book&apos;s glossary, a Wikipedia entry, and extracted them from a programming book&apos;s <a href="/blog/2013/07/09/releasing-the-functional-javascript-companion/">source code</a>.</p><h3 id="2.-convert-the-list-into-the-format-specified-by-amazon">2. Convert the list into the format specified by Amazon</h3><p>Below is the basic scaffolding of the html file Amazon requires along with some inline styles that I think look decent on devices. This has some extra stuff in it and also doesn&apos;t contain everything Amazon specifies. But it works.</p><pre><code class="language-html">&lt;html xmlns:math=&quot;http://exslt.org/math&quot; xmlns:svg=&quot;http://www.w3.org/2000/svg&quot;
      xmlns:tl=&quot;https://kindlegen.s3.amazonaws.com/AmazonKindlePublishingGuidelines.pdf&quot;
      xmlns:saxon=&quot;http://saxon.sf.net/&quot; xmlns:xs=&quot;http://www.w3.org/2001/XMLSchema&quot;
      xmlns:xsi=&quot;http://www.w3.org/2001/XMLSchema-instance&quot;
      xmlns:cx=&quot;https://kindlegen.s3.amazonaws.com/AmazonKindlePublishingGuidelines.pdf&quot;
      xmlns:dc=&quot;http://purl.org/dc/elements/1.1/&quot;
      xmlns:mbp=&quot;https://kindlegen.s3.amazonaws.com/AmazonKindlePublishingGuidelines.pdf&quot;
      xmlns:mmc=&quot;https://kindlegen.s3.amazonaws.com/AmazonKindlePublishingGuidelines.pdf&quot;
      xmlns:idx=&quot;https://kindlegen.s3.amazonaws.com/AmazonKindlePublishingGuidelines.pdf&quot;&gt;
  &lt;head&gt;
    &lt;meta http-equiv=&quot;Content-Type&quot; content=&quot;text/html; charset=utf-8&quot;&gt;
    &lt;style&gt;
      h5 {
          font-size: 1em;
          margin: 0;
      }
      dt {
          font-weight: bold;
      }
      dd {
          margin: 0;
          padding: 0 0 0.5em 0;
          display: block
      }
    &lt;/style&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;mbp:frameset&gt;
      [PUT THE WORDS HERE]
    &lt;/mbp:frameset&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre><p>The <code>[PUT THE WORDS HERE]</code> part gets filled in with the markup for all of your words. The basic structure for an entry looks like the following.</p><pre><code class="language-html">&lt;idx:entry name=&quot;default&quot; scriptable=&quot;yes&quot; spell=&quot;yes&quot;&gt;
  &lt;h5&gt;&lt;dt&gt;&lt;idx:orth&gt;WORD HERE&lt;/idx:orth&gt;&lt;/dt&gt;&lt;/h5&gt;
  &lt;dd&gt;DEFINITION&lt;/dd&gt;
&lt;/idx:entry&gt;
&lt;hr/&gt;
</code></pre><p>Every word has an <code>&lt;idx:entry&gt;</code> block followed by a <code>&lt;hr&gt;</code>. These two elements together comprise a single entry.</p><p>The <code>name</code> attribute on the <code>&lt;idx:entry&gt;</code> element sets the lookup index associated with the entry. Unless you are building a dictionary with multiple indexes, you can pretty much ignore it. Whatever value is provided needs to match the value found in the <code>.opf</code> file we&apos;ll make later.</p><p>The <code>scriptable</code> attribute makes the entry available from the index and can only have the value <code>&quot;yes&quot;</code>. The <code>spell</code> can also only be <code>&quot;yes&quot;</code> and enables wildcard search and spell correction.</p><p>The markup you use inside the <code>idx:entry</code> element is mostly up to you. The only markup you need is the <code>&lt;idx:orth&gt;</code> node. Its content is the word being looked up. The rest of the markup can be whatever you want.</p><p>I wrap the term in a <code>dt</code> and the definition in <code>dd</code> because it just feels like the right thing to do and provides tags to put some CSS styles on. I wrap the <code>dt</code> element in an <code>h5</code> because I couldn&apos;t figure out what CSS styles would actually work on my Kindle voyage to put the term on its own line.</p><p>It isn&apos;t that I don&apos;t know what the styles should be but my Kindle did not respect them. Figuring out stuff like this is part of the experimentation required to produce a dictionary that you&apos;re happy with.</p><p>There is additional supported markup that provides more functionality. This includes providing alternative words that all resolve to the same entry, specifying if an exact match is required, and varying the search word from the displayed word. Most dictionaries don&apos;t need these features so I&apos;m not going to elaborate on them.</p><h3 id="3.-construct-a-cover-page.">3. Construct a cover page.</h3><p>This is just a requirement of a Kindle. Create a html file called <code>cover.html</code> and substitute in the appropriate values.</p><pre><code class="language-html">&lt;html&gt;
  &lt;head&gt;
    &lt;meta content=&quot;text/html&quot; http-equiv=&quot;content-type&quot;&gt;
  &lt;/head&gt;
  &lt;body&gt;
    &lt;h1&gt;Dune Dictionary&lt;/h1&gt;
    &lt;h3&gt;Created by Jake McCrary&lt;/h3&gt;
  &lt;/body&gt;
&lt;/html&gt;
</code></pre><p>Amazon wants you to provide an image as well but you don&apos;t actually have to do this. You probably need to do this if you actually publish the dictionary through Amazon<a href="#fn-1" id="fnref1"><sup>1</sup></a>.</p><h3 id="4.-create-a-copyright-page">4. Create a copyright page</h3><p>This is also a requirement of the Kindle publishing guide. There isn&apos;t any special markup for doing this.</p><p>Just make another html file and fill in some appropriate details.</p><h3 id="5.-create-a-usage-page">5. Create a usage page</h3><p>This isn&apos;t a requirement but I include another page that explains how to use the dictionary. Again, this is just a html document with some content in it.</p><h3 id="6.-make-an-.opf-file.">6. Make an <code>.opf</code> file.</h3><p>This is one of the poorly documented but extremely important parts of making a Kindle dictionary. This is a XML file that ties together all the previous files into an actual dictionary.</p><p>Make an opf file and name it whatever you want; in this example we&apos;ll go with <code>dict.opf</code>.</p><p>Below is the one I&apos;ve used for the Diaspora dictionary. If you&apos;ve created an image for a cover then lines 7 and 15 are the important and line 15 should be uncommented.</p><pre><code class="language-xml">&lt;?xml version=&quot;1.0&quot;?&gt;
&lt;package version=&quot;2.0&quot; xmlns=&quot;http://www.idpf.org/2007/opf&quot; unique-identifier=&quot;BookId&quot;&gt;
  &lt;metadata&gt;
    &lt;dc:title&gt;A dictionary for Diaspora by Greg Egan&lt;/dc:title&gt;
    &lt;dc:creator opf:role=&quot;aut&quot;&gt;Jake McCrary&lt;/dc:creator&gt;
    &lt;dc:language&gt;en-us&lt;/dc:language&gt;
    &lt;meta name=&quot;cover&quot; content=&quot;my-cover-image&quot; /&gt;
    &lt;x-metadata&gt;
      &lt;DictionaryInLanguage&gt;en-us&lt;/DictionaryInLanguage&gt;
      &lt;DictionaryOutLanguage&gt;en-us&lt;/DictionaryOutLanguage&gt;
      &lt;DefaultLookupIndex&gt;default&lt;/DefaultLookupIndex&gt;
    &lt;/x-metadata&gt;
  &lt;/metadata&gt;
  &lt;manifest&gt;
    &lt;!-- &lt;item href=&quot;cover-image.jpg&quot; id=&quot;my-cover-image&quot; media-type=&quot;image/jpg&quot; /&gt; --&gt;
    &lt;item id=&quot;cover&quot;
          href=&quot;cover.html&quot;
          media-type=&quot;application/xhtml+xml&quot; /&gt;
    &lt;item id=&quot;usage&quot;
          href=&quot;usage.html&quot;
          media-type=&quot;application/xhtml+xml&quot; /&gt;
    &lt;item id=&quot;copyright&quot;
          href=&quot;copyright.html&quot;
          media-type=&quot;application/xhtml+xml&quot; /&gt;
    &lt;item id=&quot;content&quot;
          href=&quot;content.html&quot;
          media-type=&quot;application/xhtml+xml&quot; /&gt;
  &lt;/manifest&gt;
  &lt;spine&gt;
    &lt;itemref idref=&quot;cover&quot; /&gt;
    &lt;itemref idref=&quot;usage&quot; /&gt;
    &lt;itemref idref=&quot;copyright&quot;/&gt;
    &lt;itemref idref=&quot;content&quot;/&gt;
  &lt;/spine&gt;
  &lt;guide&gt;
    &lt;reference type=&quot;index&quot; title=&quot;IndexName&quot; href=&quot;content.html&quot;/&gt;
  &lt;/guide&gt;
&lt;/package&gt;
</code></pre><p>An import element in this file is the <code>&lt;DefaultLookupIndex&gt;</code> element. The <code>&lt;DefaultLookupIndex&gt;</code> content needs to contain the same value from the <code>name</code> attribute on your <code>&lt;idx:entry&gt;</code> elements. The <code>&lt;DictionaryInLanguage&gt;</code> and <code>&lt;DictionaryOutLanguage&gt;</code> tell the Kindle the valid languages for your dictionary.</p><p>The other elements in the <code>&lt;metadata&gt;</code> should be pretty self-explanatory.</p><p>The <code>&lt;manifest&gt;</code> gives identifiers for the various files you&apos;ve made in the previous steps</p><p>The commented out <code>&lt;img&gt;</code> shows how you&apos;d add the cover image if you opt to have one. For sideloading dictionaries onto Kindles, it is not required.</p><p>The <code>&lt;spine&gt;</code> section references the <code>&lt;item&gt;</code>s from the <code>&lt;manifest&gt;</code> and specifies the order they appear in your book.</p><p>I honestly don&apos;t remember why the <code>&lt;guide&gt;</code> section is in there or what it is doing in this example. I&apos;m guessing that is what causes there to be an index with the word list in the dictionary but I haven&apos;t tried removing it and the documentation doesn&apos;t talk about it. I only have it there since I had it in earlier dictionaries I made.</p><h3 id="7.-combine-the-files-together">7. Combine the files together</h3><p>The publishing guidelines (as of October 2020) tell you to combine the previously created files together using the command line tool <code>kindlegen</code>. The problem with those instructions is that Amazon doesn&apos;t offer <code>kindlegen</code> as a download anymore. If you want to use it, you can still find it through the Internet Archive (see newly added section at end of article).</p><p>Instead of following the publishing guidelines, we&apos;ll use Kindle Previewer to finish making the dictionary. It is pretty straight forward.</p><ol><li>Download the <a href="https://www.amazon.com/gp/feature.html?ie=UTF8&amp;docId=1000765261">Kindle Previewer</a> application.</li><li>Open it up and click <code>File &gt; Open</code>.</li><li>Find your <code>dict.opf</code> file and open that.</li><li><code>File &gt; Export</code> and export it as a <code>.mobi</code> file.</li></ol><p>The conversion log will complain about a couple things such as missing cover. As long as these are just <code>Warnings</code> it doesn&apos;t matter.</p><p>I&apos;ve found the preview in this app doesn&apos;t match what it looks like on your device so take it with a grain of salt.</p><h3 id="7.-put-it-onto-your-device">7. Put it onto your device</h3><p>Finally, put the dictionary onto your Kindle. You can do this by either using a USB cable or by emailing it to your Kindle&apos;s email address.</p><p>Once it is on your Kindle, open it up and double check that the formatting is correct. Next, open the book you&apos;ve made it for and try looking up a word. If the lookup fails or uses another dictionary, click the dictionary name in the pop-up to change your default dictionary to yours. Now when you try to look up a word, your dictionary is searched first.</p><p>The great thing is that if a word <em>isn&apos;t</em> in your dictionary then the Kindle searches the other dictionaries<a href="#fn-2" id="fnref2"><sup>2</sup></a>. This feature is great as it lets your dictionary be very focused. Hopefully Amazon doesn&apos;t remove this feature.</p><h2 id="end">End</h2><p>It was interesting creating another dictionary so long after I made my first couple. Some of the new features, like the ability to require an exact word match, would have been useful for my <a href="/blog/2013/07/09/releasing-the-functional-javascript-companion/">second dictionary</a>. The actual markup recommendations have changed over the years but luckily my <a href="https://gumroad.com/l/dune-dictionary">Dune dictionary</a> still works. I&apos;m not constantly checking that it works, so if Amazon had changed something and it broke, I probably wouldn&apos;t notice until someone reported it.</p><p>The Kindle documentation is much better now compared to 2013 but it still isn&apos;t great.</p><p>It is also a bummer that <code>kindlegen</code> is gone. It was nice to be able to convert the input files from the command line. I also think this means you can no longer make a dictionary from a Linux machine as I don&apos;t remember seeing Kindle Previewer support.</p><p>If you&apos;re ever in a situation where you think a custom dictionary would be useful, feel free to reach out.</p><p>Go forth and make dictionaries.</p><h2 id="kindlegen-download"><code>kindlegen</code> download</h2><p>I haven&apos;t verified this but a reader sent me an email and provided the following helpful details about downloading kindlegen.</p><pre><code>Thanks a lot for your page about kindle dictionaries.
For your information, it is still possible to use the kindlegen on linux.
 
You can download the old i386 version from the internet archive:
https://archive.org/details/kindlegen_linux_2_6_i386_v2_9
 
In order to make it work on a recent amd64 architecture, you need to
 
enable i386 support:
      enable multi-arch
      (debian) sudo dpkg --add-architecture i386
               sudo apt update
Install essential libraries:
      (debian) sudo apt install libc6:i386 libncurses5:i386 libstdc++6:i386
</code></pre><ol class="footnotes"><li class="footnote" id="fn-1"><p>This is actually a challenge to do due to restrictions on what Amazon allows published.<a href="#fnref1">↩</a></p></li><li class="footnote" id="fn-2"><p>No idea if it searches all of them in some order but I&apos;m very glad it works this way.<a href="#fnref2">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2020/10/03/go-create-silly-small-programs/index.html</id>
    <link href="https://jakemccrary.com/blog/2020/10/03/go-create-silly-small-programs/index.html"/>
    <title><![CDATA[Go create silly, small programs]]></title>
    <updated>2020-10-03T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Over the summer, I developed a couple of small, sort of silly programs. One, <a href="https://jakemccrary.com/experiments/photo-fit/">Photo Fit</a>, is a little tool that runs in a web browser and resizes photos to fit as your phone&apos;s background. The other, <a href="https://default-equipment.herokuapp.com/">Default Equipment</a>, runs on Heroku and automates changing the &quot;bike&quot; of my Strava-tracked e-bike rides to be my <a href="https://onewheel.com/">onewheel</a>.</p><p>These weren&apos;t created to solve large problems in the world. There is no plan to make any money with them. As of October 2020, Default Equipment doesn&apos;t even work for other people (though it could, send me a message if you&apos;d like to use it and I&apos;ll get around to it).</p><p>Each was created to fix a minor annoyance in my life and, because these tools can live on the Internet, they can fix the same minor annoyance in other lives.</p><p>With an increasing amount of software in the world, being able to write software is nearly sorcery<a href="#fn-1" id="fnref1"><sup>1</sup></a>. As a developer, you can identify a problem in the world and then change the world to remove that problem. And, depending on the problem, you can remove it for everyone else.</p><p>Software developers aren&apos;t alone in being able to identify problems and remove them through creation. Carpenters can build shelves for their books. Cooks can prepare food to remove hunger. You can come up with nearly an infinite number of other examples.</p><p>The difference is that a solo developer can solve problems for an unknown number of other folks. This is enabled by the Internet enabled ease of distribution. This is very powerful.</p><p>Developers can expose their solution to others through a web application. Desktop or mobile applications can be distributed through various app stores or made available as a download. Source code can be made available for others to run. Being able to distribute easily and cheaply is a game changer.</p><p>A developer&apos;s change to the world might be a minor improvement. Photo Fit might never be used by anyone besides me. But it is still out there, making the world slightly better. It is available for someone to stumble upon when they are also annoyed by the same problem.</p><p>It felt good to write these tiny, useful programs. If you scope them small enough, there is a definitive ending point<a href="#fn-2" id="fnref2"><sup>2</sup></a>. This lets you feel that finishing-a-project satisfaction quickly. The small size also allows you experiment with new techniques and tools without committing to a large and ongoing commitment.</p><p>I wrote both Photo Fit and Default Equipment in TypeScript. Before the beginning of summer, I didn&apos;t know TypeScript and had little exposure to Node.js. Now I have some experience with both and gained that while making small improvements to my life and potentially the lives of others.</p><p>If you haven&apos;t developed software to solve a small problem recently, I&apos;d recommend doing it. Don&apos;t hesitate to remove a problem that feels silly. Removing those problems can still make your life slightly better and gives you an opportunity to learn. It feels good to remove an annoyance from your life. If you can, make that software available to others so their lives are improved as well. Take advantage of the power of easy distribution to improve the world and not just your tiny slice of it.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>This is taken to an extreme in the fantasy series <a href="https://www.goodreads.com/series/131379-magic-2-0">Magic 2.0</a>.<a href="#fnref1">↩</a></p></li><li class="footnote" id="fn-2"><p>Excluding any ongoing maintenance. But if you&apos;re making something small enough you can approach near zero ongoing maintenance. One of my longest running solve-my-own-problems application, Book Robot, has been operating for nearly 7 years with minimal effort.<a href="#fnref2">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2020/08/31/utilities-i-like-selecta/index.html</id>
    <link href="https://jakemccrary.com/blog/2020/08/31/utilities-i-like-selecta/index.html"/>
    <title><![CDATA[Utilities I like: selecta]]></title>
    <updated>2020-08-31T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p><a href="https://github.com/garybernhardt/selecta">Selecta</a> is a command-line utility that gives you the power to fuzzy select items from a list of text. What does that mean? It means you pipe <code>selecta</code> a list of text on stdin, it helps you make a choice from items in that list, and then <code>selecta</code> prints that choice to stdout.</p><p>Here is an example of me using it to help me narrow in on what file I&apos;d like to pass to <code>wc</code>.</p><video autoplay loop muted playsinline>
  <source src="/images/selecta-search.webm" type="video/webm">
  <source src="/images/selecta-search.mp4" type="video/mp4">
</video><p>In this example, I search for markdown files using <code>ripgrep</code> (<code>rg</code>), type part of a filename, hit enter to select the match, and then see the <code>wc</code> stats of that file. This isn&apos;t the greatest example of using <code>selecta</code> but it adequately shows what it does.</p><p>Some number of years ago, I wrote a script called <code>connect-db</code>. This script used <code>selecta</code>, along with <code>grep</code>, <code>sed</code>, and <code>cut</code>, to provide a very pleasant command-line experience for connecting to known databases. My coworkers and I used this script frequently.</p><p>By combining <code>selecta</code> with other stdin/stdout friendly command-line tools you can build really enjoyable, time-saving tools. <a href="https://github.com/garybernhardt/selecta">Selecta</a> is a useful utility to add to your toolkit.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2020/07/03/introducing-photo-fit/index.html</id>
    <link href="https://jakemccrary.com/blog/2020/07/03/introducing-photo-fit/index.html"/>
    <title><![CDATA[Introducing Photo Fit]]></title>
    <updated>2020-07-03T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Earlier this year, I wanted to use a landscape photo as my background on my phone. It wasn&apos;t the photo below but we can use it as an example.</p><p><img alt="Landscape image of my keyboard" src="/images/photo-fit/keyboard-new-keycaps.jpg" title="Landscape image of my keyboard" /></p><p>When I made it my background, my phone<a href="#fn-1" id="fnref1"><sup>1</sup></a> zoomed in to make it fit the portrait orientation of the phone.</p><p><img alt="Screenshot of phone with zoomed in keyboard photo" src="/images/photo-fit/phone-background-before.jpg" title="Screenshot of phone with zoomed in keyboard photo" /></p><p>This is not great. I don&apos;t want a zoomed in version that fits my vertical phone. I want to see the whole photo with black bars at the top and bottom</p><p>I tried to find a way to add these bars using my phone. I couldn&apos;t find an easy way.</p><p>At this point, a reasonable solution would have been transferring the photo to a computer, editing it, and transferring it back to my phone. I didn&apos;t do that. Instead, I wrote a little TypeScript<a href="#fn-2" id="fnref2"><sup>2</sup></a> web app that adds the bars for you. You open the website on your phone, select an image, and then download a properly sized image.</p><p><img alt="Screenshot of phone with properly fitting image" src="/images/photo-fit/phone-background-after.jpg" title="Screenshot of phone with properly fitting image" /></p><p>The tool uses the canvas API and does all of the work in the browser itself. It was a fun, bite-sized project and it gave me an excuse to write some TypeScript and do some web programming. This was the first time I&apos;ve written TypeScript since learning it and I haven&apos;t done any web programming in a while.</p><p>Making <a href="/experiments/photo-fit/">Photo Fit</a> was not a fast approach to changing my phone&apos;s background. But, now the tool exists and anyone, including future me, can quickly resize their photo from the comfort of their own phone.</p><p><a href="/experiments/photo-fit/">Photo Fit</a> is live and available for others to use. I&apos;ve only tested it on my own phone and desktop browsers. It might not work! If you do try it and something weird happens, plese let me know.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>A Samsung S8 running Android 9<a href="#fnref1">↩</a></p></li><li class="footnote" id="fn-2"><p>I recently learned some TypeScript through <a href="https://www.executeprogram.com">Execute Program</a>. Execute program is a really neat application of spaced repetition for learning programming concepts.<a href="#fnref2">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2020/06/28/using-bazel-to-help-fix-flaky-tests/index.html</id>
    <link href="https://jakemccrary.com/blog/2020/06/28/using-bazel-to-help-fix-flaky-tests/index.html"/>
    <title><![CDATA[Using Bazel to help fix flaky tests]]></title>
    <updated>2020-06-28T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Flaky tests are terrible. These are tests that pass or fail without anything changing in the code. They often pass the majority of the time and fail rarely. This makes them hard to detect and cause developers to often just run the tests again.</p><p>Flaky tests erode your team&apos;s confidence in your system. They cause folks to get in the habit of not trusting the output of tests. This discourages people from writing tests as they stop seeing them as something that improves quality and instead view them as a drag on productivity.</p><p>Flaky tests are often hard to fix. If they were easy to fix, they wouldn&apos;t have been flaky in the first place. One difficulty in fixing them is that the failures are often hard to reproduce.</p><p>Often, the first step in fixing a flaky test is to write a script to run the tests multiple times in a row. If you are using <a href="https://bazel.build/">Bazel</a> as your build tool you don&apos;t need to write this.</p><p>Here is an example <code>bazel</code><a href="#fn-1" id="fnref1"><sup>1</sup></a> command for helping you recreate flaky test failures.</p><p><code>bazel test --test_strategy=exclusive --test_output=errors --runs_per_test=50 -t- //...</code></p><p>The above command is running all the test targets in a workspace and each flag is important.</p><ul><li><code>--runs_per_test=50</code> is telling Bazel to run each test 50 times.</li><li><code>--test_output=errors</code> is telling Bazel to only print errors to your console.</li><li><code>-t-</code> is a shortcut for <code>--nocache_test_results</code> (or <code>--cache_test_results=no</code>). This flag tells Bazel to <strong>not</strong> cache the test results.</li><li><code>--test_strategy=exclusive</code> will cause tests to be run serially. Without this, Bazel could run your test targets concurrently and if your tests aren&apos;t designed for this you may run into other failures.</li></ul><p>Flaky tests are terrible and you should try not to have them. Try your best to have reliable tests.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>I&apos;ve written this while using Bazel 3.2.0. If you are reading this far in the future the flags may have changed.<a href="#fnref1">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2020/05/04/how-to-be-alerted-when-a-long-running-process-finishes/index.html</id>
    <link href="https://jakemccrary.com/blog/2020/05/04/how-to-be-alerted-when-a-long-running-process-finishes/index.html"/>
    <title><![CDATA[How to be automatically notified when long running processes finish]]></title>
    <updated>2020-05-04T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Let me set the stage. I kick off the compilation of a large Scala codebase. This will take minutes to finish, so I switch to Slack and catch up on what coworkers have posted. Someone posted an interesting link and I follow it to an article. Fifteen minutes later, I notice the compilation finished twelve minutes ago. I silently grumble at myself, disappointed that I didn&apos;t start the next step twelve minutes ago.</p><p>Has some variation of the above happened to you?</p><p>It doesn&apos;t happen to me anymore because now my computer tells me when any long running process finishes. This might sound annoying but it is great. I no longer feel guilty<a href="#fn-1" id="fnref1"><sup>1</sup></a> for dropping into Slack and can immediately get back to the task at hand as soon the process finishes.</p><p>I&apos;ve done this by enhancing on my setup for showing the <a href="/blog/2020/04/21/using-bash-preexec-for-monitoring-the-runtime-of-your-last-command/">runtime of the previous command in my prompt</a>. You don&apos;t have to read that article for the rest of this one to make sense, but you should because it shows you how to add a very useful feature to your prompt.</p><p>Below is the code that causes my computer to tell me when it finishes running commands that takes longer than 30 seconds. It is found in my <code>~/.bashrc</code>. An explanation follows the code snippet.</p><pre><code class="language-bash"># Using https://github.com/rcaloras/bash-preexec
preexec() {
  _last_command=$1
  if [ &quot;UNSET&quot; == &quot;${_timer}&quot; ]; then
    _timer=$SECONDS
  else 
    _timer=${_timer:-$SECONDS}
  fi 
}

_maybe_speak() {
    local elapsed_seconds=$1
    if (( elapsed_seconds &gt; 30 )); then
        local c
        c=$(echo &quot;${_last_command}&quot; | cut -d&apos; &apos; -f1)
        ( say &quot;finished ${c}&quot; &amp; )
    fi
}

precmd() {
  if [ &quot;UNSET&quot; == &quot;${_timer}&quot; ]; then
     timer_show=&quot;0s&quot;
  else 
    elapsed_seconds=$((SECONDS - _timer))
    _maybe_speak ${elapsed_seconds}
    timer_show=&quot;$(format-duration seconds $elapsed_seconds)&quot;
  fi
  _timer=&quot;UNSET&quot;
}

# put at the bottom of my .bashrc
[[ -f &quot;$HOME/.bash-preexec.sh&quot; ]] &amp;&amp; source &quot;$HOME/.bash-preexec.sh&quot;
</code></pre><p><a href="https://github.com/rcaloras/bash-preexec">Bash-Preexec</a> triggers the <code>preexec</code>, immediately before a command is execute, and <code>precmd</code> functions, immediately before the shell prompt reappears. Those two functions are enough to figure out how much time has elapsed while a command ran. You setup Bash-Preexec by downloading <a href="https://github.com/rcaloras/bash-preexec/blob/master/bash-preexec.sh">bash-preexec.sh</a> and sourcing it in your <code>~/.bashrc</code>.</p><p><code>preexec</code> is passed the command being ran and it captures it in <code>_last_command</code>. It also captures the current number of seconds the shell has been running as <code>_timer</code>.</p><p><code>precmd</code> uses the value in <code>_timer</code> to calculate the elapsed time in seconds and then calls the function <code>_maybe_speak</code> with this as an argument. It also does the work required for showing the elapsed time in my prompt.</p><p>If the elapsed time is greater than 30 seconds then <code>_maybe_speak</code> uses <code>cut</code> to discard the arguments of captured command, leaving me with the command itself. It then uses <code>say</code> to produce an audible alert of what command just finished. I discard the arguments because otherwise the <code>say</code> command can go on for a long time.</p><p><code>say</code> is a tool that ships with macOS. I haven&apos;t gotten around to it yet but I&apos;ll need to use something else on my Linux machines.</p><p>You may have noticed that I run <code>say</code> in the background and in a subshell. Running it in the background lets me continue interacting with my shell while <code>say</code> finishes executing and running it in a subshell prevents text from appearing in my shell when the background job finishes.</p><p>With this setup, I can kick off a slow compile or test run and not feel so bad about dropping into Slack or reading Reddit. It is wonderful and I&apos;d recommend it (though, I&apos;d more strongly recommend not having commands that take a while to run).</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>I still feel a little guilty as doing so will break any momentum/flow I had going on, but that flow was already broken by the slowness of the command.<a href="#fnref1">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2020/04/26/hanging-a-hangboard-using-a-doorway-pull-up-bar/index.html</id>
    <link href="https://jakemccrary.com/blog/2020/04/26/hanging-a-hangboard-using-a-doorway-pull-up-bar/index.html"/>
    <title><![CDATA[How to hang a hangboard using a doorway pull-up bar]]></title>
    <updated>2020-04-26T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>If you&apos;ve browsed the <a href="/adventure">adventure</a> section of my website you know I&apos;m a climber. Currently, the climbing gyms in Chicago are closed due to COVID-19 concerns. This has put a damper on my training but I own a hangboard and have been able to keep training my fingers at home.</p><p>A hangboard allows you to apply stress to your fingers in a measured and controlled fashion. It is a vital tool for a climber who is serious about getting stronger. It is also a great rehab tool for coming back from injuries.</p><p>Below is my hangboard.</p><p><img alt="Hangboard mounted using hooks and a pull-up bar" src="/images/hangboard.png" /></p><p>As you can see from the photo, I&apos;ve hung mine using a doorway pull-up bar and a bunch of hooks. This lets me easily take it down and causes no permanent damage to anything in my apartment. The towels are there to make sure the door frame isn&apos;t crushed by any of the hard pieces.</p><p>Originally, I followed <a href="https://www.youtube.com/watch?v=Cu-MrncHpJo">this video</a> to mount it using some pipe and shoving the pipe into the pull-up bar. This setup made me uncomfortable as the forces on the pull-up bar were far away from the intended location. This resulted in a lot of flexing and I was concerned about how the pull-up bar was acting on the frame.</p><p>I searched online for other ideas and saw a setup that used hooks. This was appealing to me as it moves your weight under the bar. A quick trip to Home Depot and a bit of easy construction and now I can keep up my finger strength when stuck at home. Here are the steps to build one.</p><ol><li>Buy a 2 inch x 10 inch wood board (or some other 2 inch x N inch board that is big enough for whatever you want to attach to it).</li><li>Cut the board so it spans the width of your doorway plus a few extra inches. Home Depot can do this for you.</li><li>Mount your hangboard to the board.</li><li>Take <a href="https://www.homedepot.com/p/Everbilt-Screw-in-Steel-Bicycle-Hook-with-Vinyl-Coating-25-lbs-21407/206585761">hooks</a>, typically used for hanging bicycles up in a garage, and screw them into the top of your 2-in x 10-in.</li><li>Hang the hooks over the pull-up bar. Adjust the hooks so each is pulling on the bar.</li><li>Find some padding, I used towels, and put the padding between the door trim and other hard surfaces.</li><li>Hang on your hangboard and get stronger.</li></ol><p>The board and hook method was much easier to construct than the other pull-up bar method and feels much more solid. The pull-up bar isn&apos;t rated for too much weight, so I&apos;m not going to do any super heavy, two-handed hangs but it is plenty solid for other hangboard exercises.</p><p>If you&apos;re a climber and don&apos;t want to permanently mount a handboard, I&apos;d highly recommend this. If you don&apos;t own a hangboard, I pick up something from <a href="https://www.tensionclimbing.com/hangboards/">Tension Climbing</a>. Their wooden boards are easy on the finger tips and have all the edge sizes you&apos;ll need.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2020/04/21/using-bash-preexec-for-monitoring-the-runtime-of-your-last-command/index.html</id>
    <link href="https://jakemccrary.com/blog/2020/04/21/using-bash-preexec-for-monitoring-the-runtime-of-your-last-command/index.html"/>
    <title><![CDATA[Using Bash-Preexec for monitoring the runtime of your last command]]></title>
    <updated>2020-04-21T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>My article on <a href="/blog/2015/05/03/put-the-last-commands-run-time-in-your-bash-prompt/">putting the runtime of your last command into your bash prompt</a> is one of my most surfaced-by-google articles. Why is this a great to your prompt?  To quote my previous article:</p><blockquote><p>I’m fairly certain the following scenario has happened to every terminal user. You run a command and, while it is running, realize you should have prefixed it with <code>time</code>. You momentarily struggle with the thought of killing the command and rerunning it with <code>time</code>. You decide not to and the command finishes without you knowing how long it took. You debate running it again.</p></blockquote><blockquote><p>For the last year I’ve lived in a world without this problem. Upon completion, a command’s approximate run time is displayed in my prompt. It is awesome.</p></blockquote><p>I&apos;ve been living without the above problem since sometime in 2014 and not having that problem is still awesome.</p><p>I have made some changes since 2014.</p><p>One change was switching to using <a href="https://github.com/rcaloras/bash-preexec">Bash-Preexec</a> instead of directly using <code>trap</code> and <code>$PROMPT_COMMAND</code> for calling functions to start and stop tracking runtime. Bash-Preexec lets you trigger a function (or multiple) right after a command has been read and right before each prompt.</p><p>The usage is pretty straight forward. In the most basic case, you source <code>bash-preexec.sh</code> and then provide functions named <code>preexec</code>, which is invoked right before a command is executed, and/or <code>precmd</code>, which is invoked just before each prompt. <code>bash-preexec.sh</code> can be downloaded from <a href="https://github.com/rcaloras/bash-preexec/">its repo</a>.  The changes required to move to Bash-Preexec pretty <a href="https://github.com/jakemcc/dotfiles/commit/46fc3dc9d4d7d0d73152c77b7383645af42b3d5d">pretty minimal</a>.</p><p>The other change was introducing the script, <a href="https://github.com/jakemcc/dotfiles/blob/9c8c0315f35b55df6cef7e21261e3dcbbfac86e1/home/.bin/format-duration#L3-L4">format-duration</a> by <a href="https://twitter.com/gfredericks_">Gary Fredericks</a>, to humanely format the time. This script converts seconds into a more readable string (example: 310 to <code>5m10s</code>)</p><p>Here is a screenshot of everything in action (with a reduced prompt, my normal one includes git and other info).</p><img src="/images/runtime-humane-example.png" alt="Command line prompt showing runtimes of previous commands" width=320 height=150><p>Below is a simplified snippet from my <code>.bashrc</code> that provides runtimes using both of these additions.</p><pre><code class="language-bash">preexec() {
  if [ &quot;UNSET&quot; == &quot;${timer}&quot; ]; then
    timer=$SECONDS
  else 
    timer=${timer:-$SECONDS}
  fi 
}

precmd() {
  if [ &quot;UNSET&quot; == &quot;${timer}&quot; ]; then
     timer_show=&quot;0s&quot;
  else 
    the_seconds=$((SECONDS - timer))
    # use format-duration to make time more human readable
    timer_show=&quot;$(format-duration seconds $the_seconds)&quot; 
  fi
  timer=&quot;UNSET&quot;
}

# Add $last_show to the prompt.
PS1=&apos;[last: ${timer_show}s][\w]$ &apos;

# a bunch more lines until the end of my .bashrc
# where I include .bash-preexec.sh
[[ -f &quot;$HOME/.bash-preexec.sh&quot; ]] &amp;&amp; source &quot;$HOME/.bash-preexec.sh&quot;
</code></pre><p>No more wondering about the runtime of commands is great. Introducing <code>format-duration</code> made reading the time easier while Bash-Preexec made reading the implementation easier. I highly recommend setting up something similar for your shell.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2020/03/16/a-retrospective-format/index.html</id>
    <link href="https://jakemccrary.com/blog/2020/03/16/a-retrospective-format/index.html"/>
    <title><![CDATA[A retrospective format for remote or co-located teams]]></title>
    <updated>2020-03-16T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><blockquote><p>See all of my remote/working-from-home <a href="/blog/categories/remote/">articles here</a>.</p></blockquote><p><a href="https://retrospectivewiki.org/index.php?title=Agile_Retrospective_Resource_Wiki">Retrospectives</a> are a useful type of meeting to have periodically with your team. There are many different <a href="https://retrospectivewiki.org/index.php?title=Retrospective_Plans">formats</a> of retrospectives.</p><p>One of them can be summarized in the following steps:</p><ol><li>Gather the team together</li><li>Set the stage</li><li>Brainstorm answers to the questions <code>What went well?</code> and <code>What needs improvement?</code></li><li>Discuss the answers</li></ol><p>Let&apos;s talk about each step and see how each works with an co-located or remote team.</p><h3 id="step-1:-gather-the-team">Step 1: Gather the team</h3><p>This step is self explanatory. If you are an in-person team, then this is gathering everyone together in a room for some allotted about of time. If you are a remote team, or have remote folks on your team, then this is gathering everyone together in a video conference.</p><p>Preferably everyone in the retro is communicating in same way. This means if <strong>anyone</strong> is remote, it is preferable that <strong>everyone</strong> join the video conference from their own computer instead of using a single screen and video from a shared conference room. My earlier article about <a href="/blog/2016/06/28/seven-tips-for-successful-remote-meetings/">tips for remote meetings</a> goes into more details on this topic.</p><p>Everyone using the same communication method puts everyone on the same page and dramatically improves the experience for the remote folks. With a mixed group, we&apos;ll want to use some remote collaboration tools anyway, so it is useful for everyone to have their own computer with them. They might as well use it for video communication as well.</p><h3 id="step-2:-set-the-stage">Step 2: Set the stage</h3><p>This part doesn&apos;t differ between an entirely in-person meeting, mixed, or entirely remote meeting.</p><p>Take the time to set the stage for the meeting. Remind everyone that we&apos;re here to improve and to listen with an open mind. Remind everyone to try to not make things personal and not take things personally. This is a good time to read the <a href="https://retrospectivewiki.org/index.php?title=The_Prime_Directive">Prime Directive</a>.</p><p>This is also a good time to set the boundaries of the discussion. What is the retrospective covering? Is it covering the last few weeks? The last quarter? The new working from home experience? Pick a topic so everyone in the meeting focuses on the same things.</p><h3 id="step-3:-answer-the-questions">Step 3: Answer the questions</h3><p>In this step, we will answer the questions <code>What went well?</code> and <code>What needs improvement?</code> and use those answers for discussion in the remainder of the meeting. Timebox this step to 5 to 10 minutes.</p><p>In an in-person setting, this is often done through the use of <a href="https://www.post-it.com/3M/en_US/post-it/products/~/Post-it-Products/Notes/Super-Sticky/?N=4327+5927575+7568222+3294529207+3294857497&amp;rt=r3">Post-it</a> notes. Give each attendee a marker and a stack of notes and have each person write down as many answers as they can come up with, one per post-it note, to the two questions. Dedicate a section of a whiteboard or wall for each question and have people bring the generated answers to the respective sections. Try to group the answers by topics.</p><p>With a remote meeting, you don&apos;t have the physical whiteboard and cards. That is perfectly fine! Once you figure out your remote collaboration tools, this part of the retrospective isn&apos;t difficult.</p><p>I&apos;ve mostly done remote retrospectives using <a href="https://trello.com/">Trello</a>. Trello works great for this as it is multi-user and does a great job of presenting lists to a group. Here is how previous teams I&apos;ve worked with setup Trello for remote retrospectives.</p><p>First, make a Trello board and make sure everyone has an invite to view and edit the board. Second, add the following columns to the board.</p><p><img alt="First three columns before any cards" src="/images/remote-retros/retros-first-three-columns.png" title="First three columns before any cards" /></p><p>The first column is for Step 2 of the process and is there to remind everyone why we&apos;re all spending time in this meeting.</p><p>Columns two and three are used in this step. Have attendees add cards to these columns as they come up with answers If anyone notices duplicates during this time frame, move them near each other by dragging them up or down in the column. If you notice someone else has already put a card that you&apos;d put up there, don&apos;t bother putting it down again (this differs from the in-person meeting).</p><p><img alt="First three columns with cards before voting" src="/images/remote-retros/first-three-before-voting.png" /></p><h4 id="[remote-only]-step-3.5:-vote-on-cards">[remote only] Step 3.5: Vote on cards</h4><p>This step sneaks into the remote retrospective and is missing from the in-person retro. In the in-person retro, duplication of post-it notes serves as this voting stage.</p><p>Once all the answers have been generated, or time is up, it is time to vote on what will be discussed in the next step. Only have people vote on the <code>What needs improvement?</code> answers.</p><p>There are at least two ways of doing this in Trello but my favorite is having attendees hover their mouse cursor over the card and then hit <code>space bar</code><a href="#fn-1" id="fnref1"><sup>1</sup></a>. This sticks their avatar on the card (in Trello speak, I believe this is called joining a card). You can either restrict folks to a certain number of votes, say 3, or let them go wild and vote as many times as they want. I haven&apos;t found the outcomes to be much different and find infinite votes more fun.</p><p><img alt="First three columns with votes" src="/images/remote-retros/first-three-columns-with-votes.png" /></p><p>Once voting is finished (again, timer or when it seems to have reached an end), have one person sort the cards by number of votes with the highest votes at the top of the list.</p><p><img alt="First three columns with cards sorted by votes" src="/images/remote-retros/first-three-columns-votes-sorted.png" /></p><h3 id="step-4:-discuss-the-answers">Step 4: Discuss the answers</h3><p>With in-person or remote retros, go over the answers to <code>What went well?</code> first. This starts the discussion with positive feelings. This part usually goes pretty fast as we&apos;re just celebrating wins and not having a long discussions about them.</p><p>Next, start discussing the answers to <code>What needs improvement?</code></p><p>For each topic being discussed, set a five minute timer. At the end of the five minutes, do a quick poll of the attendees on if the topic should be continued or not. If it should be continued, start a three minute timer and continue discussion. At the end of those three minutes, repeat the vote for continuing or not.</p><p>Throughout the discussion, try to be mindful of people dominating conversation and give everyone a chance to voice their thoughts. Try to figure out some next steps to take to actually start making improvements on what needs to be improved.</p><p>The above is generic advice for remote or in-person retros. When you&apos;re running a remote retro using Trello, it can be useful to do the following as well.</p><p>You should add two more columns, <code>Next Steps</code> and <code>Discussed</code>, to the right of the <code>What needs improvement?</code> column.</p><p><img alt="Additional columns added to board" src="/images/remote-retros/last-three-columns-before-discussion.png" /></p><p>Since your cards are sorted in the <code>What needs improvement?</code> column, you&apos;ll always be talking about the top card. As discussion finishes, move it from the top of the <code>What needs improvement?</code> column into the <code>Discussed</code> column. As <code>Next Steps</code> are discovered, add cards to the <code>Next Steps</code> column and assign the people responsible for following up to the card. Below is an example of those three columns after discussing two cards.</p><p><img alt="Final state of last three columns" src="/images/remote-retros/final-three-columns-post-discussion.png" /></p><p>When voting on continuing discussion or not, it can be useful to have a hand signal for taking the vote and for continuing or ending the discussion. We&apos;d do a quick thumbs up or thumbs down and if half the team wants to keep going then we&apos;d seamlessly start the next timer.</p><h2 id="conclusion">Conclusion</h2><p>Retrospectives can be a very handy tool for a team&apos;s continuous improvement. If time isn&apos;t provided for reflecting, then reflecting does not happen and this makes improving harder.</p><p>Remote retrospectives provide a challenge since most of us only have experience using physical sticky notes or whiteboards for collecting answers. We don&apos;t need to recreate the same form factor for remote retrospectives. Using remote collaboration tools, such as Trello, that don&apos;t recreate the sticky-note-on-wall experience can lead to initial confusion but, once familiar with them, the experience is pleasant and allows for greater participation.</p><p>How is participation increased? Well, in an in-person retrospective you often are unable to read what everyone else has stuck up on the wall because of physical distance. With a remote retro, you&apos;re able to read every answer added to the lists.</p><p>Don&apos;t be afraid of running a remote retrospective. They can be incredibly useful.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>The alternative method I&apos;m aware of is to use a Trello Power-up to enable voting on cards. But why bother doing this when you can just stick faces on cards.<a href="#fnref1">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2020/03/10/more-working-from-home-tips/index.html</id>
    <link href="https://jakemccrary.com/blog/2020/03/10/more-working-from-home-tips/index.html"/>
    <title><![CDATA[More working from home tips]]></title>
    <updated>2020-03-10T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><blockquote><p>See all of my remote/working-from-home <a href="/blog/categories/remote/">articles here</a>.</p></blockquote><p>With the the new coronavirus spreading through the world, more people are either choosing or are being forced to work from home. From 2013 to 2018, the companies I worked for were entirely remote. For the rest of my professional career, 2007 to 2013 and 2018 to now (March 2020), I&apos;ve also frequently worked from home.</p><p>I&apos;ve managed to be very effective at it and I think others can be as well.</p><p>After years of working in an office, transitioning to working from home isn&apos;t easy. I had difficulty with the transition and people I&apos;ve mentored have as well. I think most people will be able to be effective at home, assuming their workplace is supportive, if they try to get better at it. With a supportive company or team, once you get used to working from home you probably find yourself getting more done.</p><p>The key word in the sentence &quot;I&apos;m working from home&quot; is <strong>working</strong>. You are going to be <strong>working</strong> where you spend a lot of your non-work time. This can be a difficult mental transition. Physically switching to an office environment can help switch your brain into work mode and now you no longer have that. Don&apos;t worry, it might feel rough in the beginning but you will get better at it.</p><p>I&apos;ve written more articles about working <a href="/blog/categories/remote/">remotely</a> and I&apos;d recommend you read those as well. This article is primarily targeted at the person not making a permanent change in their work from home status. My <a href="/blog/2017/10/31/a-guide-to-distributed-work/">Guide to Distributed Work</a> is a bit more targeted at someone that is permanently choosing to work at home or in a position of power to influence work from home policies at a company. I&apos;d recommend that you read it as well as many of the subjects it talks about are generally applicable. It steps through some of the pros and cons of remote work and links to other writing on the topic.</p><p>Below is a hodgepodge of tips for working from home.</p><h3 id="setup-a-home-workspace">Setup a home workspace</h3><p>In my years of remote work, I&apos;ve always managed to have a <a href="/blog/2015/03/31/my-home-work-space/">dedicated space</a> for work. In some apartments, this was a corner of a room where I put a desk and faced a wall. In other apartments, I&apos;ve been privileged enough to have a dedicated room for an office.</p><p>If you aren&apos;t planning on working from home permanently, or very frequently, then you probably don&apos;t want to spend a significant amount of money setting up a work area. This probably means you don&apos;t want to find a home with a dedicated office and you may not want or be able to dedicate a portion of a room to a desk<a href="#fn-1" id="fnref1"><sup>1</sup></a>.</p><p>Whatever your living arrangement is, I&apos;d encourage you to figure a way to have a regular spot to work at while you are working. Having a regular spot to work from will help your brain turn on and off from work mode.</p><p>Setting up a home workspace can be as low cost as using a <a href="https://www.amazon.com/Folding-Holder-Height-Adjustments-Original/dp/B01LZPMFW9/">tv tray</a> or <a href="https://www.amazon.com/Lifetime-80251-Adjustable-Folding-Granite/dp/B0074HYWFG/">folding table</a><a href="#fn-2" id="fnref2"><sup>2</sup></a> with a chair. Your setup could be as elaborate as getting a height adjustable desk with large monitors. It could be something else entirely.</p><p>Find something that works for you and stick with it.</p><p>Beyond a dedicated space to work, make sure you have a reliable internet connection. If you can, use Ethernet as it is generally better than WiFi. I&apos;ve never had a situation where I could use Ethernet and have found that having a good router is enough to make my WiFi reliable.</p><h3 id="discuss-boundaries-and-expectations-with-your-cohabitants">Discuss boundaries and expectations with your cohabitants</h3><p>If you live with others that will be at home while you need to work, you should have a discussion with them about boundaries. You are at home to do work and that expectation needs to be set. You may be able to do some household chores during breaks or take other breaks with cohabitants but everyone in your living area needs to understand you are at home to work.</p><p>If you have children that might have a particularly hard time with this, it can be useful to use some sort of physical signaling device (examples: a closed door, a light bulb being on, a closed curtain, headphones on) that you should not be interrupted.</p><h3 id="minimize-distractions">Minimize distractions</h3><p>This one is obvious but try to minimize distractions. Don&apos;t try to sit on your couch with the TV on and do work. You won&apos;t be doing great work.</p><p>If your home is loud and you have difficulty in a loud space, wear some ear plugs or noise canceling headphones.</p><p>If cohabitants are distractions, refer to the above section and have that discussion with them about needing space. One technique for dealing with interrupting cohabitants is to schedule time throughout your day for them. You can use these scheduled times as breaks through out your working day.</p><p>If you try to get some household chores done while working at home, make sure you schedule time for doing them. This could be putting the time on your calendar or simply setting a timer when taking a break. Regardless of the method, when your time is up, get back to work.</p><p>I&apos;ve often found that finishing a short, simple household task can actually jump-start finishing more complicated work tasks. Using that momentum from the household chore can make accomplishing work tasks easier.</p><h3 id="having-difficulty-starting-a-work-task?">Having difficulty starting a work task?</h3><p>Sometimes it is hard to start a task. It can be especially hard if you are new to working at home and not used to working in your environment.</p><p>One technique I&apos;ve found useful is the Pomodoro technique. The steps to this technique are below.</p><ol><li>Pick a task.</li><li>Set and start a timer (usually for 25 minutes).</li><li>Focus intensely on the task for the duration of the timer.</li><li>Make a mark on a piece of paper</li><li>If you have fewer than four marks on the paper, take a 5 minute break and then go back to step 2.</li><li>If you have four marks on the paper, then take a 15 minute break and go back to step 1.</li></ol><p>I don&apos;t follow those steps strictly and mostly use the trick of setting a timer for focused work. If at the end of the timer I feel like continuing, I&apos;ll reset the timer. If I need a break, I&apos;ll set the timer for a short period of time and take a break.</p><p>It was mentioned above, but sometimes doing a small, easy task can jump-start knocking out TODOs. This small, easy task could be something work related or some simple chore around the house.</p><h3 id="be-mindful-of-your-communication">Be mindful of your communication</h3><p>Text communication is hard. It is often taken more negative than intended. Be mindful of that.</p><p>Try to take what your coworkers write in the most positive way possible.</p><p>Try to be careful with your own written communication. It sounds ridiculous but emojis can help make you look like less of a jerk and set a friendly tone.</p><p>Don&apos;t hesitate to jump on a video or voice call with someone (or a group). Video is a much higher quality interaction than voice and both are much higher quality than text. The downside is the communication isn&apos;t persistent so be sure to write down outcomes of conversations.</p><h3 id="sync-up-with-your-team">Sync up with your team</h3><p>Try to sync up with your team (if you don&apos;t have a team, sync up with someone else from the company) at a regular interval. This should probably be at least once every couple days but it can be more regularly. I usually once a day.</p><p>It can be easy to feel like an island when you are part of a remote group. Regular sync-ups help reduce that feeling.</p><h3 id="collaborate-remotely">Collaborate remotely</h3><p>Most video conference software allows you to share your screen with others. Some of them even allow others to take control of your machine or treat your screen as a whiteboard.</p><p>Take advantage of these features. After learning how to use them, these features can often make remote collaboration as productive as in-person collaboration.</p><p>Using technology, you can even <a href="/blog/2015/01/24/remote-pairing/">pair program</a> with someone from another city.</p><p>Google Docs is another great remote collaboration tool. The best meetings I have been part of were meetings where every attendee was editing a shared Google Doc.</p><h3 id="video-meetings">Video Meetings</h3><p>When possible, have video meetings over voice only conference calls. The addition of body language through video makes remote conversations much better.</p><p>You might want to introduce hand gestures for signaling during video meetings<a href="#fn-3" id="fnref3"><sup>3</sup></a>. On a former team, we had the practice of raising a finger<a href="#fn-4" id="fnref4"><sup>4</sup></a> when you wanted to speak. This practice helped prevent people from interrupting and speaking over each other. It also let quieter people jump into conversations easier.</p><p>As far as I can tell, <a href="http://zoom.us/">Zoom</a> is still the winner in terms of video conferencing.</p><p>I also recommend using a headset with dedicated microphone for talking through your computer. The sound quality is usually better than using the built-in microphone.</p><h2 id="end">End</h2><p>It can be difficult to get good at working from home. It is definitely a skill that is learned through experience and reflection. If you have any questions about working remotely, feel free to reach out on <a href="https://twitter.com/jakemcc">twitter</a> or through <a href="mailto:jake@jakemccrary.com">email</a>.</p><p>Working from home can be a great experience.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>A desk can be any table that you can work on that is comfortable for a reasonable amount of time. It doesn&apos;t have to be what someone would typically think of as a desk.<a href="#fnref1">↩</a></p></li><li class="footnote" id="fn-2"><p>I used a table like this for years in college and when working an internship.<a href="#fnref2">↩</a></p></li><li class="footnote" id="fn-3"><p>These are also useful for in-person meetings.<a href="#fnref3">↩</a></p></li><li class="footnote" id="fn-4"><p>No, not the middle finger.<a href="#fnref4">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2020/02/25/auto-syncing-a-git-repository/index.html</id>
    <link href="https://jakemccrary.com/blog/2020/02/25/auto-syncing-a-git-repository/index.html"/>
    <title><![CDATA[Auto-syncing a git repository]]></title>
    <updated>2020-02-25T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>I&apos;m currently keep notes on my computer using plain text and <a href="https://orgmode.org/">Org mode</a>.</p><p>I keep my notes in a git repository in my home directory, <code>~/org/</code>. I want my notes to be synced between my computers without me thinking about it. Historically, I&apos;ve reached for something like Google Drive or Dropbox to do this but this time I reached for git and GitHub.</p><p>Below is the script that I ended up cobbling together from various sources found online. The script pushes and pulls changes from a remote repository and works on my macOS and linux machines.</p><p>The loop starting on line 38 does the work. Whenever a file-watcher notices a change or 10 minutes passes, the loop pulls changes from a remote repository, commits any local changes, and pushes to the remote repository. The lines before this are mostly checking that needed programs exist on the host.</p><p>I keep this running in a background terminal and I check periodically to confirm it is still running. I could do something fancier but this isn&apos;t a critical system and the overhead of checking every couple days is nearly zero. Most of the time checking happens by accident when I accidentally maximize the terminal that runs the script.</p><p>I&apos;ve been using this script for a long time now and I&apos;ve found it quite useful. I hope you do too.</p><pre><code class="language-bash">#!/bin/bash

set -e

TARGETDIR=&quot;$HOME/org/&quot;

stderr () {
    echo &quot;$1&quot; &gt;&amp;2
}

is_command() {
    command -v &quot;$1&quot; &amp;&gt;/dev/null
}

if [ &quot;$(uname)&quot; != &quot;Darwin&quot; ]; then
    INW=&quot;inotifywait&quot;;
    EVENTS=&quot;close_write,move,delete,create&quot;;
    INCOMMAND=&quot;\&quot;$INW\&quot; -qr -e \&quot;$EVENTS\&quot; --exclude \&quot;\.git\&quot; \&quot;$TARGETDIR\&quot;&quot;
else # if Mac, use fswatch
    INW=&quot;fswatch&quot;;
    # default events specified via a mask, see
    # https://emcrisostomo.github.io/fswatch/doc/1.14.0/fswatch.html/Invoking-fswatch.html#Numeric-Event-Flags
    # default of 414 = MovedTo + MovedFrom + Renamed + Removed + Updated + Created
    #                = 256 + 128+ 16 + 8 + 4 + 2
    EVENTS=&quot;--event=414&quot;
    INCOMMAND=&quot;\&quot;$INW\&quot; --recursive \&quot;$EVENTS\&quot; --exclude \&quot;\.git\&quot; --one-event \&quot;$TARGETDIR\&quot;&quot;
fi

for cmd in &quot;git&quot; &quot;$INW&quot; &quot;timeout&quot;; do
    # in OSX: `timeout` =&gt; brew install coreutils
    # in OSX: `fswatch` =&gt; brew install fswatch
    is_command &quot;$cmd&quot; || { stderr &quot;Error: Required command &apos;$cmd&apos; not found&quot;; exit 1; }
done

cd &quot;$TARGETDIR&quot;
echo &quot;$INCOMMAND&quot;

while true; do
    eval &quot;timeout 600 $INCOMMAND&quot; || true
    git pull
    sleep 5
    STATUS=$(git status -s)
    if [ -n &quot;$STATUS&quot; ]; then
        echo &quot;$STATUS&quot;
        echo &quot;commit!&quot;
        git add .
        git commit -m &quot;autocommit&quot;
        git push origin
    fi
done
</code></pre></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2020/01/11/reading-in-2019/index.html</id>
    <link href="https://jakemccrary.com/blog/2020/01/11/reading-in-2019/index.html"/>
    <title><![CDATA[Reading in 2019]]></title>
    <updated>2020-01-11T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>At the beginning of every year I reflect on the previous year of reading. I take a look at my records, fix errors, and think about reading goals for the upcoming year.</p><p>Here are links to my previous end-of-year reflections: <a href="/blog/2014/01/01/using-incanter-to-review-my-2013-reading/">2013</a>, <a href="/blog/2015/01/08/reading-in-2014/">2014</a>, <a href="/blog/2016/03/13/reading-in-2015/">2015</a>, <a href="/blog/2017/01/04/reading-in-2016/">2016</a>, <a href="/blog/2018/03/03/reading-in-2017/">2017</a>, and <a href="/blog/2019/01/21/reading-in-2018/">2018</a>.</p><p>I&apos;ve continued to keep track of my reading using <a href="http://goodreads.com">Goodreads</a>. My <a href="https://www.goodreads.com/user/show/3431614-jake-mccrary">profile</a> has nearly the full list of the books I&apos;ve read since 2010. <a href="https://www.goodreads.com/review/list/3431614-jake-mccrary?shelf=read_2019">This</a> is my 2019.</p><h2 id="2019-goal">2019 Goal</h2><blockquote><p>I have a stack of software and process books and I&apos;d like to read through at least some of them (others are more reference books). I&apos;m also going to bring over the 2018 goal of reading at least one book on writing. In a more general sense, I&apos;m hoping to put some practices together that help me gain more from the books I&apos;m reading. I&apos;m still thinking through what that means. - Me last year</p></blockquote><p>That was my goal for 2019. In list form it looks like this:</p><ol><li>Read some software or process books</li><li>Read at least one book on writing</li><li>Try to develop practices for getting more from books I&apos;ve read</li></ol><p>I read eight books related to the first goal and two (or three if I count an iffy one) related to the second. That is enough where I feel pretty good about claiming I accomplished the first two goals.</p><p>I mostly failed on the third goal. I was more aggressive about highlighting notes in my Kindle and I did occasionally look back at them. Beyond that I didn&apos;t do anything.</p><h2 id="highlights">Highlights</h2><p>Here are my five star books from 2019.</p><ul><li>Accelerate: Building and Scaling High-Performing Technology Organizations by Nicole Forsgren, Jez Humble, and Gene Kim (<a href="https://www.goodreads.com/review/show/2411886698">my review</a>)</li><li>Elements of Clojure by Zachary Tellman (<a href="https://www.goodreads.com/review/show/2684688658">my review</a>)</li><li>A Tour of C++ by Bjarne Stroustrup (<a href="https://www.goodreads.com/review/show/2822201641">my review</a>)</li><li>Developer Hegemony: The Future of Labor (<a href="https://www.goodreads.com/review/show/2795008954">my review</a>)</li><li>Digital Minimalism: Choosing a Focused Life in a Noisy World by Cal Newport</li><li>Effective Java by Joshua Bloch (<a href="https://www.goodreads.com/review/show/2701581699">my review</a>)</li><li>21 Lessons for the 21st Century by Yuval Noah Harari (<a href="https://www.goodreads.com/review/show/2562286276">my review</a>)</li><li>Draft No. 4: On the Writing Process by John McPhee (<a href="https://www.goodreads.com/review/show/2300752363">my review</a>)</li><li>The Push: A Climber&apos;s Journey of Endurance, Risk and Going Beyond Limits by Tommy Caldwell (<a href="https://www.goodreads.com/review/show/2200390288">my review</a>)</li><li>The Nickel Boys by Colson Whitehead</li><li>The Great Believers by Rebecca Makkai (<a href="https://www.goodreads.com/review/show/3020415147">my review</a>)</li><li>The Bonfire of the Vanities by Tom Wolfe (<a href="https://www.goodreads.com/review/show/2847590304">my review</a>)</li><li>The Paper Menagerie and Other Stories by Ken Liu (<a href="https://www.goodreads.com/review/show/1827462108">my review</a>)</li><li>Exhalation: Stories by Ted Chiang (<a href="https://www.goodreads.com/review/show/2804306157">my review</a>)</li><li>Golden Son (Red Rising, #2) by Pierce Brown</li><li>Morning Star (Red Rising Saga, #3) by Pierce Brown</li><li>Iron Gold (Red Rising Saga, #4) by Pierce Brown (<a href="https://www.goodreads.com/review/show/2760320870">my review</a>)</li><li>Dark Age (Red Rising Saga #5) by Pierce Brown</li><li>Animal Farm by George Orwell (<a href="https://www.goodreads.com/review/show/2811868686">my review</a>)</li></ul><h4 id="accelerate:-building-and-scaling-high-performing-technology-organizations-by-nicole-forsgren,-jez-humble,-and-gene-kim">Accelerate: Building and Scaling High-Performing Technology Organizations by Nicole Forsgren, Jez Humble, and Gene Kim</h4><p>This is a stellar book on practices of technology organizations that help build high performing companies. If you work at a company that produces software in any capacity, I&apos;d highly recommend this book. This is a book that I&apos;ve recommended to any coworker looking, and some not looking, for book recommendations.</p><h4 id="elements-of-clojure-by-zachary-tellman">Elements of Clojure by Zachary Tellman</h4><p>This book has Clojure in the title but it is applicable to more than that language. The book was published a section at a time and as a result I&apos;ve read parts of it many times. The content clearly shows that Zach has put a lot of thought into the topic.</p><h4 id="a-tour-of-c++-by-bjarne-stroustrup">A Tour of C++ by Bjarne Stroustrup</h4><p>I&apos;ve written C++ off and on since I started programming nearly 20 years ago. Over those years, I&apos;ve seen C++ transform as new versions were released. Earlier in 2019, I was starting to write C++ again and this book was recommended by a coworker. I had last written C++ back in 2013 and this book was a perfect way to refresh my stuck in early 2013 knowledge. There is no fluff in this book and it is full of useful information.</p><h4 id="developer-hegemony:-the-future-of-labor">Developer Hegemony: The Future of Labor</h4><p>This is a tough read. It isn&apos;t tough because of difficult writing. It is tough because it makes you depressed until you power through and reach the end.</p><p>This book delivers a very cynical look at corporations. It provides guidelines for getting ahead and climbing the corporate ladder.</p><p>Then the book promotes an alternative approach, that of doing your own thing and going independent. It makes a good case of it.</p><p>Beware of this one, it might make you question what you are doing with your career and life.</p><h4 id="digital-minimalism:-choosing-a-focused-life-in-a-noisy-world-by-cal-newport">Digital Minimalism: Choosing a Focused Life in a Noisy World by Cal Newport</h4><p>There is a common theme of awareness throughout many of the books I read and this book hits that theme. This book can help you become a more thoughtful user of technology. There are many useful recommendations in this book. One of them is the suggestion that you can use social media and other technology differently than how the creators want you to use it. In 2019 I wrote about how I use <a href="/blog/2019/04/30/how-i-use-social-media/">social media</a> which shows how I apply this idea.</p><h4 id="effective-java-by-joshua-bloch">Effective Java by Joshua Bloch</h4><p>I&apos;ve read earlier editions in the past and decided to read the latest edition when it seemed like I&apos;d be writing Java again. This book is still good and a must read if you work with Java.</p><h4 id="21-lessons-for-the-21st-century-by-yuval-noah-harari">21 Lessons for the 21st Century by Yuval Noah Harari</h4><p>This book covers a lot of ground.</p><p>Here is a quote from my friend <a href="https://twitter.com/deobald">Steven Deobald</a> about this book.</p><blockquote><p>Through stories and anecdotes woven into his almost unbelievably extensive research as a historian, &quot;21 Lessons&quot; is perhaps as entertaining and insightful as any other book I&apos;ve read. It is accessible to anyone and the ideas presented regarding the fate of our species are stitched together beautifully. The arc of the 21 chapters has a progressive, almost orchestral, quality to it. Each chapter builds on all those which precede it and although some chapters have surprisingly variable writing styles, none feels like Harari is attempting to showboat or to force his medium into the overly artistic.</p></blockquote><h4 id="draft-no.-4:-on-the-writing-process-by-john-mcphee">Draft No. 4: On the Writing Process by John McPhee</h4><p>This was a pleasure to read. I like reading books about writing and this is a good one that talks about McPhee&apos;s approach towards creative non-fiction.</p><h4 id="the-push:-a-climber&apos;s-journey-of-endurance,-risk-and-going-beyond-limits-by-tommy-caldwell">The Push: A Climber&apos;s Journey of Endurance, Risk and Going Beyond Limits by Tommy Caldwell</h4><p>What can I say? I&apos;m a sucker for books on climbing and learning more about the icons of the sport I love. If you&apos;ve watched the movie <a href="https://www.imdb.com/title/tt7286916/">The Dawn Wall</a> then some of this will be familiar to you.</p><h4 id="the-nickel-boys-by-colson-whitehead">The Nickel Boys by Colson Whitehead</h4><p>This is a great book. Go read the <a href="https://www.goodreads.com/book/show/42270835-the-nickel-boys">Goodreads</a> page and pick it up.</p><h4 id="the-great-believers-by-rebecca-makkai">The Great Believers by Rebecca Makkai</h4><p>A friend of mine gave me a copy of this book and I&apos;m glad she did. It tells the story of the AIDs epidemic in Chicago. This is a great piece of writing. I&apos;m not surprised at all that it has won many awards.</p><h4 id="the-bonfire-of-the-vanities-by-tom-wolfe">The Bonfire of the Vanities by Tom Wolfe</h4><p>This book is great. The satire just drips off the pages. There are passages in this book where you can just <em>feel</em> the anxiety of the characters.</p><p>Every character is despicable and it is wonderful.</p><h4 id="the-paper-menagerie-and-other-stories-by-ken-liu">The Paper Menagerie and Other Stories by Ken Liu</h4><p>This was my second time reading this book. It is an excellent collection of short stories.</p><p>The first time I read this book, in <a href="/blog/2017/01/04/reading-in-2016/">2016</a>, I read the stories in order. This time I took advantage of the Kindle&apos;s estimate of how long a chapter would take and I jumped around, picking out stories that fit how long I wanted to read. Both ways of reading this collection were excellent.</p><h4 id="exhalation:-stories-by-ted-chiang">Exhalation: Stories by Ted Chiang</h4><p>I absolutely loved Ted Chiang&apos;s <em>Stories of Your Life and Others</em> and was excited when this collection of stories was published. I had high hopes for this collection and I was not disappointed.</p><p>Some of the stories I had read prior to them being included in this collection but that didn&apos;t matter. I enjoyed reading the new stories and revisiting the previously published ones.</p><blockquote><p>We don’t normally think of it as such, but writing is a technology, which means that a literate person is someone whose thought processes are technologically mediated. We became cognitive cyborgs as soon as we became fluent readers, and the consequences of that were profound.</p></blockquote><p>That is a quote from a story in this collection. It felt right to include it in an article about reading.</p><h4 id="golden-son,-morning-star,-iron-gold,-and-dark-age-by-pierce-brown">Golden Son, Morning Star, Iron Gold, and Dark Age by Pierce Brown</h4><p>The four titles above are books two through five in Pierce Brown&apos;s <em>Red Rising</em> saga. I also read the first book in the series, Red Rising, in 2019 but it only earned a four star rating from me. I obviously enjoyed this series and devoured it.</p><p>The books tell the story of a world full of inequality. The world created is full of interesting characters and dilemmas.</p><h4 id="animal-farm-by-george-orwell">Animal Farm by George Orwell</h4><p>This was either my second or third time reading Animal Farm. It is still good. Reading it in 2019 and mapping in book behavior to the modern political climate was interesting.</p><h3 id="non-five-star-highlights">Non-Five Star highlights</h3><ul><li>Irresistible: The Rise of Addictive Technology and the Business of Keeping Us Hooked by Adam Alter (<a href="https://www.goodreads.com/review/show/1966643946">my review</a>)</li><li>Permanent Record by Edward Snowden (<a href="https://www.goodreads.com/review/show/2983575511">my review</a>)</li><li>Atonement by Ian McEwan (<a href="https://www.goodreads.com/review/show/1697924044">my review</a>)</li><li>I Hear You by Michael S. Sorensen (<a href="https://www.goodreads.com/review/show/2923747300">my review</a>)</li><li>Recursion by Blake Crouch (<a href="https://www.goodreads.com/review/show/2923743967">my review</a>)</li><li>Version Control by Dexter Palmer (<a href="https://www.goodreads.com/review/show/1827462266">my review</a>)</li></ul><h4 id="irresistible:-the-rise-of-addictive-technology-and-the-business-of-keeping-us-hooked-by-adam-alter">Irresistible: The Rise of Addictive Technology and the Business of Keeping Us Hooked by Adam Alter</h4><p>Yet another book that is at least somewhat about awareness. This book talks about behavioral addiction but not just addictive technology.</p><p>Is it the single book out of the handful of books I&apos;ve read in this space that I&apos;d recommend? No, but it is a good addition to my collection on the topic.</p><h4 id="permanent-record-by-edward-snowden">Permanent Record by Edward Snowden</h4><p>I consider myself fairly knowledgeable about Snowden and what he did but I still learned more through this book. One part I particularly enjoyed was Snowden reflecting on what has changed since his actions.</p><p>Another part I particularly enjoyed was Snowden&apos;s telling of the early Internet. This was an Internet where identities online weren&apos;t necessarily tied to a real one. I&apos;m approximately the same age as Snowden and had similar experiences with being a young person during the early Internet days. It was interesting to be reminded of that time while reading this book.</p><p>I <a href="https://www.goodreads.com/notes/49833241-permanent-record/3431614-jake-mccrary?ref=bsop">highlighted</a> a lot of passages and there are probably more I should have highlighted.</p><h4 id="atonement-by-ian-mcewan">Atonement by Ian McEwan</h4><p>This book was so close to being five stars. I started reading this book because I mistook the title for that of a science fiction book I&apos;ve been intending to read. I&apos;m glad I did.</p><p>It took me a little while to get into the book but once I did I was hooked.</p><p><a href="https://www.goodreads.com/review/show/1697596771?book_show_action=false">Here</a> is a review from one of my friends that captures some of what I felt about this book.</p><h4 id="i-hear-you-by-michael-s.-sorensen">I Hear You by Michael S. Sorensen</h4><p>It provides some guidance towards being a more validating person. The book is short and to the point. I&apos;ve managed to take some of its advice and I think it has been useful.</p><h4 id="recursion-by-blake-crouch">Recursion by Blake Crouch</h4><p>This was really good. It is action packed and an interesting concept.</p><h4 id="version-control-by-dexter-palmer">Version Control by Dexter Palmer</h4><p>I really enjoy this book. It tells the story of a relationship with bits of science fiction. I really enjoyed my friend <a href="https://www.goodreads.com/review/show/2240581079?book_show_action=false">Dan&apos;s</a> review.</p><h2 id="stats">Stats</h2><p>The page count numbers for 2019 books are a bit screwed up so I&apos;m only doing a books per month graph this year.</p><p><img alt="Book and pages count by month" src="/images/reading-by-month-2019.svg" title="Number of books in each month" /></p><p>Unsurprisingly, electronic books continue to be the dominate format.</p><pre><code>|           | 2019 | 2018 | 2017 | 2016 | 2015 |
|-----------+------+------+------+------+------|
| ebook     |   43 |   37 |   37 |   56 |   47 |
| hardcover |    1 |    1 |    7 |    0 |    1 |
| paperback |    7 |    5 |    5 |    3 |    3 |
</code></pre><p>This year I read a noticeably higher amount of non-fiction books than in 2018.</p><pre><code>|             | 2019 | 2018 |
|-------------+------+------|
| fiction     |   28 |   29 |
| non-fiction |   23 |   14 |
</code></pre><h2 id="2020-goals">2020 Goals</h2><p>I was encouraged by how many non-fiction books I read this year and how many of them ended up earning a five star rating. I&apos;d like to continue that trend of reading high-quality non-fiction books.</p><p>I&apos;ve also been reading a lot of books but I haven&apos;t always been the best at trying to consciously apply the lessons from those books. I&apos;m going to try to improve that this year.</p><p>Those are pretty fuzzy goals but I&apos;m alright with that.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2019/10/12/building-an-atreus-keyboard/index.html</id>
    <link href="https://jakemccrary.com/blog/2019/10/12/building-an-atreus-keyboard/index.html"/>
    <title><![CDATA[Building an Atreus keyboard]]></title>
    <updated>2019-10-12T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>I recently built an <a href="https://atreus.technomancy.us">Atreus keyboard</a>. This keyboard is an extremely small keyboard with only 42 keys. Below is the photo of my result.</p><p><img alt="My completed Atreus keyboard" src="/images/atreus/atreus-done.jpg" /></p><p>As you can see, it has a split layout and the keys are aligned vertically and staggered. Thanks to using Ergodox keyboards since 2014, I&apos;m very used to this key layout and find it superior to traditional keyboards.</p><p>The keyboard is very small. To give you an idea of how small it is, here is a photo of it next to one of my Ergodox keyboards and with a bit of my fingers in the shot.</p><p><img alt="Atreus size comparison" src="/images/atreus/atreus-with-ergodox.jpg" /></p><p>Building the keyboard was pretty straight forward. The included instructions are thorough and include plenty of photos. All of the components are through-hole so the soldering is not difficult. This would be a good first keyboard project.</p><p>I already had USB cable, key switches, key caps, and a micro-controller so I purchased the partial kit from Phil. It came with everything else, except for something to coat the wood, that you need to build the keyboard. I wanted to connect the keyboard to USB C ports, so I used a <a href="https://www.amazon.com/gp/product/B07CWDYP18">micro to USB C cable</a>.</p><p>I enjoyed the color of the laser cut wood and appreciated the burn marks. I didn&apos;t want to lose the color or burns so I coated the wood with a water based clear polyurethane with a satin finish. This was probably the most difficult part of the build, and it was pretty easy, simply because I lack experience finishing wood.</p><p>When reading other build logs I noticed that someone else put a zip tie on their USB cable to help prevent it from tugging on the micro-controller. I have no idea how helpful this is but it seemed like a good thing to do so I also did it. To do this you basically just wrap the cable with a zip tie and cram it against the case so that the zip tie prevents tugging on the micro-controller. You can see it in the picture below.</p><p><img alt="My completed Atreus keyboard" src="/images/atreus/atreus-inside-zip-tie.jpg" /></p><p>I&apos;ve only been typing on the keyboard for basically this blog post but I&apos;ve already found myself adapting to it pretty quickly. I don&apos;t intend for it to replace my Ergodox for normal usage but I think it will be a great portable keyboard.</p><p>Overall it was a fun project and I&apos;m glad I did it. I look forward to customizing the firmware to make the key layout fit my usage.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2019/08/23/building-a-onewheel-stand/index.html</id>
    <link href="https://jakemccrary.com/blog/2019/08/23/building-a-onewheel-stand/index.html"/>
    <title><![CDATA[Building a Onewheel stand]]></title>
    <updated>2019-08-23T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>I&apos;ve owned a <a href="https://onewheel.com/products/xr">Onewheel XR</a> for about a year now. It is a one-wheeled electric skateboard-like device that is super fun for zipping around Chicago.</p><p>When I first got it, I purchased a <a href="https://www.amazon.com/gp/product/B06XPCNFRQ">small guitar stand</a>. It worked but it was always a bit finicky and I was never satisfied with it. I had to sit the Onewheel on it just right to have it stay on it without causing the legs of the stand to spread too wide.</p><p>This resulted in me buying a <a href="https://www.amazon.com/gp/product/B01C5G2TQY">second guitar stand</a> and trying that out. This one was even worse.</p><p>I grew frustrated with these non-purpose built stands and started looking into purchasing a Onewheel stand. There are plenty of beautiful stands out there, both officially from <a href="https://onewheel.com/collections/onewheel-xr-accessories/products/onewheel-deep-shack-rack">Future Motion</a> and from third party vendors like <a href="https://float-supply.com/product-category/stands/">The Float Life</a>.</p><p>Then I remembered that my old coworker, Tom Marsh, built his own and put the <a href="https://www.thingiverse.com/thing:3682716">plans online</a>. This inspired me to go the DIY route.</p><p>I thought that a stand made out of pipe would look pretty good and be easy to construct. It also gave me a good excuse to ride my Onewheel to Home Depot.</p><p>I explored the plumbing section of Home Depot and bought a variety of pipe and pipe fittings and took them back home to experiment with putting them together.</p><p>I ended up building the stand below.</p><p><img alt="onewheel pipe stand" src="/images/onewheel-pipe-stand.jpg" title="Onewheel pipe stand" /></p><p><img alt="onewheel in pipe stand" src="/images/onewheel-in-pipe-stand.png" title="Onewheel in pipe stand" /></p><p>I think the above stand looks great and it was easy to build.</p><p>Here is the part list:</p><ul><li>2 1/2 inch x 8 inch nipple</li><li>1 1/2 inch x 6 inch nipple</li><li>2 1/2 inch x 3 inch nipple</li><li>2 1/2 inch 90 degree elbow</li><li>2 1/2 inch 3-way side outlet</li><li>2 1/2 inch cap</li></ul><p>I washed off the black coating using Goo Gone and then assembled the stand. This ups the risk of rust but I think that might actually look cool so I&apos;m not too worried about it. You could optionally coat the pipes for some protection.</p><p>Once you have the parts the assembly is very straight forward. The only additional work I might do is to put some rubber feet on the bottom to prevent scratches to my floor.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2019/08/15/switching-my-ergodox-to-qmk-firmware/index.html</id>
    <link href="https://jakemccrary.com/blog/2019/08/15/switching-my-ergodox-to-qmk-firmware/index.html"/>
    <title><![CDATA[Switching my Ergodox to QMK firmware]]></title>
    <updated>2019-08-15T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Last fall I started to work in an office again. I&apos;ve used a hand-built Ergodox for years now and really prefer working on it. This meant I needed another ergodox for the office. Luckily, now you don&apos;t have to build your own. I bought an <a href="https://ergodox-ez.com">Ergodox EZ</a><a href="#fn-1" id="fnref1"><sup>1</sup></a>.</p><p>The Ergodox EZ uses the <a href="https://github.com/qmk/qmk_firmware">QMK</a> firmware. This has a lot of fancier options than the firmware I had been using on my hand-built ergodox.</p><p>This mostly didn&apos;t matter and I just configured the Ergodox EZ to match my original Ergodox&apos;s layout. Then I started a new job and found myself programming in Scala using IntelliJ IDEA.</p><p>Shockingly, after not using IntelliJ for years, I still remembered many of the keyboard shortcuts. This was great! Unfortunately, in my years since last using IntelliJ, I created some conflicting keyboard shortcuts for managing my window layout. These were mostly shortcuts that involved holding Command + Alt and pushing an arrow key. Luckily, the QMK firmware supports a <em>Meh</em> key.</p><p>What is the <em>Meh</em> key? It is a key that presses Control + Alt + Shift all at the same time.</p><p>This is great for setting up shortcuts that don&apos;t conflict with ones found in most normal programs. This let me <a href="https://github.com/jakemcc/cljs-phoenix/commit/fa2186589d99a4763c7bf79e1f795cb910063a4e">change my window manger</a> shortcuts to use the <em>Meh</em> key and I was then conflict free.</p><p>I can&apos;t handle having different shortcuts across different machines with the same OS, so I needed to needed to update my original Ergodox to use the QMK firmware so I could also have a <em>Meh</em> key at home. Luckily, the QMK firmware also works on it and, maybe even more luckily, the Ergodox EZ firmware just works with my original Ergodox.</p><p>This actually means I can simply take the compiled Ergodox EZ firmware and flash it straight to my Ergodox. Any time I&apos;ve done this the keyboard keeps working.</p><p>Unfortunately, the LEDs in my original Ergodox are mostly hidden by non-translucent keys. These LEDs indicate when I&apos;m not main layer and I find them really useful. I only have a single translucent keycap and would prefer only that LED to be used.</p><p>Here are the steps I took to make that change.</p><ol><li>Use the <a href="https://config.qmk.fm/#/ergodox_ez/LAYOUT_ergodox">graphical QMK Configurator</a> to visually configure my keyboard layout. In the <strong>Keymap Name</strong> field, put <code>jakemcc</code>.</li><li>Click the <strong>Compile</strong> button in the above configurator.</li><li>Download the full source.</li><li>Unzip the source and edit <code>qmk_firmware/keyboards/ergodox_ez/keymaps/jakemcc/keymap.c</code> to include snippet of code below this list.</li><li>In <code>qmk_firmware</code> run <code>make ergodox_ez:jakemcc</code>.</li><li>Find <code>ergodox_ez_jakemcc.hex</code> and flash my original Ergodox.</li></ol><pre><code class="language-c">uint32_t layer_state_set_user(uint32_t state) {
  if (biton32(state) == 0) {
    ergodox_right_led_1_off();
  } else {
    ergodox_right_led_1_on();
  }
  return state;
}
</code></pre><p>This snippet gets added to the bottom of the <code>keymap.c</code>. It only turns on led 1, which is the one under my translucent key, whenever my keyboard isn&apos;t on layer 0.</p><p>Now, I can use the fancy <em>Meh</em> key to be conflict free and easily tell when I&apos;m not on my main layer. This is wonderful.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>I bought one with Cherry MX Clear switches. I&apos;ve since switched them to Cherry MX Browns. The clears were too firm for me. I did not get Cherry MX Blues because I didn&apos;t want my fellow coworkers to be annoyed by the glorious clickty-clack of those switches.<a href="#fnref1">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2019/04/30/how-i-use-social-media/index.html</id>
    <link href="https://jakemccrary.com/blog/2019/04/30/how-i-use-social-media/index.html"/>
    <title><![CDATA[How I use social media]]></title>
    <updated>2019-04-30T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Over the years, I&apos;ve read many articles about the negative aspects of social media. You&apos;ve probably read articles extolling the benefits of cutting social media out of your life. These articles are abundant and easy to find through a search for <a href="https://www.google.com/search?q=stop+social+media">&quot;stop social media&quot;</a> or <a href="https://www.google.com/search?q=quit+social+media">&quot;quit social media&quot;</a>.</p><p>Social media hasn&apos;t played a significant role in my life for a couple of years. I first started being more mindful of how I consumed social media in 2013. Back then, I temporarily switched to using a <a href="https://en.wikipedia.org/wiki/Feature_phone">feature phone</a> (a non-smart phone) for a month and a half. This really reset my relationship with consuming media on a phone. Since my phone was my primary entry point into Twitter and Facebook, my usage of both plummeted.</p><p>Since then, I&apos;ve continued to take a careful look at how I use social media and have made tweaks to get maximum enjoyment with minimal downsides. This has involved changing how I use the desktop web applications for both Twitter and Facebook<a href="#fn-1" id="fnref1"><sup>1</sup></a>.</p><p>The following books have helped shape my thinking towards digital distractions. They&apos;ve put into words some of the practices I stumbled into. They&apos;ve affected how I use smart phones and how I approach social media.</p><ul><li><a href="https://www.amazon.com/Deep-Work-Focused-Success-Distracted-ebook/dp/B00X47ZVXM">Deep Work</a> by Cal Newport</li><li><a href="https://www.amazon.com/dp/B07DBRBP7G">Digital Minimalism</a> by Cal Newport</li><li><a href="https://www.amazon.com/dp/B00G1J1D28">Essentialism</a> by Greg Mckeown</li><li><a href="https://www.amazon.com/Distracted-Mind-MIT-Press-High-Tech/dp/0262534436">The Distracted Mind</a> by Adam Gazzaley &amp; Larry D. Rosen</li></ul><p>One of the ideas in both <em>Digital Minimalism</em> and <em>Essentialism</em> is that you can pick and choose what you add to your life. This extends to individual features of products you use. This is something I arrived at prior to reading these books and it was nice hearing others putting this idea into words.</p><p>Below is how I&apos;ve chosen to use various social media platforms.</p><h2 id="twitter">Twitter</h2><p>I only consume Twitter on my computer and I read it through <a href="https://tweetdeck.twitter.com/">Tweetdeck</a>.</p><p>I don&apos;t check my entire feed. Instead, I have Tweetdeck setup to display a few curated lists of accounts along with mentions and direct messages. One list is composed of close friends, another highlights some people in the software development space, and another contains some Twitter art projects.</p><p>Because I focus on a limited number of accounts, I don&apos;t have an infinite list to scroll through. This focus keeps Twitter useful to me and allows me to check it every few days and still stay up to date on topics I care about.</p><p>I rarely tweet but when I do it is usually to promote my own or another person&apos;s writing. I also occasionally tweet as an <a href="https://twitter.com/ErowidBobRoss">art bot</a>.</p><h2 id="facebook">Facebook</h2><p>I only consume Facebook on my computer and mostly stopped using the website in 2016. The 2016 US presidential election made me realize I didn&apos;t find the Facebook news feed useful. It did not add positive value to my life.</p><p>That is when I found the <a href="https://chrome.google.com/webstore/detail/news-feed-eradicator-for/fjcldmjmjhkklehbacihaiopjklihlgg?hl=en">News Feed Eradicator</a> Chrome extension. This extension gets rid of the news feed. It is great.</p><p>Without the news feed, I no longer open the site and mindlessly scroll through the firehose of updates. I no longer know what is going on in the curated lives of my friends that still use Facebook. That is ok. Now when I run into them in real life, I can catch up and learn about their kids and their lives. I can have an honest reaction to learning that someone got married instead of sort of already knowing it. Someone can tell me about a trip they took and can show me photos I&apos;ve never seen before.</p><p>I haven&apos;t completely deleted my Facebook account because it does add value to my life through a couple of groups and Facebook messenger. Only using these features has reduced the frequency I visit Facebook to once every few days. That is more than enough to keep up with what is going in in the Chicago climbing community and events going on at local climbing gyms.</p><p>I rarely post to Facebook but when I do it is often to promote something I&apos;ve written.</p><h2 id="goodreads">Goodreads</h2><p>I&apos;m not really sure if Goodreads counts as a social media site. I use it to keep track books I want to read and books I&apos;ve already read. It isn&apos;t something that consumes any amount of my time mindlessly.</p><h2 id="linkedin">LinkedIn</h2><p>I&apos;m not sure if you can consider my usage of LinkedIn to be actual usage. It mostly results in email in my inbox that almost immediately gets archived. It does keep me somewhat informed about what job opportunities are out there though recruiter outreach.</p><p>I very rarely post anything to LinkedIn.</p><h2 id="instagram">Instagram</h2><p>I&apos;ll completely admit that this is the social media platform that I waste time on. It is the only social media app on my phone and that increases how frequently I use it.</p><p>I signed up for Instagram in order to follow tattoo artists. This helped me learn what tattoo styles I enjoyed the most. This was a huge success and now I have a much better appreciation and eye for this art.</p><p>Eventually, my usage of Instagram expanded to follow some friends, local Chicago artists, and professional rock climbers. Following each of these groups is slightly beneficial but I&apos;m not sure if it is an overall positive impact compared to the temptation to fill downtime with Instagram scrolling.</p><p>I&apos;m approaching the point of deleting Instagram from my phone and experiencing that.</p><p>I post occasionally to Instagram both using the story feature and normal posts. These are usually photos of some street art or stickers put up in Chicago. It is very infrequent.</p><h1 id="end">End</h1><p>So that is how I consume social media. It mostly happens on my computer and I use a subset of features a platform offers. I&apos;ve reached a point where I feel like I&apos;m getting a lot of the pros without too many of the cons.</p><p>It is an area in which I&apos;ll keep experimenting. I&apos;d encourage you to as well. Try a different usage pattern for an extended period of time and then reflect on your changed behavior. Keep the changes that have made a positive impact.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>Ignoring LinkedIn and Goodreads, I think Facebook and Twitter were the only social media platforms I used back then.<a href="#fnref1">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2019/03/20/breaking-change-and-more-in-lein-test-refresh-0-dot-24-dot-0/index.html</id>
    <link href="https://jakemccrary.com/blog/2019/03/20/breaking-change-and-more-in-lein-test-refresh-0-dot-24-dot-0/index.html"/>
    <title><![CDATA[Breaking change and more in lein-test-refresh 0.24.1]]></title>
    <updated>2019-03-20T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Today I released <a href="https://github.com/jakemcc/test-refresh">lein-test-refresh</a> <code>0.24.1</code><a href="#fn-1" id="fnref1"><sup>1</sup></a>. I don&apos;t always announce new lein-test-refresh versions with an article but this release breaks some existing behavior so I thought it was worth it.</p><p>Each of these changes is the direct result of interacting with four different <code>lein-test-refresh</code> users. Some of this took place on GitHub and others through email. Thanks to all of you for taking the time to think about improvements and notice oddities and bring them to my attention.</p><h3 id="breaking-change:-monitoring-keystrokes-to-perform-actions">Breaking change: Monitoring keystrokes to perform actions</h3><p>Prior to this release, if you hit Ctrl-D then STDIN reads an EOF and <code>test-refresh</code> would quit. With version 0.24.1, <code>test-refresh</code> no longer does that. Instead, it stops monitoring for input and just keeps running tests. Since it stops monitoring for input it no longer notices when you hit Enter to cause your tests to rerun. You can still stop <code>lein test-refresh</code> by sending a SIGINT with Ctrl-C.</p><p>This change was made because there is some combination of environments where if <code>test-refresh</code> execs <code>/bin/bash</code> then it receives an EOF on STDIN. Before this change, that means <code>test-refresh</code> would quit unexpectedly. Now it will keep going.</p><p>Thanks <a href="https://github.com/cloojure">Alan Thompson</a> for bringing this to my attention and taking the time to help diagnose the problem.</p><h3 id="you-can-supply-your-own-narrowing-test-selector">You can supply your own narrowing test selector</h3><p>Being able to tell <code>test-refresh</code> to narrow its focus by adding <code>:test-refresh/focus</code> as metadata on a test or namespace has quickly become a favorite feature of many users. Now you can configure a shorter keyword by specifying configuration in your profile. See the <a href="https://github.com/jakemcc/test-refresh/blob/1b5165660d9e40d9394809a95b148ec758a6d56b/sample.project.clj#L61-L65">sample project.clj</a> for how to set this up.</p><p>Thanks <a href="https://github.com/metametadata">Yuri Govorushchenko</a> for the suggestion.</p><h3 id="experimental:-run-in-a-repl">Experimental: Run in a repl</h3><p>I&apos;ve turned down this feature in the past but a narrower request came up and I thought it seemed useful. <code>test-refresh</code> now exposes a function you can call in a repl to run <code>test-refresh</code> in that repl. This makes the repl useless for any other task. To do this, first add <code>lein-test-refresh</code> as a dependency instead of a plugin to your project.clj. Then, require the namespace and call the function passing in one or more paths to your test directories. Example below.</p><pre><code class="language-clojure">user=&gt; (require &apos;com.jakemccrary.test-refresh)
nil
user=&gt; (com.jakemccrary.test-refresh/run-in-repl &quot;test&quot;)
*********************************************
*************** Running tests ***************
</code></pre><p><a href="https://github.com/jakemcc/test-refresh/issues/80">This request</a> was done so that you can run it in Cursive&apos;s repl and gain the ability to click on stacktraces. Thanks <a href="https://github.com/klauswuestefeld">Klaus Wuestefeld</a> for bringing this up again with a really solid and focused use case.</p><h3 id="better-output-on-exceptions-while-reloading">Better output on exceptions while reloading</h3><p>This was a <a href="https://github.com/jakemcc/test-refresh/pull/81">pull request</a> from <a href="https://github.com/minhtuannguyen">Minh Tuan Nguyen</a>. Now figuring out where to look for the error will be a little easier.</p><h2 id="thank-you">Thank you</h2><p>Thanks to all the users of lein-test-refresh. I&apos;ve found it to be very valuable to the way I work and I&apos;m very happy that others do as well.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>This was originally 0.24.0 but that had a bug in it. Sorry about that.<a href="#fnref1">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2019/02/13/testing-asynchronous-javascript-with-jasmine/index.html</id>
    <link href="https://jakemccrary.com/blog/2019/02/13/testing-asynchronous-javascript-with-jasmine/index.html"/>
    <title><![CDATA[Testing asynchronous JavaScript with Jasmine]]></title>
    <updated>2019-02-13T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>I was recently adding a feature to an internal web UI that caught all unhandled JavaScript errors and reported them to the backend service. The implementation went smoothly with most of the effort spent figuring out how to test the code that was reporting the errors.</p><p>If the error reporting failed, I didn&apos;t want to trigger reporting another error or completely lose that error. I decided to log a reporting error to the console. I wanted to write a test showing that errors reporting errors were handled so that a future me, or another developer, didn&apos;t accidentally remove this special error handling and enable a never ending cycle of of reporting failed reporting attempts.</p><p>It took me a while to figure out how to do this. I searched the web and found various articles about using <a href="https://jasmine.github.io/">Jasmine</a> to do async tests. They were helpful but I also wanted to mock out a function, <code>console.error</code>, and assert that it was called. None of the examples I found were explicit about doing something like this. I forget how many different approaches I tried, but it took a while to figure out the below solution.</p><p>Here is the code I wanted to test.</p><pre><code class="language-javascript">function reportEvent(event) {
  return fetch(&apos;/report-event&apos;, {
    method: &apos;POST&apos;,
    headers: {&apos;Content-Type&apos;: &apos;application/json&apos;},
    body: JSON.stringify({name: &apos;ui name&apos;, ...event})
  }).catch(function(e) { console.error(&apos;Problem reporting event:&apos;, e)});
}
</code></pre><p>It takes an incoming <code>event</code> object and merges it with a default value and posts that to the backing service. <code>fetch</code> returns a Promise and the code handles errors by calling <code>catch</code> on it and logging.</p><p>Below is what I eventually came up with for testing the error handling feature of <code>reportEvent</code>.</p><pre><code class="language-javascript">describe(&apos;reporting events&apos;, function() {
  it(&apos;logs errors&apos;, (done) =&gt; {
    spyOn(console, &apos;error&apos;).and.callFake(() =&gt; {
      expect(console.error).toHaveBeenCalled();
      done();
    });
    spyOn(window, &apos;fetch&apos;).and.returnValue(Promise.reject(&apos;error!&apos;));
    reportEvent({level: &apos;WARN&apos;, msg: &apos;ERROR!&apos;});
  });
});
</code></pre><p>This uses <code>spyOn</code> to mock out <code>fetch</code> and <code>console.error</code>. The <code>fetch</code> call is told to return a rejected Promise. The <code>console.error</code> spy is a bit more interesting.</p><p>The <code>console.error</code> spy is told to call a fake function. That function asserts that the <code>console.error</code> spy has been called. More importantly, it also calls a <code>done</code> function. That <code>done</code> function is a callback passed to your test by Jasmine. Calling <code>done</code> signals that your async work is completed.</p><p>If <code>done</code> is never called then Jasmine will fail the test after some amount of time. By calling <code>done</code> in our <code>console.error</code> fake, we&apos;re able to signal to Jasmine that we&apos;ve handled the rejected promise.</p><p>You don&apos;t actually need the <code>expect(console.error).toHaveBeenCalled();</code> as <code>done</code> won&apos;t be called unless <code>console.error</code> has been called. If you don&apos;t have it though then Jasmine will complain there are no assertions in the test.</p><p>So there we have it, an example of using some of Jasmine&apos;s asynchronous test features with spies. I wish I had found an article like this when I started this task. Hopefully it saves you, and future me, some time.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2019/01/28/how-to-use-leiningen-test-selectors-to-filter-by-test-name/index.html</id>
    <link href="https://jakemccrary.com/blog/2019/01/28/how-to-use-leiningen-test-selectors-to-filter-by-test-name/index.html"/>
    <title><![CDATA[How to use Leiningen test selectors to filter by test name]]></title>
    <updated>2019-01-28T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><!-- Estimate: 30 minutes --><!-- First draft: 38 minutes --><!-- First edit: 13 minutes --><!-- Second edit: 8 minutes --><p>Leiningen test selectors are great. They allow you to filter what tests run by applying a function to the test&apos;s metadata. If that function returns a truthy value then that test will run. <a href="https://github.com/jakemcc/test-refresh/blob/master/CHANGES.md#040">lein-test-refresh</a> supports them and even includes a built in one for its <a href="https://github.com/jakemcc/test-refresh#built-in-test-narrowing-test-selector">focus feature</a>.</p><p>I was recently <a href="https://github.com/jakemcc/test-refresh/issues/78">asked</a> if test-refresh could support filtering tests using a regular expression against the name of a namespace or test. Lucky for me, test-refresh already supports this because of its support of test selectors.</p><p>Most of the examples of Leiningen test selectors show very simple functions that look for the existence of a keyword in the metadata. We can do more than that. We can write a predicate that does whatever we want with the metadata.</p><p>To take a look at a test&apos;s metadata, I generated a new project and looked at the generated default test file.</p><pre><code class="language-clojure">(ns selector.core-test
  (:require [clojure.test :refer :all]
            [selector.core :refer :all]))

(deftest a-test
  (testing &quot;FIXME, I fail.&quot;
    (is (= 0 1))))
</code></pre><p>I then used my repl and to see what metadata was on the test.</p><pre><code class="language-clojure">selector.core-test&gt; (meta #&apos;a-test)
{:test #function[selector.core-test/fn--17267],
 :line 5,
 :column 1,
 :file &quot;/Users/jake/src/jakemcc/blog-examples/selector/test/selector/core_test.clj&quot;,
 :name a-test,
 :ns #namespace[selector.core-test]}
</code></pre><p>Given the metadata above, I wrote the selector below which lets us select only integration tests.</p><pre><code class="language-clojure">:test-selectors {:integration (fn [m]
                                (or (clojure.string/includes? (str (:ns m))
                                                              &quot;integration&quot;)
                                    (clojure.string/includes? (str (:name m))
                                                              &quot;integration&quot;)))}
</code></pre><p>You could write the above code is many different ways. Whatever you write, it needs to look for the existence of <code>integration</code> in either the test&apos;s name or namespace.</p><p>If you wanted to make <code>lein test</code> or <code>lein test-refresh</code> only run non-integration tests you can add a default test selector to the project.clj.</p><pre><code class="language-clojure">:test-selectors {:default (fn [m]
                            (not (or (clojure.string/includes? (str (:ns m))
                                                               &quot;integration&quot;)
                                     (clojure.string/includes? (str (:name m))
                                                               &quot;integration&quot;))))
                 :integration (fn [m]
                                (or (clojure.string/includes? (str (:ns m))
                                                              &quot;integration&quot;)
                                    (clojure.string/includes? (str (:name m))
                                                              &quot;integration&quot;)))}
</code></pre><p>Enjoy! I hope this example helps you run a subset<a href="#fn-1" id="fnref1"><sup>1</sup></a> of your Clojure tests through Leiningen test selectors.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>Running a subset of your tests can be helpful and test-refresh has a few features that help you do that. If you can, I&apos;d still recommend making all your tests fast enough to run them all the time.<a href="#fnref1">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2019/01/27/how-to-display-a-message-to-all-tmux-clients/index.html</id>
    <link href="https://jakemccrary.com/blog/2019/01/27/how-to-display-a-message-to-all-tmux-clients/index.html"/>
    <title><![CDATA[How to display a message to all tmux clients]]></title>
    <updated>2019-01-27T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><!-- Estimated time: 30 minutes --><!-- First Draft: 26 minutes --><!-- First edits: 14 minutes --><p>Lately, I&apos;ve been using <a href="https://github.com/tmux/tmux">tmux</a> a lot. This resulted in me figuring out how to get <a href="https://github.com/jakemcc/test-refresh#notifications">lein-test-refresh</a> to send <a href="/blog/2019/01/06/notifications-with-tmux-and-lein-test-refresh/">notifications using tmux</a>.</p><p>The setup linked above works great for when I&apos;m doing work all by myself. It showed a problem when using ssh and tmux to pair with another developer. Instead of both developers receiving a notification, only one did. One is better than none but not ideal.</p><p>Below is a GIF showing the problem. Each window simulates a different developer.</p><p><img alt="tmux only showing one developer a notification" src="/images/tmux-pair-fail.gif" /></p><p>This wasn&apos;t too hard to fix. A little digging through the tmux manpage shows that <code>tmux display-message</code> takes an optional flag telling it which client receives the message. If we can get a list of all the clients then iterating over them and sending a message to each is straightforward.</p><p><code>tmux list-clients</code> give us this list. Below is the output.</p><pre><code>$ tmux list-clients
/dev/ttys002: 0 [78x41 xterm-256color] (utf8)
/dev/ttys006: 0 [78x42 xterm-256color] (utf8)
</code></pre><p>What we care about are the parts that look like <code>/dev/ttys002</code>. At first I used <code>cut</code> to grab these values but then I dug a bit deeper into the <code>tmux</code> manpage.</p><p>It turns out that you can specify a format to <code>tmux list-clients</code>. Running <code>tmux list-clients -F &quot;#{client_name}&quot;</code> gives us the output we care about.</p><pre><code>$ tmux list-clients -F &quot;#{client_name}&quot;
/dev/ttys002
/dev/ttys006
</code></pre><p>We can combine that with <code>xargs</code> to send a message to multiple clients.</p><p><img alt="tmux xargs example" src="/images/tmux-xargs-example.gif" /></p><p>That command is a bit much to put into <code>lein-test-refresh</code>&apos;s configuration so I shoved it in a script called <code>notify</code> and configured <code>lein-test-refresh</code> to use it. Script and GIF of that below. Now both you and your pair can get notifications.</p><pre><code class="language-bash">#!/bin/bash

USAGE=&quot;Usage: notify &lt;message&gt;

example: notify &apos;Tests passed!&apos;&quot;

if [ -z &quot;$1&quot; ]; then
    echo &quot;$USAGE&quot;
    exit 1
fi

message=&quot;$1&quot;

tmux list-clients -F &quot;#{client_name}&quot; \
    | xargs -n1 -I{} tmux display-message -c {} &quot;$message&quot;
</code></pre><p><img alt="Example using notify script" src="/images/tmux-notify-script.gif" /></p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2019/01/21/reading-in-2018/index.html</id>
    <link href="https://jakemccrary.com/blog/2019/01/21/reading-in-2018/index.html"/>
    <title><![CDATA[Reading in 2018]]></title>
    <updated>2019-01-21T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>At the beginning of every year I like to take the time to reflect on my previous year&apos;s reading. It gives me a time to correct my data and think about where I want my reading to go in the upcoming year.</p><p>Here are links to my previous end-of-year reflections: <a href="/blog/2014/01/01/using-incanter-to-review-my-2013-reading/">2013</a>, <a href="/blog/2015/01/08/reading-in-2014/">2014</a>, <a href="/blog/2016/03/13/reading-in-2015/">2015</a>, <a href="/blog/2017/01/04/reading-in-2016/">2016</a>, and <a href="/blog/2018/03/03/reading-in-2017/">2017</a>.</p><p>I&apos;ve continued to keep track of my reading using <a href="http://goodreads.com">Goodreads</a>. My <a href="https://www.goodreads.com/user/show/3431614-jake-mccrary">profile</a> continues to have the full list of the books I&apos;ve read since 2010. <a href="https://www.goodreads.com/review/list/3431614-jake-mccrary?read_at=2018">Here</a> is my entire 2018 record.</p><p>I slacked off a bit when writing reviews for all of my read books in Goodreads. I often didn&apos;t write a review until some time had passed after completing the book and, as a result, I think I did a worse job reviewing books. Some books don&apos;t even have a written review. I&apos;m not a fan of this and will push myself some in 2019 to do a better job.</p><h2 id="2018-goal">2018 Goal</h2><blockquote><p>There are a few more books on writing that I&apos;ve wanted to read for a while. I&apos;m planning on reading at least one of them this year. I&apos;m also want to read more Octavia Butler. - Me (in the previous reading post)</p></blockquote><p>That was my goal for 2018. It breaks down into two goals:</p><ol><li>Read at least one book on writing.</li><li>Read more Octavia Butler.</li></ol><p>I succeeded on the Octavia Butler goal and completely failed with the other.</p><h2 id="2018-numbers">2018 Numbers</h2><p>I read 43 books for a total of 16,213 pages. This is a bit less than last year but still a fair amount.</p><h2 id="highlights">Highlights</h2><p>Below is a list of my five star books from 2018. I&apos;ve linked to my Goodreads reviews when I&apos;ve written one.  Unfortunately, this year I didn&apos;t do a great job of always writing a review so some of them are missing or very short.</p><p>I generally highlight a lot of passages while reading and then rarely go back to look at them. I&apos;ve included links to my highlights. Are they worthwhile without the context of the book? I have no idea. I&apos;ve reread them and got something out of them but many are also so far removed from my memory that they are almost useless.</p><ul><li>Being Mortal: Medicine and What Matters in the End by Atul Gawande (<a href="https://www.goodreads.com/review/show/1380346555">my review</a>, <a href="https://www.goodreads.com/notes/23290548-being-mortal/3431614-jake-mccrary">my highlights</a>)</li><li>Sapiens: A Brief History of Humankind by Yuval Noah Harari (<a href="https://www.goodreads.com/review/show/1679140092">my review</a>, <a href="https://www.goodreads.com/notes/20873740-sapiens/3431614-jake-mccrary">my highlights</a>)</li><li>Essentialism: The Disciplined Pursuit of Less by Greg McKeown (<a href="https://www.goodreads.com/review/show/1178908716">my review</a>, <a href="https://www.goodreads.com/notes/19776547-essentialism/3431614-jake-mccrary">my highlights</a>)</li><li>Crucial Conversations Tools for Talking When Stakes Are High by Kerry Patterson,  Joseph Grenny, Ron McMillan, Al Switzler (<a href="https://www.goodreads.com/notes/13041257-crucial-conversations-tools-for-talking-when-stakes-are-high/3431614-jake-mccrary">my highlights</a>)</li><li>Rediscovering JavaScript: Master ES6, ES7, and ES8 by Venkat Subramaniam (<a href="https://www.goodreads.com/review/show/2450809712">my review</a>)</li><li>CivilWarLand in Bad Decline by George Saunders (<a href="https://www.goodreads.com/review/show/104227855">my review</a>)</li><li>The Obelisk Gate (The Broken Earth #2) by N.K. Jemisin</li><li>The Stone Sky (The Broken Earth, #3) by N.K. Jemisin (<a href="https://www.goodreads.com/review/show/2467372092">my review</a>)</li><li>The Hate U Give by Angie Thomas (<a href="https://www.goodreads.com/review/show/2221117127">my review</a>)</li><li>Six of Crows (Six of Crows, #1) by Leigh Bardugo (<a href="https://www.goodreads.com/review/show/2309941150">my review</a>)</li><li>Crooked Kingdom (Six of Crows #2) by Leigh Bardugo</li></ul><h4 id="being-mortal:-medicine-and-what-matters-in-the-end-by-atul-gawande">Being Mortal: Medicine and What Matters in the End by Atul Gawande</h4><p>This book deals with the end of our lives. It was great. There is a lot of good insight here. Like a lot of the non-fiction books I read, I really should go back and take notes on what I highlighted.</p><p>We&apos;re all going to deal with death and sickness. This book can help.</p><h4 id="sapiens:-a-brief-history-of-humankind-by-yuval-noah-harari">Sapiens: A Brief History of Humankind by Yuval Noah Harari</h4><p>My entire Goodreads review is two sentences.</p><blockquote><p>This is an incredible book. You should read this. - Me</p></blockquote><p>I still agree with this. My friend, Steve Deobald, <a href="https://www.goodreads.com/review/show/2347504001?book_show_action=false">described</a> this book as &quot;the most lucid book he&apos;s ever read.&quot; There is a reason this book has a 4.45 rating on Goodreads. Go read the blurb about it there and then buy and read this book<a href="#fn-1" id="fnref1"><sup>1</sup></a>.</p><h4 id="essentialism:-the-disciplined-pursuit-of-less-by-greg-mckeown">Essentialism: The Disciplined Pursuit of Less by Greg McKeown</h4><blockquote><p>If you don’t prioritize your life, someone else will. - Greg McKeown</p></blockquote><p>A really great book encouraging you to focus on what matters and, as a result, make a bigger impact and be happier. It is better to make a mile of progress on one thing instead of making inches of progress in a bunch.</p><p>Tim Ferris recently <a href="https://tim.blog/2019/01/09/greg-mckeown-essentialism/">published a podcast</a> with Greg McKeown which I&apos;d also recommend. I&apos;ve enjoyed listening to the podcast after a bit of time away from the book. This has helped reinforce ideas from the book. If you&apos;re hesitant to read the book, take the time to listen and pay attention to this long podcast.</p><p>I highlighted over 100 sections of this book. I plan on revisiting these notes and this book periodically.</p><h4 id="crucial-conversations-tools-for-talking-when-stakes-are-high-by-kerry-patterson,-joseph-grenny,-ron-mcmillan,-al-switzler">Crucial Conversations Tools for Talking When Stakes Are High by Kerry Patterson, Joseph Grenny, Ron McMillan, Al Switzler</h4><p>A crucial conversation is one where the stakes are high, opinions vary, and emotions run strong. This book provides guidance for handling those conversations better.</p><p>I enjoyed this book and thought I picked up some useful tips from it. I think this is another where doing follow up work would help solidify some of the concepts.</p><h4 id="rediscovering-javascript:-master-es6,-es7,-and-es8-by-venkat-subramaniam">Rediscovering JavaScript: Master ES6, ES7, and ES8 by Venkat Subramaniam</h4><p>Do you write JavaScript?</p><p>Did you write JavaScript in the past but then move on to languages like ClojureScript and miss all the changes that happened to JavaScript?</p><p>Both of those sentences apply to me. This book has been great at catching up on modern JavaScript. I find myself referencing it while writing JavaScript and it has been very helpful. It is to the point and I find myself referencing it periodically.</p><h4 id="civilwarland-in-bad-decline-by-george-saunders">CivilWarLand in Bad Decline by George Saunders</h4><p>I really like this book. It is a wonderful collection of short stories. This was my second time reading it and I still enjoyed it.</p><h4 id="the-obelisk-gate-(the-broken-earth-#2)-and-the-stone-sky-(the-broken-earth,-#3)-by-n.k.-jemisin">The Obelisk Gate (The Broken Earth #2) and The Stone Sky (The Broken Earth, #3) by N.K. Jemisin</h4><p>N.K. Jemisin has won a Hugo three years in a row. Those three years line up with each release of a book in The Broken Earth series. They are really good.</p><p>This series is great. The world is interesting and the story compelling. I highly recommend it.</p><h4 id="the-hate-u-give-by-angie-thomas">The Hate U Give by Angie Thomas</h4><p>Reading lets you experience life from a different perspective. This book is good. It was quickly made into a movie which is also pretty good.</p><p>I read this as part of my book club and it was universally enjoyed.</p><h4 id="six-of-crows-(six-of-crows,-#1)-and-crooked-kingdom-(six-of-crows-#2)-by-leigh-bardugo">Six of Crows (Six of Crows, #1) and Crooked Kingdom (Six of Crows #2) by Leigh Bardugo</h4><p>I just really enjoyed this series. I enjoyed the fantasy world it was set in and have read most of Leigh Bardugo&apos;s other books that are set in this same world.</p><p>The series is a young adult series. It isn&apos;t complex. The reading isn&apos;t difficult. It isn&apos;t going to change your life and you&apos;re not going to be blown away by the writing. It almost feels weird to include this series in the same list as CivilWarLand and The Broken Earth series. Even still, I found myself sucked into the story and didn&apos;t mind spending the short amount of time it took to read the books.</p><h3 id="non-five-star-highlights">Non-Five Star highlights</h3><h4 id="life-3.0:-being-human-in-the-age-of-artificial-intelligence-by-max-tegmark">Life 3.0: Being Human in the Age of Artificial Intelligence by Max Tegmark</h4><p><a href="https://www.goodreads.com/notes/35881748-life-3-0/3431614-jake-mccrary">my highlights</a></p><p>I thought this was a really interesting book.</p><h4 id="when:-the-scientific-secrets-of-perfect-timing-by-daniel-h.-pink">When: The Scientific Secrets of Perfect Timing by Daniel H. Pink</h4><p><a href="https://www.goodreads.com/review/show/2278408888">my review</a>, <a href="https://www.goodreads.com/notes/35786699-when/3431614-jake-mccrary">my highlights</a></p><p>I really enjoyed this. Pink references other works to build a narrative about how timing matters. When should you take a nap? Is it better to go do the doctors in the morning or afternoon? How do are cognitive abilities generally change throughout the day? How should you try to end your vacations?</p><p>I did take some notes on the book while reading it and I have referenced them. It was a good book. I should have taken more notes.</p><h4 id="bloodchild-and-other-stories-by-octavia-e.-butler">Bloodchild and Other Stories by Octavia E. Butler</h4><p><a href="https://www.goodreads.com/review/show/2293497979">my review</a>, <a href="https://www.goodreads.com/notes/40274755-bloodchild-and-other-stories/3431614-jake-mccrary">my highlights</a></p><p>This is a great collection of short stories and non-fiction articles written by Octavia Butler. I really love her writing. I&apos;ve read a few of her works and still enjoy <a href="https://www.goodreads.com/book/show/60926.Lilith_s_Brood">Lilith&apos;s Brood</a> the most.</p><p>Below is a quote from her about science fiction that really resonated with me. It really hits home on one of the reasons I love reading science fiction.</p><blockquote><p>But still I’m asked, what good is science fiction to Black people? What good is any form of literature to Black people? What good is science fiction’s thinking about the present, the future, and the past? What good is its tendency to warn or to consider alternative ways of thinking and doing? What good is its examination of the possible effects of science and technology, or social organization and political direction? At its best, science fiction stimulates imagination and creativity. It gets reader and writer off the beaten track, off the narrow, narrow footpath of what “everyone” is saying, doing, thinking—whoever “everyone” happens to be this year. And what good is all this to Black people? - Octavia Butler</p></blockquote><h4 id="eeeee-eee-eeee-by-tao-lin">Eeeee Eee Eeee by Tao Lin</h4><p><a href="https://www.goodreads.com/review/show/1788752328">my review</a></p><p>This book is real bizarre. For some reason I liked it.</p><h4 id="factfulness:-ten-reasons-we&apos;re-wrong-about-the-world--and-why-things-are-better-than-you-think-by-hans-rosling">Factfulness: Ten Reasons We&apos;re Wrong About the World--and Why Things Are Better Than You Think by Hans Rosling</h4><p><a href="https://www.goodreads.com/review/show/2402596250">my review</a>, <a href="https://www.goodreads.com/notes/36185940-factfulness/3431614-jake-mccrary">my highlights</a></p><p>This book is great. It is very approachable and dispels some wrong common knowledge.</p><h2 id="stats">Stats</h2><p>I struggled generating stats this year. I kept having data issues with Goodreads. There is data that is in Goodreads that is failing to export both through their export feature and API. I&apos;m somewhat wondering what I would need to do to track reading in a different way.</p><p>Below is the reading stats per month. The numbers are based on when the book is completed. December is partially so low because the other books all carried over to January.</p><p><img alt="Book and pages count by month" src="/images/2018-books-and-pages.svg" title="Book and pages count by month" /></p><p>Electronic books continue to make up the majority of the books I&apos;m reading.</p><pre><code>|           | 2018 | 2017 | 2016 | 2015 |
|-----------+------+------+------+------|
| ebook     |   37 |   37 |   56 |   47 |
| hardcover |    1 |    7 |    0 |    1 |
| paperback |    5 |    5 |    3 |    3 |
</code></pre><p>There are two physical books not included in my read books that I started and still need to finish. They are a both books focused on fitness (climbing injuries and proper movement) and aren&apos;t books I&apos;m actively reading.</p><p>Nearly a third of my reading was non-fiction. For the second year in a row, only two of those were software related.</p><pre><code>|             | Number of books |
|-------------+-----------------|
| fiction     |              29 |
| non-fiction |              14 |
</code></pre><h2 id="2019-goals">2019 Goals</h2><p>I have a stack of software and process books and I&apos;d like to read through at least some of them (others are more reference books). I&apos;m also going to bring over the 2018 goal of reading at least one book on writing.</p><p>In a more general sense, I&apos;m hoping to put some practices together that help me gain more from the books I&apos;m reading. I&apos;m still thinking through what that means.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>In the beginning of 2019 I also read Harari&apos;s &quot;21 lessons for the 21st Century.&quot; Spoiler alert: this book will end up in my 2019 reading summary post.<a href="#fnref1">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2019/01/06/notifications-with-tmux-and-lein-test-refresh/index.html</id>
    <link href="https://jakemccrary.com/blog/2019/01/06/notifications-with-tmux-and-lein-test-refresh/index.html"/>
    <title><![CDATA[Notifications with tmux and lein-test-refresh]]></title>
    <updated>2019-01-06T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>I&apos;ve been using Emacs in a remote <a href="https://github.com/tmux/tmux">tmux</a> session lately and I&apos;ve been missing <a href="https://github.com/jakemcc/test-refresh#notifications">lein-test-refresh</a> notifications when my Clojure tests pass or fail. Luckily, it only took me a little bit of searching to figure out a solution for when I&apos;m working inside of tmux.</p><p>Below is a GIF of the notifications I get as my tests run and pass or fail.</p><p><img alt="tmux and test-refresh notifications" src="/images/tmux-test-refresh.gif" title="tmux and test-refresh notifications" /></p><p>With the above notifications, I can keep my focus on my code and only switch to the tmux window with <code>lein test-refresh</code> running when a test fails.</p><p>This was pretty easy to setup. You can trigger a message in tmux by running <code>tmux display-message &lt;MESSAGE_HERE&gt;</code>. To configure <a href="https://github.com/jakemcc/test-refresh#notifications">lein-test-refresh</a> to send notifications to tmux simply include the following in your <code>:test-refresh</code> section of your <code>project.clj</code> or <code>profiles.clj</code>.</p><pre><code class="language-clojure">:test-refresh {:notify-command [&quot;tmux&quot; &quot;display-message&quot;]}
</code></pre><p>I hope you enjoy this. Its has made using a remote terminal with tmux and <a href="https://github.com/jakemcc/test-refresh">lein-test-refresh</a> more enjoyable.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2018/12/27/a-more-helpful-makefile/index.html</id>
    <link href="https://jakemccrary.com/blog/2018/12/27/a-more-helpful-makefile/index.html"/>
    <title><![CDATA[A more helpful makefile]]></title>
    <updated>2018-12-27T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>In an <a href="/blog/2016/11/30/unify-your-project-interfaces/">older article</a> of mine I extolled the virtues of having unified interfaces for interacting with your projects. I recently started working at Coinbase and the group I&apos;m working with is mostly using makefiles as that common interface. We still have some more work to do to unify the makefile targets of the various projects but I&apos;ve picked up one tip that makes switching between projects easier.</p><p>That tip is to have the default target of your makefile be one that prints out a helpful message. This looks like the following.</p><pre><code>.PHONY: help
help:
	@grep -E &apos;^[0-9a-zA-Z_-]+:.*?## .*$$&apos; $(MAKEFILE_LIST) | \
	 sort | \
	 awk &apos;BEGIN {FS = &quot;:.*?## &quot;}; {printf &quot;\033[36m%-30s\033[0m %s\n&quot;, $$1, $$2}&apos;
</code></pre><p>There is a lot going on there but it basically looks through your makefile targets and finds the ones that have a comment starting with <code>##</code> after the target dependencies. Those targets are printed to the console along with the comment.</p><p>As an example, the makefile for my website looks similar to the below file.</p><pre><code>.PHONY: help
help:
	@grep -E &apos;^[0-9a-zA-Z_-]+:.*?## .*$$&apos; $(MAKEFILE_LIST) | \
	 sort | \
	 awk &apos;BEGIN {FS = &quot;:.*?## &quot;}; {printf &quot;\033[36m%-30s\033[0m %s\n&quot;, $$1, $$2}&apos;

.PHONY: watch
watch: ## Watch for changes and serve preview of site with drafts
	bundle exec rake clean
	bundle exec rake preview

.PHONY: develop
develop: ## Serve a preview of the site without drafts and refresh changes
	bundle exec rake clean
	bundle exec rake develop

.PHONY: new_adventure
new_adventure: ## Start a new adventure post
	bundle exec rake new_adventure

.PHONY: new_post
new_post: ## Start a new post
	bundle exec rake new_post 

.PHONY: deploy
deploy: ## deploy
	./deploy.sh
</code></pre><p>When this file, when I run <code>make</code> in this websites source, I get the following output.</p><pre><code>0 [last: 0s] 21:11:50 ~/src/jakemcc/blog (master)
$ make
deploy                         deploy
develop                        Serve a preview of the site without drafts and refresh changes
new_adventure                  Start a new adventure post
new_post                       Start a new post
watch                          Watch for changes and serve preview of site with drafts
</code></pre><p>This is super useful when you&apos;re starting doing work in a new project. With this feature you can get a quick list of useful targets and a description. It allows you to quickly see what can be done in a project.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2018/07/16/built-in-test-narrowing-with-lein-test-refresh/index.html</id>
    <link href="https://jakemccrary.com/blog/2018/07/16/built-in-test-narrowing-with-lein-test-refresh/index.html"/>
    <title><![CDATA[Built-in test narrowing with lein-test-refresh]]></title>
    <updated>2018-07-16T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>If you follow my work you probably know that I value fast feedback cycles. Most of the open-source I maintain was developed to enable faster feedback cycles. This is why <a href="https://github.com/jakemcc/test-refresh/">lein-test-refresh</a> and <a href="https://github.com/clojure-expectations/lein-autoexpect">lein-autoexpect</a> were originally created.</p><p>Leiningen supports <a href="https://github.com/technomancy/leiningen/blob/master/doc/TUTORIAL.md#tests">test selectors</a> and lein-test-refresh <a href="https://github.com/jakemcc/test-refresh/blob/master/CHANGES.md#040">does as well</a>. This lets you start-up a testing session and only run tests or namespaces with certain metadata on them. This is a super useful feature as it lets you narrow your testing scope to one (or a handful) of tests while working on solving a specific problem.</p><p>lein-test-refresh now has built-in functionality that allows you to focus your test scope without restarting the Leiningen test process. If lein-test-refresh sees a <code>deftest</code> or <code>ns</code> form marked with <code>:test-refresh/focus true</code> in its metadata, then it will only run tests marked with <code>:test-refresh/focus</code>.</p><p>Below is an example of what this looks like.</p><pre><code class="language-clojure">(deftest ^:test-refresh/focus test-addition
  (is (= 2 (+ 1 1))))
</code></pre><p>This functionality has only been available for a short period of time and I&apos;ve already found it useful. I think you will too. Enjoy.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2018/06/15/tracking-changes-to-a-reagent-atom/index.html</id>
    <link href="https://jakemccrary.com/blog/2018/06/15/tracking-changes-to-a-reagent-atom/index.html"/>
    <title><![CDATA[Tracking changes to a Reagent atom]]></title>
    <updated>2018-06-15T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>I was recently having some difficulty debugging a problem in a ClojureScript single page application. The SPA was implemented using <a href="https://reagent-project.github.io/">reagent</a><a href="#fn-1" id="fnref1"><sup>1</sup></a>.</p><p>This interface stores most of its state in a global <code>reagent.core/atom</code> called <code>db</code>. To debug the problem, I thought it would be useful to track how the global state changed as I interacted with the interface. How do we do that?</p><p>For the rest of this article, pretend that <code>(require &apos;[reagent.core :as reagent])</code> has been executed.</p><p>First, let&apos;s define <code>db-history</code> in the same namespace as the global <code>reagent/atom</code>, <code>db</code>. This is where we&apos;ll collect the changes to <code>db</code>.</p><pre><code class="language-clojure">(ns ui.data
  (:require [reagent.core :as reagent]))

(defonce db (reagent/atom {:app/current-page :offer-list}))

(defonce db-history (atom []))
</code></pre><p>Next, let&apos;s write a function called <code>aggregate-state</code>. This function grabs the current value in <code>db</code> and <code>conj</code>s it onto <code>db-history</code>. It also limits the history to the most recent 101 states.</p><pre><code class="language-clojure">(defn aggregate-state []
  (let [d @db]
    (swap! db-history (fn [hist]
                        (-&gt; (take 100 hist)
                            vec
                            (conj d))))))
</code></pre><p>Now we need to invoke <code>aggregate-state</code> whenever <code>db</code> changes. We can do this using <code>reagent/track</code>. <code>reagent/track</code> takes a function and optional arguments and invokes that function whenever a <code>reagent/atom</code> that function depends on changes.</p><p><code>reagent/track!</code> is similar except it immediately invokes the function instead of waiting for the first change. We can use it to cause <code>aggregate-state</code> to get called whenever <code>db</code> changes.</p><pre><code class="language-clojure">(defonce db-history-logger (reagent/track! aggregate-state))
</code></pre><p>Now history of the global state is being tracked. But we need a way to access it. Below is what I ended up writing. When you call <code>ui.data.history()</code> in Chrome&apos;s JavaScript console, it returns an object you can click on to explore. If you pass in strings as arguments to <code>history</code> then it only selects some of the data from the global <code>db</code> and history.</p><pre><code class="language-clojure">(defn ^:export history [&amp; args]
  (let [d @db
        k (if (seq args)
            (map keyword args)
            (keys d))]
    (clj-&gt;js {:history (mapv (fn [x] (select-keys x k)) @db-history)
              :current (select-keys d k)})))
</code></pre><p>It only took about fifteen lines of code to gain a view of our application&apos;s state changes over time. This view helped me solve my problem. Hopefully it will help you too.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>This particular project is nearly four years old and has had many hands on it over the years. Working in it reminds me of how useful re-frame is on larger applications like this one.<a href="#fnref1">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2018/06/14/clojure-prevent-multiple-simultaneous-invocations/index.html</id>
    <link href="https://jakemccrary.com/blog/2018/06/14/clojure-prevent-multiple-simultaneous-invocations/index.html"/>
    <title><![CDATA[Preventing duplicate long-running invocations in Clojure]]></title>
    <updated>2018-06-14T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>A couple months ago I was looking into a problem and noticed that there was a situation where an expensive operation could be running simultaneously multiple times. This was wasteful.</p><p>This operation happened on a timer and could also be triggered by a power user through the UI. A power user could accidentally (or purposefully) mash on a UI button and cause the instance they&apos;re interacting with to grind to a halt<a href="#fn-1" id="fnref1"><sup>1</sup></a>.</p><p>It was pretty easy to prevent. All I needed to introduce was an <code>atom</code> and lean on <code>compare-and-set!</code>. <code>compare-and-set!</code> is a pretty neat function (and concept found in many languages). Here is the docstring:</p><blockquote><p>Atomically sets the value of atom to newval if and only if the current value of the atom is identical to oldval. Returns true if set happened, else false</p></blockquote><p>Basically, <code>compare-and-set!</code> changes the value of an atom only if it starts from a specified value and returns a boolean letting you know if it did.</p><p>To prevent an operation from running multiple times, introduce an atom and wrap calling the operation in a conditional using <code>compare-and-set!</code>.  After doing the work, be sure to <code>reset!</code> your atom back to the starting value.</p><p>Below is the code.</p><pre><code class="language-clojure">(defonce running? (atom false))

(defn- expensive-operation!&apos; []
  ;; do work
  )

(defn expensive-operation! []
  (when (compare-and-set! running? false true)
    (try
      (expensive-operation!&apos;)
      (finally
        (reset! running? false)))))
</code></pre><ol class="footnotes"><li class="footnote" id="fn-1"><p>OK, not really grind to a halt, but consume unnecessary resources.<a href="#fnref1">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2018/03/03/reading-in-2017/index.html</id>
    <link href="https://jakemccrary.com/blog/2018/03/03/reading-in-2017/index.html"/>
    <title><![CDATA[Reading in 2017]]></title>
    <updated>2018-03-03T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>I typically like to reflect on my previous years reading closer to the beginning of the next year. We are just entering March, so I&apos;ve missed doing that.</p><p>Here are links to my previous end-of-year reflections: <a href="/blog/2014/01/01/using-incanter-to-review-my-2013-reading/">2013</a>, <a href="/blog/2015/01/08/reading-in-2014/">2014</a>, <a href="/blog/2016/03/13/reading-in-2015/">2015</a>, <a href="/blog/2017/01/04/reading-in-2016/">2016</a>.</p><p>I&apos;ve continued to keep track of my reading using <a href="http://goodreads.com">Goodreads</a>. My <a href="https://www.goodreads.com/user/show/3431614-jake-mccrary">profile</a> continues to have the full list of the books I&apos;ve read since 2010. <a href="https://www.goodreads.com/review/list/3431614-jake-mccrary?read_at=2017">Here</a> is my entire 2017 record.</p><h2 id="2017-goal">2017 Goal</h2><p>My goal entering 2017 was to revisit some past favorites. I started this goal without setting a number, so I&apos;ll just have to trust how I feel about it.</p><p>In 2017, I reread Frank Herbert&apos;s Dune and John William&apos;s Stoner. I also read new-to-me books by the authors David Foster Wallace, Haruki Murakami, George Saunders, and Neal Stephenson. I&apos;ve also reread a George Saunders book in the first part of 2018.</p><p>I mostly achieved 2017&apos;s goal. If I had reread another book, I&apos;d consider it 100% completed, but I&apos;m going to count reading some favorite authors towards the goal.</p><h2 id="2017-numbers">2017 Numbers</h2><p>I read a total of 49 books for a total of 17,853 pages. I also read every issue of Amazon&apos;s <em>Day One</em> weekly periodical<a href="#fn-1" id="fnref1"><sup>1</sup></a>.</p><p>The number of five-star books I read this last year was low compared to previous years.</p><h2 id="recommendations">Recommendations</h2><p>I only gave out seven five-star ratings. Two of the seven were books I reread.The review links are to my review on Goodreads.</p><ul><li>Stories of Your Life and Others - Ted Chiang (<a href="https://www.goodreads.com/review/show/1868465450">my review</a>)</li><li>A Supposedly Fun Thing I&apos;ll Never Do Again: Essays and Arguments - David Foster Wallace (<a href="https://www.goodreads.com/review/show/1071157288">my review</a>)</li><li>Dune - Frank Herbert (<a href="https://www.goodreads.com/review/show/2200354838">my review</a>)</li><li>Lilith&apos;s Brood - Octavia Butler (<a href="https://www.goodreads.com/review/show/2136588256">my review</a>)</li><li>Stoner - John Williams (<a href="https://www.goodreads.com/review/show/1896951901">my review</a>)</li><li>The Sense of Style: The Thinking Person&apos;s Guide to Writing in the 21st Century - Steven Pinker (<a href="https://www.goodreads.com/review/show/1216210735">my review</a>)</li><li>Shoe Dog - Phil Knight (<a href="https://www.goodreads.com/review/show/1866261263">my review</a>)</li></ul><p>Below are more details on some of the above five-star books and some shoutouts for some non-five star books. Looking back over my books, I&apos;d recommend any four-star or higher book without hesitation but am not going to put them all here.</p><h4 id="lilith&apos;s-brood-by-octavia-butler"><em>Lilith&apos;s Brood</em> by Octavia Butler</h4><p><em>Lilith&apos;s Brood</em> was one of the last books I read in 2017. It is a three book series published as a single book. It is amazing. This series achieves precisely what I want in a great science fiction book. I highly recommend this book. Reading this book reminded me why I love reading.</p><p>A quote from a non-fiction essay by Octavia Butler describes why good science fiction is fantastic.</p><blockquote><p>But still I’m asked, what good is science fiction to Black people? What good is any form of literature to Black people? What good is science fiction’s thinking about the present, the future, and the past? What good is its tendency to warn or to consider alternative ways of thinking and doing? What good is its examination of the possible effects of science and technology, or social organization and political direction? At its best, science fiction stimulates imagination and creativity. It gets reader and writer off the beaten track, off the narrow, narrow footpath of what “everyone” is saying, doing, thinking—whoever “everyone” happens to be this year. And what good is all this to Black people? - <strong>Octavia Butler</strong></p></blockquote><h4 id="the-sense-of-style-by-steven-pinker"><em>The Sense of Style</em> by Steven Pinker</h4><p>Yes, I read a book on writing and think this is one of the top books I read last year. I initially read a Kindle edition from my local library and then immediately bought the hardcover so I can reference it while writing.</p><p>The writing is great. The book is humorous. I&apos;d highly recommend to anyone that writes. I should reread this.</p><h4 id="dune-by-frank-herbert"><em>Dune</em> by Frank Herbert</h4><p><em>Dune</em> is a classic for a reason. It was still great my second time through it. If you haven&apos;t read Dune, you are missing out.</p><p>If you read it on a Kindle, I have a <a href="http://gum.co/dune-dictionary">custom Kindle dictionary</a> that makes reading it more pleasurable.</p><h4 id="stoner-by-john-williams"><em>Stoner</em> by John Williams</h4><p>It is still unclear to me why I like this book so much, but I do. The writing is crisp. The story is depressing.</p><h4 id="stories-of-your-life-and-others-by-ted-chiang"><em>Stories of Your Life and Others</em> by Ted Chiang</h4><p>Over the years I&apos;ve started to enjoy reading short story collections. Every story in this collection was great. I devoured this book and then everything else I could find by Ted Chiang.</p><h4 id="capital-in-the-twenty-first-century-by-thomas-piketty"><em>Capital in the Twenty-First Century</em> by Thomas Piketty</h4><p>This is a massive book. It probably deserved five-stars. It presents a ton of information to the reader. It is boring. It also made me think about the role of taxes in society and changed my thoughts about them.</p><p>If you&apos;ve been putting this off, you can probably skip to the last section and still get a lot from this book.</p><p><a href="https://www.goodreads.com/review/show/926621222?book_show_action=true">Here</a> is a review that does a spot on job of describing the book and <a href="https://www.goodreads.com/review/show/2041297829">my own review</a>.</p><h4 id="bobiverse-series-by-dennis-taylor">Bobiverse Series by Dennis Taylor</h4><p><a href="https://www.goodreads.com/series/192752-bobiverse">This</a> is a fun light-hearted science fiction series. It still manages to explore some deep topics. Read the description and if it sounds interesting to you, pick it up.</p><h2 id="stats">Stats</h2><p>Similar to last year, April and September were times when I wasn&apos;t reading a ton.</p><p><img alt="Chart of reading per month" src="/images/reading-by-month-2017.jpg" title="Chart of reading per month" /></p><p>This year physical books made a comeback. I checked out more physical books from the library this year than in the past.</p><pre><code>|           | 2017 | 2016 | 2015 |
|-----------+------+------+------|
| ebook     |   37 |   56 |   47 |
| hardcover |    7 |    0 |    1 |
| paperback |    5 |    3 |    3 |
</code></pre><p>My average rating went down a bit.</p><pre><code>| Year | Avg Rating |
|------+------------|
| 2011 |       3.84 |
| 2012 |       3.66 |
| 2013 |       3.55 |
| 2014 |       3.49 |
| 2015 |       3.86 |
| 2016 |       3.85 |
| 2017 |       3.77 |
</code></pre><p>I read a lot of non-fiction books this year. Only two of them were directly related to software.</p><pre><code>|             | Number of books |
|-------------+-----------------|
| fiction     |              30 |
| non-fiction |              19 |

</code></pre><h2 id="2018-goals">2018 Goals</h2><p>There are a few more books on writing that I&apos;ve wanted to read for a while. I&apos;m planning on reading at least one of them this year. I&apos;m also want to read more Octavia Butler.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>Unfortunately, this periodical has ended after years of publishing once a week. I&apos;m bummed. I really enjoyed receiving a short story and poem once a week.<a href="#fnref1">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2018/02/20/creating-serverless-applications-with-clojurescript-and-firebase/index.html</id>
    <link href="https://jakemccrary.com/blog/2018/02/20/creating-serverless-applications-with-clojurescript-and-firebase/index.html"/>
    <title><![CDATA[Creating serverless applications with ClojureScript and Firebase]]></title>
    <updated>2018-02-20T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Earlier this year, I traveled to India and gave a presentation at <a href="http://inclojure.org">IN/Clojure</a>. I talked about building serverless ClojureScript applications that use <a href="https://firebase.com">Firebase</a> to persist and sync data between clients.</p><p>I was pretty pleased with how the talk went. The people who talked to me after seemed to enjoy the presentation and were inspired to try out some of the techniques and tools I mentioned.</p><p>Here is the talk. I hope you enjoy it. It was fun to give.</p><iframe width="560" height="315" src="https://www.youtube.com/embed/rMqo3lgxe7o" frameborder="0" allow="encrypted-media" allowfullscreen></iframe></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2018/02/18/using-clojure-macros-for-nicer-error-handling/index.html</id>
    <link href="https://jakemccrary.com/blog/2018/02/18/using-clojure-macros-for-nicer-error-handling/index.html"/>
    <title><![CDATA[Using Clojure macros for nicer error handling]]></title>
    <updated>2018-02-18T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>In July 2017, I found myself editing some Clojure code that looked approximately like this.</p><pre><code class="language-clojure">(defn validate-required-fields [params]
  (when-not (contains? params :source)
    &quot;Missing source field&quot;))

(defn validate-invariants [params]
  (when (&gt;= (:lower params) (:higher params))
    &quot;lower field must be smaller than higher&quot;))

;; route handler taken out of other routes
(GET &quot;/event-redirect/:event_type&quot; request []
  (let [params (:params request)]
    (if-let [field-error (validate-required-fields params)]
      {:status 400 :body field-error}
      (if-let [invariant-error (validate-invariants params)]
        {:status 400 :body invariant-error}
        (publish-and-redirect params)))))
</code></pre><p>This route handler validates its inputs, and if they fail validation, then it returns an error response. I found this pretty ugly. This small chunk of code has numerous <code>if</code> branches and quite a bit of nesting. All of this makes it hard to read and hurts understanding.</p><p>While adding a new feature to it, I remembered some code I wrote with <a href="https://github.com/snoe">Case</a> back in late 2015. Back then we were working on Lumanu and wrote a Clojure macro that we called <code>halt-on-error-&gt;&gt;</code>. This macro worked similarly to <code>-&gt;&gt;</code>, except it allowed any step in the processing pipeline to halt execution and trigger an error handler. We were working on a web crawler at the time, and this macro significantly improved the readability of our data processing pipeline. There was a lot of error handling code throughout the web crawler, and this macro helped keep it readable.</p><p>I realized that using a similar macro would make this code easier to follow. I recreated <code>halt-on-error-&gt;&gt;</code> to allow any form to cause it to return early. The above code could then be written like below.</p><pre><code class="language-clojure">(defn validate-required-fields [params]
  (if (contains? params :source)
    params
    (exec/halt {:status 400 :body &quot;Missing source field&quot;})))

(defn validate-invariants [params]
  (if (&lt; (:lower params) (:higher params))
    params
    (exec/halt {:status 400 :body &quot;lower field must be smaller than higher&quot;})))

(GET &quot;/event-redirect/:event_type&quot; request []
  (exec/halt-on-error-&gt;&gt; request
                         :params
                         validate-required-fields
                         validate-invariants
                         publish-and-redirect))
</code></pre><p>Once you understand <code>halt-on-error-&gt;&gt;</code>, this chunk of code is much easier to read.</p><p>Let&apos;s implement <code>halt-on-error-&gt;&gt;</code>.</p><h2 id="implementing-halt-on-error-&gt;&gt;">Implementing <code>halt-on-error-&gt;&gt;</code></h2><p>Here are some tests for that specify how <code>halt-on-error-&gt;&gt;</code> should work.</p><pre><code class="language-clojure">(ns halt.execution-test
  (:require  [halt.execution :as exec]
             [clojure.test :refer :all]))

(def produce-error (constantly (exec/halt {:x &quot;foobar&quot;})))

(defn success-fn
  &quot;Weird function that appends suffix to s&quot;
  [suffix s]
  (str s suffix))

(deftest single-step
  (is (= &quot;first&quot; (exec/halt-on-error-&gt;&gt; (success-fn &quot;first&quot; &quot;&quot;)))))

(deftest two-steps-with-no-error
  (is (= &quot;firstsecond&quot; (exec/halt-on-error-&gt;&gt; (success-fn &quot;first&quot; &quot;&quot;)
                                              (success-fn &quot;second&quot;)))))

(deftest error-as-first-step
  (is (= {:x &quot;foobar&quot;} (exec/halt-on-error-&gt;&gt; (produce-error))))
  (is (= {:x &quot;foobar&quot;} (exec/halt-on-error-&gt;&gt; (produce-error)
                                              (success-fn &quot;first&quot;)))))

(deftest error-after-first-step
  (is (= {:x &quot;foobar&quot;} (exec/halt-on-error-&gt;&gt; (success-fn &quot;first&quot; &quot;&quot;)
                                              (produce-error)
                                              (success-fn &quot;second&quot;)))))

(deftest works-with-anonymous-functions
  (is (= 1 (exec/halt-on-error-&gt;&gt; (success-fn &quot;first&quot; &quot;&quot;)
                                  ((fn [x] (exec/halt 1)))))))
</code></pre><p>Below is an implementation of <code>halt-on-error-&gt;&gt;</code>.</p><pre><code class="language-clojure">(ns halt.execution)

(defrecord Stopper [x])

(defn halt [data]
  (Stopper. data))

(defmacro halt-on-error-&gt;&gt; [form &amp; forms]
  (let [g (gensym)
        pstep (fn [step] `(if (instance? Stopper ~g) ~g (-&gt;&gt; ~g ~step)))]
    `(let [~g ~form
           ~@(interleave (repeat g) (map pstep forms))]
       (if (instance? Stopper ~g)
         (.x ~g)
         ~g))))
</code></pre><p>So what is this macro doing? First, it uses <code>gensym</code> to get a symbol with a unique name and stores this in <code>g</code>. It then defines a helper function called <code>pstep</code> for use in the code generation part of the macro.</p><p>This macro generates a <code>let</code> block that repeatedly executes a form and assigns the return value back to <code>g</code>. <code>g</code> is then checked to confirm execution should continue before it is threaded into the next form. If <code>g</code> is ever an instance of a <code>Stopper</code>, execution halts and the value wrapped in the <code>Stopper</code> is returned.</p><p>Looking at an expanded version of a macro can be easier to understand than a written explanation. Below is a macroexpanded version of one of the tests.</p><pre><code class="language-clojure">;; What is being expanded
(macroexpand-1 &apos;(exec/halt-on-error-&gt;&gt; (success-fn &quot;first&quot; &quot;&quot;)
                                       (produce-error)
                                       (success-fn &quot;second&quot;)))

;; The expansion
(let [G__15365 (success-fn &quot;first&quot; &quot;&quot;)
      G__15365 (if (instance? halt.execution.Stopper G__15365)
                 G__15365
                 (-&gt;&gt; G__15365 (produce-error)))
      G__15365 (if (instance? halt.execution.Stopper G__15365)
                 G__15365
                 (-&gt;&gt; G__15365 (success-fn &quot;second&quot;)))]
  (if (instance? halt.execution.Stopper G__15365)
    (.x G__15365)
    G__15365))
</code></pre><p>Looking at that expansion, you can see how we are using a <code>let</code> block to repeatedly assign to the same symbol and we check that return value before executing the next stop.</p><p>This isn&apos;t a new pattern. There are <a href="https://github.com/kumarshantanu/promenade">libraries</a> that implement similar ideas. At IN/Clojure 2018, Varun Sharma gave a <a href="https://www.slideshare.net/VarunSharma143/elegant-errorhandling-for-a-more-civilized-age">talk</a> about how this cleaned up their code. You can even get bogged down and throw around words like monad when talking about it.</p><p>I&apos;d encourage you to look at your code and see if you have areas where error handling code is detracting from the readability. This might be an area where this, or something similar to it, would help.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2017/11/26/parsing-multiple-date-formats/index.html</id>
    <link href="https://jakemccrary.com/blog/2017/11/26/parsing-multiple-date-formats/index.html"/>
    <title><![CDATA[Parsing multiple date formats with clj-time]]></title>
    <updated>2017-11-26T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>I recently needed to optimize the speed of some Clojure code. After investigating, I identified that a huge number of exceptions were being thrown and handling these was slowing down the process.</p><p>The code throwing the exceptions was parsing strings into Joda-Time DateTime objects using the <a href="https://github.com/clj-time/clj-time">clj-time</a> library.</p><p>The code was calling <a href="https://github.com/clj-time/clj-time/blob/cce58248937bc05452ebfc8b65134961227a554e/src/clj_time/coerce.clj#L33-L38">clj-time.coerce/from-string</a> which calls <a href="https://github.com/clj-time/clj-time/blob/cce58248937bc05452ebfc8b65134961227a554e/src/clj_time/format.clj#L156-L165">clj-time.format/parse</a>. <code>format/parse</code> iterates through up to approximately 50 formatters in an attempt to parse whatever string you pass it. If one of these formatters doesn’t parse the string, it throws an exception which <code>format/parse</code> catches and ignores before attempting the next formatter.</p><p>This was pretty wasteful. This was especially wasteful in the code I was working in since it only needed to handle two different date formats.</p><p>Luckily, Joda-Time has a way to build a formatter that handles multiple formats and clj-time provides access to it. Below is code that creates a formatter that handles two different formats.</p><pre><code class="language-clojure">(ns multiple-dates.core
  (:require [clj-time.core :as time]
            [clj-time.format :as time-format]))

(def multi-format
  (time-format/formatter time/utc
                         &quot;YYYY-MM-dd&quot;
                         &quot;YYYY-MM-dd&apos;T&apos;HH:mm:ss.SSSZ&quot;))

(defn parse [s]
  (time-format/parse multi-format s))
</code></pre><p>And below are some examples of using it in the repl.</p><pre><code class="language-clojure">multiple-dates.core&gt; (parse &quot;2017-09-04&quot;)
#object[org.joda.time.DateTime 0x5d5e4cd7 &quot;2017-09-04T00:00:00.000Z&quot;]

multiple-dates.core&gt; (parse &quot;2017-09-04T12:11:02.123Z&quot;)
#object[org.joda.time.DateTime 0x174f3a5c &quot;2017-09-04T12:11:02.123Z&quot;]

multiple-dates.core&gt; (parse &quot;2017-09-04-12:11:02.123Z&quot;)
IllegalArgumentException Invalid format: &quot;2017-09-04-12:11:02.123Z&quot; is malformed at &quot;-12:11:02.123Z&quot;  org.joda.time.format.DateTimeFormatter.parseDateTime (DateTimeFormatter.java:945)
</code></pre><p>Looking back at that code, it seems pretty straightforward. I’ll admit that it took me and my pair a while to figure out how to do this using <code>clj-time</code>. I ended up looking at Joda-Time&apos;s documentation and implemented this using Java interop before I cracked how to use <code>clj-time.format/formatter</code> to do the same thing.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2017/10/31/a-guide-to-distributed-work/index.html</id>
    <link href="https://jakemccrary.com/blog/2017/10/31/a-guide-to-distributed-work/index.html"/>
    <title><![CDATA[A guide to distributed work]]></title>
    <updated>2017-10-31T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><blockquote><p>See all of my remote/working-from-home <a href="/blog/categories/remote/">articles here</a>.</p></blockquote><p>Whether it is working as the one remote employee at a traditional company or being one of many at a distributed company, remote work is becoming an option for many of us.</p><p>The number of employees that work remotely is growing<a href="#fn-1" id="fnref1"><sup>1</sup></a>. Technology improvements, pervasive Internet, and mindset changes are some of the drivers for this change.</p><p>The remainder of this article highlights some observations from my last few years of working for distributed companies. I&apos;ve also interviewed and corresponded with many other remote and formerly remote workers. Their contributions, along with books and articles on remote work, have influenced my thinking about what it means to have a successful distributed company. I&apos;ve been working remotely since October 2013 in roles that have ranged from being a team lead to software developer to CTO. Each company I&apos;ve worked for was fully distributed. There were no offices.</p><p>The types of distributed companies I&apos;ve worked with have not been asynchronous. They have had core working hours centered around one of the middle time zones in the continental United States. You could work from anywhere, but you needed to be willing to work while others were working. I consider this synchronous remote work.</p><p>Much of the following also applies to individuals working remotely for a not-entirely-distributed company or team. Being the only remote individual in a non-remote team comes with its own set of challenges that I&apos;m not going to attempt to present. If you are part of a team where some members work remotely, my recommendation is that you should treat that team as a remote team. If you don&apos;t, the remote worker will have a harder time keeping up with the rest of the team.</p><h2 id="benefits">Benefits</h2><p>If you ask someone that has never worked remotely before for the benefits of working remotely, they would probably be able to guess at some of the most obvious benefits. The top two responses I&apos;ve received to this question are having no commute and more flexibility in your schedule. These two advantages are huge. There are other, less obvious advantages as well.</p><h3 id="no-commute">No commute</h3><p>This is the benefit that most workers, remote and non-remote, identify when asked about benefits of remote work. This benefit stands out because it is huge and obvious. In the United States, the average one-way commute is <a href="https://project.wnyc.org/commute-times-us/embed.html#5.00/42.000/-89.500">25 minutes long</a>. The average worker spends nearly one hour going to and from their work.</p><p>If you are going to work five days a week, then you&apos;re spending over four hours commuting.  That is half of a full workday riding a bicycle, car, train, or bus. In the best case scenario, you are using that time to read, listen to a podcast, or trying to think deeply about a problem. In reality, you&apos;re trying to do one of those activities, but you are continuously distracted by the world around you. You have to worry about avoiding accidents, driving safely, or another distracting concern.</p><p>Commuting has been shown to have negative effects on the commuters<a href="#fn-6" id="fnref6"><sup>6</sup></a>. Being able to work remotely lets you avoid those problems.</p><h3 id="flexibility-in-schedule">Flexibility in schedule</h3><p>This is another benefit that most people can immediately identify. Remote works gives you more power over your schedule. Even if you&apos;re part of a synchronous distributed team, you gain flexibility. All of a sudden your breaks become time you can use to enrich your non-work life.</p><p>Picking up or dropping off your children at daycare or school becomes easier. Taking your dog for a midday walk becomes possible. Since you aren&apos;t commuting, you have more time to make breakfast for you and your family. You can run errands or go to your favorite neighborhood lunch spot during the day. These errands and restaurant trips are quicker since you&apos;re effectively doing them at off hours as most of your neighbors are working at their office.</p><h3 id="more-time-with-family">More time with family</h3><p>If you&apos;re working at home, then it becomes easy to see your family more. You can say hi to your kids when they get home from school. You have more time in the evening to spend with your baby before bedtime.</p><h3 id="customize-your-workspace">Customize your workspace</h3><p>You get to choose where you work. For many, this will be in a home office. This is your space. You get to make it your own.</p><p>Do you like to work in a cave? Paint the walls a dark color, block out the windows and enjoy your cave-like experience. Do you prefer sunlight and plants? Work near a window and add houseplants to your space. Do you want an awesome sit-stand desk and chair? Buy <a href="/blog/2015/03/31/my-home-work-space/">them</a>.</p><p>One of my former colleagues likes to walk on a treadmill while standing or, if sitting, he enjoyed having <a href="https://www.amazon.com/DeskCycle-Exercise-Pedal-Exerciser-White/dp/B00B1VDNQA">pedals</a> under his desk. These are customizations he would have a hard time getting at most offices.</p><h3 id="eat-the-food-you-want-to-eat">Eat the food you want to eat</h3><p>Many of us have preferred diet (or a diet we&apos;re forced to follow). When you work from home, it is easier to eat food you know you should eat.</p><p>When you work from an office, you have a few food options. You can bring your food, go out to eat, or (if your employer offers it) eat food provided by your company. If you follow a restrictive diet, all of these options are more hassle than making your lunch at home every day.</p><p>Feel like eating food someone else has prepared? You can still do that while working from home.</p><h3 id="fewer-interruptions">Fewer interruptions</h3><p>As a remote worker, you can choose your working location. This lets you select a spot with fewer distractions. You can pick an ideal location that helps you achieve a state of [flow](<a href="https://en.wikipedia.org/wiki/Flow_(psychology)">https://en.wikipedia.org/wiki/Flow_(psychology)</a>).</p><p>Minimizing interruptions is one of the keys to accomplishing challenging tasks. After an interruption, it takes up to a half an hour to get back to your original task<a href="#fn-2" id="fnref2"><sup>2</sup></a>.</p><h3 id="off-hours-support">Off-hours support</h3><p>This is a benefit I have not seen mentioned many other places. Off-hours support becomes much easier if you are working remotely. The actions you would take for an urgent alert at 1 AM are the same actions you would take at 1 PM.</p><p>When you get that 1 AM page you don&apos;t have to struggle to remember how you check production while at home; this is an activity you do on a regular basis. You know what you need to do. You don&apos;t have to remember how to VPN into your company&apos;s network; you do that every day.</p><p>No one likes getting woken up by a support call. At least this way you get to use your normal tools for solving problems.</p><h3 id="recruiting">Recruiting</h3><p>Since you aren&apos;t limited to your locale, you can recruit from a much broader region. This means you can find the top talent for your company. This is huge.</p><p>Back in late 2013, it was quite challenging to find experienced Clojure programmers. Because <a href="https://outpace.com">Outpace</a> is a distributed company, we were able to hire experts from across the entire United States. We would not have been able to recruit nearly as well if we were limited to a single location.</p><h3 id="employee-retention">Employee retention</h3><p>If your company supports remote work, then you remove an entire reason for an employee leaving. Sometimes, a person needs to move.  Working for a company that supports remote work allows them to move and not leave the company.</p><h3 id="reduced-office-costs">Reduced office costs</h3><p>Having a distributed workforce can reduce office costs drastically. In a fully distributed company, it could reduce the cost to zero. Realistically, I&apos;d expect the company I work for to provide computer hardware so there are still some costs<a href="#fn-3" id="fnref3"><sup>3</sup></a>. Unless the company pays for Internet and phone, the recurring costs are minimal.</p><h2 id="downsides">Downsides</h2><p>While there are many benefits, there are also downsides to working remotely. When I talk to non-remote workers about working remotely, I typically hear &quot;I don&apos;t know how you do it, I&apos;d be so distracted.&quot; This statement touches on one of the downsides of remote work, but it isn&apos;t largest one. Below are some downsides that I and others have observed.</p><h3 id="loneliness-and-isolation">Loneliness and isolation</h3><p>Nearly everyone I&apos;ve talked to, including myself, puts this as the top downside.</p><p>Most of us are social creatures. You do not get the same type of social interaction with your coworkers when you are working remotely. Instead of bumping into a wide range of people in the office, you interact with a smaller group through video chats, phone calls, chat rooms, and email. Depending on what you are doing, you might not even get that interaction.</p><p>This is very unfamiliar to most of us. We&apos;re used to being in physical proximity to other humans. We&apos;re used to having small talk and grabbing coffee or lunch with other people.</p><p>You can combat these feelings by setting up communication with other employees at your company. Have some dedicated chat rooms for non-work discussions. Have a daily, short video meeting that is a status check within the team so that everyone gets to see another person&apos;s face at least once a day. If you work in a city that has other remote workers from your company then meet up occasionally for dinner, lunch, or happy hour.</p><p>If you are having troubles with loneliness and isolation, try to find an area where you can work surrounded by other people. Two options are co-working spaces and coffee shops. Alternatively, try to have social activities you regularly do with non-coworkers in your area. Having strong connections with non-coworkers can help combat loneliness.</p><p>If I stay inside for more than a couple days, I get grumpy. I didn&apos;t realize this when I worked in an office. Noticing this has benefited my rock climbing, as I&apos;ve made that my main non-work social activity. Even if I merely go bouldering by myself, being around other humans helps. If you&apos;re working remotely and feeling grumpy, try to find an activity you can regularly do and see if doing that helps.</p><h3 id="distractions">Distractions</h3><p>This is the downside that non-remote workers most often identify. People assume that television and other distractions in your home are irresistible and will cause you not to get work done. When you are working 100% remotely, you don&apos;t have the same distractions you have when you are only occasionally working remotely. You can&apos;t do laundry every day. You only have so much TV you can watch.</p><p>Personally, I don&apos;t have a problem with distractions when working at home. I know others that do. They mostly have when they first started working from home. When they first started working at home, they found themselves doing too much around the house. As a result, they worked late hours or felt like they weren&apos;t getting enough work done. Once you recognize the problem, it is possible to train yourself not to get distracted.</p><p>Roommates, kids, and family are another (sometimes welcome) distraction. You can combat interruptions from others by setting boundaries. Many of my coworkers have a rule that when their office door is closed, they are unavailable. I&apos;ll claim that coworkers interrupting you in an office are more distracting as the much rarer interruption from someone within your home.</p><p>Employees that work remotely are typically choosing to work remotely. Once they get used to working remotely, distractions stop being a problem. They know they need to produce quality work and will take steps to make sure they do.</p><h3 id="working-too-much">Working too much</h3><p>When you first start working from home, you suddenly find yourself living in the same space that you work. This lack of change in location and commute makes it easy to keep working. You get invested in a problem and all of a sudden it is past the time when you should have stopped working.</p><p>Even if you manage to stop working on time, it is easy to slip back into work mode. The computer setup I like to use in the evening is in the same location as my work setup. This makes it easy for me to take one more peek at our monitoring dashboards or check my work email.</p><p>You do not want to overwork and you do not want your teammates to overwork. In the short-term, overwork can be beneficial. Long-term it leads to burnout and poor outcomes.</p><h3 id="fewer-interactions">Fewer interactions</h3><p>This is a negative and positive. When you are working remotely, you have fewer random interactions with coworkers. You most likely interact with your team plus the same handful of people outside of your team regularly but you rarely interact with others.</p><p>In an office, there is a chance you&apos;ll run into people outside your usual circle of communication. You might eat lunch with a broader variety of people. You may bump into others while getting a coffee or a snack.</p><p>You can help increase interactions on a distributed team by having some chat rooms that encourage random discussions. Another option is to have a regular and optional meeting scheduled where people can give an informal presentation on something that interests them.</p><h2 id="tools">Tools</h2><p>You will need to select tools that work for distributed teams. Most computer or web-based tools can work in a distributed setting. Any physical tools (such as pen and paper) will not work.</p><p>A prime example of this is the <a href="http://www.caroli.org/the-card-wall/">classic card wall</a> for tracking work. A physical wall with actual cards will not work as soon as there is a single remote worker on a team. Instead of a physical wall, you&apos;ll need to using something like <a href="https://trello.com">Trello</a>.</p><p>It is less important to get stuck on a particular tool recommendation and more essential to pay attention to the categories of tools. Categories of tools tend to be more stable than a specific recommendation.</p><h3 id="text-chat">Text chat</h3><p>You&apos;ll want a chat application. <a href="https://slack.com">Slack</a> and <a href="https://www.stride.com/">Stride</a> are just two of the many available services.</p><h3 id="video-conference">Video conference</h3><p>Video conferencing is a technology that you should embrace. It is much better than talking to either an individual or a group on the phone. Being able to read body language makes communication far better. Personally, I&apos;ve used Google Hangouts and <a href="https://zoom.us">Zoom</a> for hundreds of hours each and prefer Zoom. <a href="https://appear.in">appear.in</a> is another option that doesn&apos;t require installing anything. There are many options in this space and more keep appearing. It is even built into Slack.</p><h3 id="phone-conferences">Phone conferences</h3><p>I&apos;d try to get rid of phone conferences in preference to video conferences. Video chat has many benefits over conference calls. I actually can&apos;t recommend any phone conferencing tools, but I will mention that Zoom supports people dialing into a video conference.</p><h3 id="screen-sharing">Screen sharing</h3><p>You&apos;ll want to have a way to show another person or group what is on your screen. It is even better if someone else can take control of your machine or use their cursor to point towards something on your screen.</p><p>Most of my experience with this is using the feature built-in to Zoom. Pretty much every video conference tool I&apos;ve used (appear.in, Google Hangouts, etc.) has screen sharing built-in.</p><h3 id="real-time-collaboration-on-a-document">Real-time collaboration on a document</h3><p>Being able to collectively edit a document with a group is pretty amazing. <a href="http://etherpad.org/">Etherpad</a> and <a href="https://www.google.com/intl/en/docs/about/">Google Docs</a> are two options. Most of my experience is with Google Docs.</p><p>When a document supports real-time collaboration, you can do amazing things. You can use it to capture ideas from a remote group. An extreme version of this can be viewed by opening <a href="http://idlewords.com/talks/fan_is_a_tool_using_animal.htm">this page</a> and searching for &quot;Google doc.&quot;</p><p>You can use a shared document to facilitate a remote meeting (this goes incredibly well once you get the practice of it). Having a document that everyone in a meeting can edit is so much better than a whiteboard that only one or two people can simultaneously use.</p><h3 id="whiteboards">Whiteboards</h3><p>Whiteboards are an example of a tool that is always brought up, even by remote workers, as something that distributed teams miss. There are alternatives.</p><p>Whiteboards are a very convenient tool when meeting in-person, but there are other ways of collaborating when working remotely. Shared documents and screen sharing go a long way towards enabling collaboration. Tools that work well for remote collaboration often have another benefit over whiteboards; they are easier to persist and reference later.</p><p>One whiteboard alternative is Zoom&apos;s built-in whiteboard. It works fairly well. Another is to use <a href="https://drawings.google.com">Google Drawings</a>. <a href="https://precursorapp.com/">Precursor</a> is a design focused collaborative tool that can also work.</p><p>Even after four years, I occasionally find myself missing a whiteboard or shared piece of paper. Drawing with a mouse isn&apos;t ideal. I know some developers that use an iPad or a Wacom tablet to help them quickly sketch diagrams for their team.</p><h2 id="communication">Communication</h2><p>Communication is inherently different on a distributed team. You cannot just walk across an office to interrupt someone and ask them a question. Communication happens mostly through text. You need to be skilled at written communication.</p><p>You lose context with written communication when compared to vocal or in-person communication. You no longer have body language or tone of voice to help you interpret what someone means. Lack of tone is huge. This is one reason that text communication is interpreted as more emotionally negative or neutral than intended<a href="#fn-4" id="fnref4"><sup>4</sup></a>. If you&apos;re reading text communication, try to read it with a positive tone.</p><p>It can also be useful to have rules around the expectations of different forms of communication. How quickly do you need to respond to an email? How quick should a response be to a chat room message?  When should you pick up the phone and call someone?</p><h3 id="chat-rooms">Chat rooms</h3><p>Chat room applications (IRC, Slack, Stride, Flowdock, etc.) are pretty great. They provide a medium of communication that has a lower barrier to entry than email. Chat tools have a place in the distributed teams tool chest.</p><p>The chat room becomes even more central to work once you start including useful bots in the room. These bots can perform many functions. You can look in the <a href="https://slack.com/apps">Slack App Directory</a> to see some of the bots that people are producing.</p><p>If you start adding bots and other automated messages to your chat application, you might want to think about setting up a separate channel. Some messages are not worthy of being interjected into your team&apos;s main chat. They can be distracting and hurt the flow of conversation. These messages tend to be ones that are informative, but not critical. For my teams, these messages include things like git commits and Trello card updates. It is great to see these in a single spot but annoying when they interrupt a conversation.</p><p>Chat rooms can also be a big time sink. They are a source of concentration interrupting notifications. The feeling of missing out on conversations can drive people to join a large number of channels. This piles on the potential for distraction.</p><p>Chat rooms also provide a feeling of immediacy that isn&apos;t actually there. You don&apos;t know if key people have seen your message or have had time to respond.</p><p>Despite having search functionality, I&apos;ve found it hard to find previous conversations in chat applications. If something important appears in chat, I&apos;d recommend extracting it from the chat application and recording it somewhere else.</p><p>I&apos;d also recommend turning off notifications for all but most definite &quot;someone is trying to reach me&quot; triggers. Encourage members of your chat to use entire channel notifications sparingly and only for messages that need everyone&apos;s attention. There are not many messages that immediately require everyone&apos;s attention.</p><p>It can be a challenge to follow chat conversations, especially if they span a larger unit of time. Don&apos;t be afraid to move a conversation to email or another medium that is better suited for longer and more complex discussions.</p><p>Many chat applications offer the ability to have private rooms or to send direct messages to a user. Don&apos;t be afraid of using these private channels, but if your communication can be public, it should be public. It can be challenging to ask a question and admit you don&apos;t know something but seeing that dialogue might help others. Similarly, having discussions about a feature, bug, or task can help spread knowledge.</p><h3 id="email">Email</h3><p>Despite all of the efforts to replace email; email is still useful. It is the most common form of communication between companies, it is pervasive, and it usually comes with good search capabilities.</p><p>A good email thread can keep a topic contained in a form that is possible to follow. Unlike a chat room, there (usually) aren&apos;t off-topic interjections from uninvolved parties.</p><h3 id="phone">Phone</h3><p>You shouldn&apos;t be afraid of calling someone. Just recognize that this is an interruption. Your company should have a directory of telephone numbers that is accessible to everyone.</p><p>One downside of any voice conversation is that it is not automatically persisted. It can be worth following up a phone call with an email summarizing the discussion and the next steps.</p><h3 id="picking-the-right-communication-medium">Picking the right communication medium</h3><p>When you are working on a distributed team, you can no longer walk over to someone&apos;s desk and interrupt them. This is great. Not every question deserves an immediate answer.</p><p>Agree with your team when to use different forms of communication. Set expectations with regards to response times and urgency for different mediums. Maybe direct chat messages are expected to be responded to in under 10 minutes. Perhaps emails are OK having a delay of a few hours. This is something your group will need to decide.</p><h2 id="practices">Practices</h2><p>These are some practices I&apos;ve seen work well with distributed teams. Many of them are slight variations on what you might have experienced on a co-located team.</p><h3 id="stand-ups">Stand-ups</h3><p>Most of the teams I&apos;ve been part of, whether distributed or co-located, have had a daily stand-up meeting. The intention of this meeting was to provide a short, scheduled time for communicating any roadblocks to progress, interesting information, status updates, and desire for help.</p><p>For a distributed stand-up, the team joins a video conference and we gather around a shared Google Doc that has prompts similar to the snippet below.</p><pre><code>Day: 2017-07-11

What&apos;s interesting?

Want help?

Meetings:
</code></pre><p>These prompts provide a starting point for team members to add additional text. Our stand-ups were at the beginning of the day, so frequently team members would add text to the document at the end of the prior day. Filling in the document at the end of the day instead of right before the stand-up was useful as memories and thoughts were often fresher without having to remember them the following morning.</p><p>After being filled in, the document would look like below.</p><pre><code>Day: 2017-07-11

What&apos;s interesting?
  - New deploy lowered response time by 20% [Jake]
  - Discovered bug in date-time library around checking if date is within interval [Sue]
  - Greg joined the team!
  - Adding blocker functionality going to take longer than expected [Donald]

Want help?
  - Having difficulties running batch process locally. [Tom]
  - I&apos;m having a hard time understanding propensity calculation [Mike]

Meetings:
  - API overview with client @ 2 PM Central [Jake/Jeremy]
</code></pre><p>We would gather around the Google Doc and everyone would take a couple of minutes to read silently. If anyone felt like something was worth talking about they would <strong>bold</strong> the text and then we&apos;d work from top to bottom and have a quick discussion. For our <code>Want help?</code> section we&apos;d solicit volunteers and move on. The <code>Meetings</code> section was primarily there to provide visibility as to when certain members might not be available. After we worked through the <code>Want help?</code> section we&apos;d pop over to Trello and review the work in progress and make sure everyone had an idea of what they would be doing that day.</p><p>The nice thing about doing a stand-up around a shared Google Doc is that you can put in richer media than just text. Screenshots of monitoring graphs were a regular addition to the <code>What&apos;s interesting?</code> section.</p><p>Every day a new section was added to the top of the Google Doc and the previous day was pushed lower on the page. Having this written history of stand-ups was useful as it allowed us to notice patterns through a persisted medium instead of relying on our memory. It also let someone who was on vacation come back and have an idea of what had happened while they were gone. Below is what the document would look like on the next day (comments removed to keep the example shorter).</p><pre><code>Day: 2017-07-12

What&apos;s interesting?
  - [...]

Want help?
  - [...]

Meetings:
  - [...]

-------
Day: 2017-07-11

What&apos;s interesting?
  - [...]

Want help?
  - [...]

Meetings:
  - [...]
</code></pre><p>Above is an example from one of the teams I led. Another team used the following prompts.</p><pre><code>1. Accomplished Yesterday

2. Requires Attention/Roadblocks

3. Scope Creep Alerts

4. Would like to Do Today
</code></pre><p>The important thing is to find something that works for your team. Different teams are going to prefer different formats.</p><p>Another interesting benefit of using a Google Doc to drive your stand-up is that it can be visible to other teams. You can even combine teams into a single document. Below is an example with two teams in a single document.</p><pre><code>**Everyone**
  - [...]

**Team Events**
  1. Accomplished Yesterday
     - [...]
  2. Requires Attention/Roadblocks
     - [...]
  3. Scope Creep Alerts
     - [...]
  4. Would like to do today
     - [...]

**Team Engine**
What&apos;s interesting?
  - [...]
Want help?
  - [...]
Meetings:
  - [...]
</code></pre><p>I&apos;ve seen this work successfully with five related teams in a single document. News and information that affects everyone is added in the <code>Everyone</code> section. Team specific information is put in the team sections. Each team still has their individual stand-up where they only look at their section. But since their section is part of the larger document, they get a taste of what is going on in the other related teams. This helps replace the random hallway chatter you get in a shared office and gives everyone a slightly broader picture.</p><p>This worked shockingly well. I&apos;ve had colleagues reach out, some years after leaving, to ask for the template we used for this multi-team stand-up.</p><h3 id="stand-downs">Stand-downs</h3><p>Stand-downs are a meeting that provides time to informally chat with a group. I&apos;ve seen them used as an optional end-of-day water cooler activity for a group to talk about whatever. These chats often happen, but are unscheduled, in an office.</p><p>These meetings should be an optional, short meeting scheduled near the end of the day. This gives team members a good excuse (socializing with their coworkers) to stop working (which helps with the problem of overwork). No one should feel pressure to be at these meetings.</p><p>The conversation may or may not be work-related. Maybe you discuss a language feature someone learned. You might talk about a book you started reading. It doesn&apos;t matter what is discussed; these meetings can help a team get closer and promote more social interaction.</p><p>I&apos;ve also worked with teams that play games, such as <a href="https://jackboxgames.com/project/jbpp3/">Jackbox Party Packs</a>, through video conferences.</p><h3 id="remote-pair-programming">Remote Pair Programming</h3><p><a href="http://c2.com/xp/PairProgramming.html">Pair programming</a> is a technique that people often employ when working in-person. It works even better when remote and helps solve some of the difficulties of working remotely.</p><p>Remote pair programming helps fight the feeling of loneliness and isolation that remote workers feel. Remote pairing forces intense interaction between two people. It also helps keep the two people focused on work. It is easy for a single person to get distracted by browsing Hacker News but much harder for both people to get sucked into it.</p><p>The ideal in-person pair programming setup is when you take a single computer and hook up two large monitors, two keyboards, and two mice and then mirror the monitors. This lets both programmers use their personal keyboard and stare straight ahead at their monitor.</p><p>Remote pair programming is an even better setup. One developer, the host, somehow shares their environment with the other developer. This can be done through screen sharing using Zoom, Slack, VNC, tmate, or some other way. The important part is that both developers can see the code and, if necessary, someway of viewing a UI component. They should both be able to edit the code.</p><p>Like the ideal local pair programming environment, each developer uses their personal keyboard, mouse, and monitor. Unlike the local pair programming environment, they each also have their own computer. This lets one developer look up documentation on their computer while the host developer continues to write code. It also allows the non-host developer to shield the host from distractions like answering questions in the chat room or responding to emails.</p><p>When remote programming it is easier for the non-host developer to stop paying attention. It is easier to be rude and not pay attention to your pair when you are not sitting next to them. If you notice your pair has zoned out, nicely call them out on it or ask them a question to get them to re-engage.</p><h3 id="one-on-ones">One-on-ones</h3><p>One-on-ones are a useful practice in both a co-located and distributed team. For those who aren&apos;t familiar with one-on-ones, they are meetings between you and your manager (flip that if you are the manager). They should be scheduled regularly and be a time where you discuss higher-level topics than the daily work. They are extremely useful for helping you develop professionally and helping a team run smoothly. If you currently have one-on-ones and you aren&apos;t finding them useful, I&apos;d recommend reading some articles with tips for making them useful. Here are a <a href="https://www.themuse.com/advice/how-to-have-oneonones-that-actually-matter">couple</a> <a href="https://hbr.org/2016/08/how-to-make-your-one-on-ones-with-employees-more-productive">articles</a> that give some pretty good advice. As both a team lead and a team member I&apos;ve found one-on-ones extremely useful. I thought they were even more useful when on a distributed team.</p><p>With a distributed team you lose out on a lot of the body language you can pick up on when in person. It is harder to tell when someone is frustrated or when a pair is not working well together. Burnout is harder to notice. One-on-ones provide a time for that information to come out.</p><h3 id="meet-in-person">Meet in person</h3><p>You should have your distributed team or company meet in person. This shouldn&apos;t happen too regularly; I think this ideally happens two to four times a year. Even if you see someone every day on video, there is still some magic that happens when you meet in person<a href="#fn-5" id="fnref5"><sup>5</sup></a>.</p><p>You can use this time to do the typical day-to-day work, but I think it is more productive to try other activities. The most successful in-person meetups I&apos;ve been part of consisted mostly of group discussions. This can take the form of a mini-conference or <a href="http://openspaceworld.org/wp2/what-is/">Open Space</a>. Another option is to brainstorm some potentially wild ideas and try implementing them to see how far you can get.</p><p>Use this time to have some meals and drinks with your coworkers. Play some board games and talk about things other than work. Sing some karaoke. Get to know your coworkers as more than someone inside your computer. Doing so can help with communication and understanding personalities.</p><h2 id="end">End</h2><p>It is an exciting time to be a remote worker. New tools are emerging that try to make remote work easier. New techniques are being discovered. Old techniques are being adapted.</p><p>I hope you&apos;ve found this article useful. If you are a remote worker, maybe you&apos;ve picked up some ideas to bring into your remote work. If you work in an office, perhaps you&apos;ve found some useful arguments for moving towards remote work.</p><p>There is much more I could write about remote work and distributed teams. Some of these sections deserve their own posts and extended examples. You can view the <a href="/blog/categories/remote/">remote category</a> of my site to view other articles I&apos;ve already written.</p><p>If you&apos;ve enjoyed this article, consider sharing (<a href="https://twitter.com/intent/tweet?text=A%20guide%20to%20distributed%20work%20https%3A%2F%2Fjakemccrary.com%2Fblog%2F2017%2F10%2F31%2Fa-guide-to-distributed-work%2F&amp;via=jakemcc">tweeting</a>) it to your followers.</p><h2 id="acknowledgments">Acknowledgments</h2><p>This article came to life from the notes and research I did prior to speaking at the <a href="http://lanyrd.com/2016/aitworkshop/">2016 AIT Workshop</a>. Some of those notes came from correspondence with <a href="https://timothypratley.blogspot.com/">Timothy Pratley</a>, <a href="http://www.rustybentley.com/">Rusty Bentley</a>, <a href="http://gigasquidsoftware.com/">Carin Meier</a>, <a href="https://twitter.com/devn">Devin Walters</a>, <a href="https://twitter.com/kidpollo">Paco Viramontes</a>, <a href="http://www.xpteam.com/">Jeff Bay</a>, and <a href="http://www.grayduckllc.com/">Michael Halvorson</a>. Discussions at the conference, with the above individuals, and working remotely at <a href="https://outpace.com">Outpace</a> and <a href="https://lumanu.com">Lumanu</a> really helped solidify my thoughts.</p><h2 id="other-references">Other references</h2><ul><li><a href="https://www.amazon.com/Remote-Office-Required-Jason-Fried/dp/0804137501">Remote - Jason Fried and David Heinemeier Hansson</a></li><li><a href="https://zapier.com/learn/the-ultimate-guide-to-remote-working/">The Ultimate Guide to Remote Work - Zapier</a></li><li><a href="http://paul.stadig.name/2013/07/remote-distributed.html">Remote != Distributed - Paul Stadig</a></li><li><a href="http://bob.mcwhirter.org/blog/2010/09/13/remote-worker-distributed-team/">Remote worker vs distributed team - Bob Mcwhirter</a></li><li><a href="http://martinfowler.com/articles/remote-or-co-located.html">Remote or co-located - Martin Fowler</a></li></ul><ol class="footnotes"><li class="footnote" id="fn-1"><p><a href="http://globalworkplaceanalytics.com/telecommuting-statistics">http://globalworkplaceanalytics.com/telecommuting-statistics</a><a href="#fnref1">↩</a></p></li><li class="footnote" id="fn-6"><p>Various articles: <a href="https://www.psychologytoday.com/blog/urban-survival/201501/commuting-the-stress-doesnt-pay">one</a>, <a href="http://time.com/9912/10-things-your-commute-does-to-your-body/">two</a>, <a href="http://www.businessinsider.com/long-commutes-have-an-impact-on-health-and-productivity-2017-5">three</a>, <a href="https://www.theguardian.com/news/datablog/2014/feb/12/how-does-commuting-affect-wellbeing">four</a>, <a href="http://www.npr.org/2011/10/19/141514467/small-changes-can-help-you-thrive-happily">five</a>, <a href="https://www.forbes.com/sites/amymorin/2014/12/07/want-to-be-happier-change-your-commute-or-change-your-attitude/#5d71cadc7417">six</a>, <a href="http://webarchive.nationalarchives.gov.uk/20160107224314/http://www.ons.gov.uk/ons/dcp171766_351954.pdf">seven</a><a href="#fnref6">↩</a></p></li><li class="footnote" id="fn-2"><p><a href="http://www.npr.org/2015/09/22/442582422/the-cost-of-interruptions-they-waste-more-time-than-you-think">http://www.npr.org/2015/09/22/442582422/the-cost-of-interruptions-they-waste-more-time-than-you-think</a><a href="#fnref2">↩</a></p></li><li class="footnote" id="fn-3"><p>Though, you may want to pay for the Internet or provide a budget to help remote employees set up their home office.<a href="#fnref3">↩</a></p></li><li class="footnote" id="fn-4"><p><a href="http://amr.aom.org/content/33/2/309.short">Carrying too Heavy a Load? The Communication and Miscommunication of Emotion by Email</a> and <a href="https://www.fastcodesign.com/3036748/evidence/why-its-so-hard-to-detect-emotion-in-emails-and-texts">Why It&apos;s So Hard To Detect Emotion In Emails And Texts</a><a href="#fnref4">↩</a></p></li><li class="footnote" id="fn-5"><p>Like being surprised at how tall or short your coworkers are. It gets me every time.<a href="#fnref5">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2017/09/29/measure-aggregate-performance/index.html</id>
    <link href="https://jakemccrary.com/blog/2017/09/29/measure-aggregate-performance/index.html"/>
    <title><![CDATA[Measuring aggregate performance in Clojure]]></title>
    <updated>2017-09-29T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Last time I needed to speed up some code, I wrote a Clojure macro that recorded the aggregate time spent executing the code wrapped by the macro. Aggregate timings were useful since the same functions were called multiple times in the code path we were trying to optimize. Seeing total times made it easier to identify where we should spend our time.</p><p>Below is the namespace I temporarily introduced into our codebase.</p><pre><code class="language-clojure">(ns metrics)

(defn msec-str
  &quot;Returns a human readable version of milliseconds based upon scale&quot;
  [msecs]
  (let [s 1000
        m (* 60 s)
        h (* 60 m)]
    (condp &gt;= msecs
      1 (format &quot;%.5f msecs&quot; (float msecs))
      s (format &quot;%.1f msecs&quot; (float msecs))
      m (format &quot;%.1f seconds&quot; (float (/ msecs s)))
      h (format &quot;%02dm:%02ds&quot; (int (/ msecs m))
                (mod (int (/ msecs s)) 60))
      (format &quot;%dh:%02dm&quot; (int (/ msecs h))
              (mod (int (/ msecs m)) 60)))))

(def aggregates (atom {}))

(defmacro record-aggregate
  &quot;Records the total time spent executing body across invocations.&quot;
  [label &amp; body]
  `(do
     (when-not (contains? @aggregates ~label)
       (swap! aggregates assoc ~label {:order (inc (count @aggregates))}))
     (let [start-time# (System/nanoTime)
           result# (do ~@body)
           result# (if (and (seq? result#)
                            (instance? clojure.lang.IPending result#)
                            (not (realized? result#)))
                     (doall result#)
                     result#)
           end-time# (System/nanoTime)]
       (swap! aggregates
              update-in
              [~label :msecs]
              (fnil + 0)
              (/ (double (- end-time# start-time#)) 1000000.0))
       result#)))

(defn log-times
  &quot;Logs time recorded by record-aggregate and resets the aggregate times.&quot;
  []
  (doseq [[label data] (sort-by (comp :order second) @aggregates)
          :let [msecs (:msecs data)]]
    (println &quot;Executing&quot; label &quot;took:&quot; (msec-str msecs)))
  (reset! aggregates {}))
</code></pre><p><code>record-aggregate</code> takes a label and code and times how long that code takes to run. If the executed code returns an unrealized lazy sequence, it also evaluates the sequence<a href="#fn-1" id="fnref1"><sup>1</sup></a>.</p><p>Below is an example of using the above code. When we used it, we looked at the code path we needed to optimize and wrapped chunks of it in <code>record-aggregate</code>. At the end of the calculations, we inserted a call to <code>log-times</code> so timing data would show up in our logs.</p><pre><code class="language-clojure">(ns work
  (:require [metrics :as m]))

(defn calculation [x]
  (m/record-aggregate ::calculation
                      (Thread/sleep (+ 300 (rand-int 60)))
                      x))

(defn work [x]
  (m/record-aggregate ::work
                      (repeatedly 10 (fn []
                                       (Thread/sleep 5)
                                       x))))

(defn process-rows [rows]
  (let [rows (m/record-aggregate ::process-rows
                                 (-&gt;&gt; rows
                                      (mapv calculation)
                                      (mapcat work)))]
    (m/log-times)
    rows))
</code></pre><p>Now, when <code>(process-rows [:a :a])</code> is called output similar to below is printed.</p><pre><code>Executing :work/process-rows took: 780.9 msecs
Executing :work/calculation took: 664.6 msecs
Executing :work/work took: 115.8 msecs
</code></pre><p>Using this technique, we were able to identify slow parts of our process and were able to optimize those chunks of our code. There are potential flaws with measuring time like this, but they were not a problem in our situation<a href="#fn-2" id="fnref2"><sup>2</sup></a>.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>See <a href="/blog/2016/12/31/measure-what-you-intended-to-measure/">Measure what you intend to measure</a><a href="#fnref1">↩</a></p></li><li class="footnote" id="fn-2"><p>See <a href="https://shipilev.net/blog/2014/nanotrusting-nanotime/">Nanotrusting the Nanotime</a><a href="#fnref2">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2017/08/27/my-current-leiningen-profiles-dot-clj/index.html</id>
    <link href="https://jakemccrary.com/blog/2017/08/27/my-current-leiningen-profiles-dot-clj/index.html"/>
    <title><![CDATA[My current Leiningen profiles.clj]]></title>
    <updated>2017-08-27T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Nearly three years ago I wrote an overview of my <a href="/blog/2015/01/11/overview-of-my-leiningen-profiles-dot-clj/">Leiningen profiles.clj</a>. That post is one of my most visited articles, so I thought I&apos;d give an update on what I currently keep in <code>~/.lein/profiles.clj</code>.</p><pre><code class="language-clojure">{:user {:plugin-repositories [[&quot;private-plugins&quot; {:url &quot;private url&quot;}]]
        :dependencies [[pjstadig/humane-test-output &quot;0.8.2&quot;]]
        :injections [(require &apos;pjstadig.humane-test-output)
                     (pjstadig.humane-test-output/activate!)]
        :plugins [[io.sattvik/lein-ancient &quot;0.6.11&quot;]
                  [lein-pprint &quot;1.1.2&quot;]
                  [com.jakemccrary/lein-test-refresh &quot;0.21.1&quot;]
                  [lein-autoexpect &quot;1.9.0&quot;]]
        :signing {:gpg-key &quot;B38C2F8C&quot;}
        :test-refresh {:notify-command [&quot;terminal-notifier&quot; &quot;-title&quot; &quot;Tests&quot; &quot;-message&quot;]
                       :quiet true
                       :changes-only true}}}
</code></pre><p>The biggest difference between my <code>profiles.clj</code> from early 2015 and now is that I&apos;ve removed all of the CIDER related plugins. I still use CIDER, but CIDER no longer requires you to list its dependencies explicitly.</p><p>I’ve also removed Eastwood and Kibit from my toolchain. I love static analysis, but these tools fail too frequently with my projects. As a result, I rarely used them and I’ve removed them. Instead, I’ve started using <a href="https://github.com/candid82/joker">joker</a> for some basic static analysis and am really enjoying it. It is fast, and it has made refactoring in Emacs noticeably better.</p><p><a href="https://github.com/jakemcc/test-refresh">lein-test-refresh</a>, <a href="https://github.com/clojure-expectations/lein-autoexpect">lein-autoexpect</a>, and <a href="https://github.com/pjstadig/humane-test-output">humane-test-output</a> have stuck around and have been updated to the latest versions. These tools make testing Clojure much nicer.</p><p>I&apos;m also taking advantage of some new features that <a href="https://github.com/jakemcc/test-refresh">lein-test-refresh</a> provides. These settings enable the most reliable, fastest feedback possible while writing tests. My <a href="/blog/2016/06/20/my-recommended-clojure-testing-setup/">recommended testing setup</a> article goes into more details.</p><p><code>lein-ancient</code> and <code>lein-pprint</code> have stuck around. I rarely use <code>lein-pprint</code> but it comes in handy when debugging project.clj problems. <code>lein-ancient</code> is great for helping you keep your project&apos;s dependencies up to date. I use a forked version that contains some changes I need to work with my company&apos;s private repository.</p><p>And there you have it. My updated profiles.clj<a href="#fn-1" id="fnref1"><sup>1</sup></a>.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>Some of you might wonder why I don&apos;t just link to this file in version control somewhere? Well, it is kept encrypted in a git repository because it also contains some secrets that should not be public that I&apos;ve removed for this post.<a href="#fnref1">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2017/07/28/using-my-phones-voice-control-for-a-month/index.html</id>
    <link href="https://jakemccrary.com/blog/2017/07/28/using-my-phones-voice-control-for-a-month/index.html"/>
    <title><![CDATA[Using my phone's voice control for a month]]></title>
    <updated>2017-07-28T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>From May 6th to June 2nd the screen of my phone had a crack. I have an Android phone, and the crack was through the software buttons at the bottom of the screen. As a result, I could not touch the back, home, or overview (app switching) buttons. For nearly a month I never saw my home screen, couldn&apos;t go back, or switch apps through touching my phone. I was very reliant on arriving notifications giving me an opportunity to open apps.</p><p>It took me some time, but I realized I could use voice commands to replace some of the missing functionality. Using voice commands, I could open apps and no longer be at the whim of notifications.</p><p>Here is an example of my phone usage during this month. My thoughts are in [brackets]. <em>Italics</em> indicate actions. Talking is wrapped in “ ”.</p><ol><li>[Alright, I want to open Instagram] &quot;Ok Google, open Instagram.&quot;</li><li>[Sweet, it worked] <em>scrolls through feed</em></li><li><em>WhatsApp notification happens</em> [Great, a notification, I can click it to open WhatsApp]</li><li><em>I read messages in WhatsApp.</em></li><li>[Time to go back to Instagram] &quot;Ok Google, open Instagram&quot;</li><li>[sigh, voice command failed, lets try again] &quot;Ok Google, open Instagram&quot;</li><li><em>Instagram opens</em> [Great, time to scroll through more pictures]</li></ol><p>As you can see, it is a bit more painful than clicking buttons to switch between different apps. Voice commands fail sometimes and, at least for me, generally take more effort than tapping the screen. That’s ok though; I was determined to embrace voice commands and experience what a future of only voice commands might feel like.</p><p>Below are some observations from using my voice to control my phone for a month.</p><h3 id="it-is-awkward-in-public">It is awkward in public</h3><p>My phone usage in public went way down. There was something about having to talk to your phone to open an app that made me not want to pull out my phone.</p><p>It is much more obvious you are using your phone when you use your voice to control it. It makes casual glances at your phone while hanging out with a group impossible. You can’t sneak a quick look at Instagram when you need to say “Ok Google, open Instagram” without completely letting everyone around you know you are no longer paying attention.</p><p>This also stopped me from using my phone in Ubers/Lyfts/cabs. I often talk to the driver or other passengers anyway, but this cemented that. I realize it is completely normal to ignore the other people in a car but I felt like a (small) asshole audibly calling out that I&apos;m ignoring other people in the car.</p><h3 id="you-become-more-conscious-of-what-apps-you-use">You become more conscious of what apps you use</h3><p>When you have to say “Okay Google, open Instagram” every time you want to open Instagram, you become way more aware of how often you use Instagram. Using your voice instead of tapping a button on your screen is a much bigger hurdle between having the urge to open something and actually opening it. It gives you more time to observe what you are doing.</p><h3 id="you-become-more-conscious-of-using-your-phone">You become more conscious of using your phone</h3><p>Using your phone becomes a lot harder. This increased difficulty helped highlight when I was using my phone. My phone’s functionality dropped drastically and, as a result, I stopped reaching for it as much.</p><p>This reminded me of when I used a dumb (feature) phone for a couple of months a few years ago. Using a non-smartphone after using a smartphone for years was weird. It helped me reign in my usage<a href="#fn-1" id="fnref1"><sup>1</sup></a>.</p><h3 id="voice-control-can-be-pretty-convenient">Voice control can be pretty convenient</h3><p>Even after repairing my screen, I still find myself using some voice commands. While making my morning coffee, I often ask my phone for the weather forecast. This is more convenient than opening an app and it lets me continue to use both hands while making coffee.</p><p>Setting alarms, starting countdown timers, adding reminders, and checking the weather are all things I do through voice commands now.</p><h3 id="i-wish-it-worked-all-the-time">I wish it worked all the time</h3><p>I suppose this is an argument for getting a <a href="https://store.google.com/us/product/google_home?hl=en-US">Google Home</a> or Amazon Echo. I have to wake up my phone to use voice commands with it. This limits the usefulness of voice commands since I need be within reach of my phone.</p><h3 id="i-wish-it-could-do-more">I wish it could do more</h3><p>At some point, I got used to asking my phone to do things. Then I started giving it more complicated commands, and it would fail. I found myself giving it multi-stage commands such as “Ok Google, turn on Bluetooth and play my playlist Chill on Spotify.” That doesn&apos;t work but it would be amazing if it did.</p><h2 id="recommendations">Recommendations</h2><p>I recommend that you force yourself to use voice commands for some period of time. Pretend your home button is broken and you have to use voice control to move around your phone. You’ll become more aware of your phone usage and you&apos;ll learn some useful voice commands that will make your technology usage nicer.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>My non-smartphone experiment four years ago is what resulted in me no longer using Facebook or Twitter on my phone. It also is the reason I silenced most notifications, including email, on my phone.<a href="#fnref1">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2017/06/30/speeding-up-site-by-optionally-loading-disqus-comments/index.html</id>
    <link href="https://jakemccrary.com/blog/2017/06/30/speeding-up-site-by-optionally-loading-disqus-comments/index.html"/>
    <title><![CDATA[Speeding up this site by optionally loading Disqus comments]]></title>
    <updated>2017-06-30T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Earlier this month I took another look at what was required for reading an article on this site. What <a href="/blog/2016/04/30/speeding-up-my-blog/">else</a> could I do to make this site load faster?</p><p>To do this, I loaded up <a href="http://webpagetest.org/">WebPageTest</a> and pointed it towards one of my <a href="/blog/2017/05/30/adding-a-json-feed-to-octopress-slash-jekyll/">posts</a>. To my shock, it took 113 requests for a total of 721 KB to load a single post. This took WebPageTest 6.491 seconds. The document complete event triggered after 15 requests (103 KB, 1.6 seconds).</p><p>113 requests to load a static article was ridiculous. Most of those requests happened as a result of loading the Disqus javascript. I find comments valuable and want to continue including them on my site. Because of this, I couldn&apos;t remove Disqus. Instead, I made loading Disqus optional.</p><p>After making the required changes, it only takes 11 requests for 61 KB of data to fully load the test post. The document complete event only required 8 requests for 51 KB of data. Optionally loading the Disqus javascript resulted in a massive reduction of data transferred.</p><p>How did I do it? The template that generates my articles now only inserts the Disqus javascript when a reader clicks a button. My <a href="https://github.com/jakemcc/jakemccrary.com/blob/74f4232ce7263ba3de48497d0c0d10a8fa1a73f9/source/_includes/disqus.html">final template</a> is at the bottom of this post.</p><p>The template adds an <code>insertDisqus</code> function that inserts a <code>&lt;script&gt;</code> element when a reader clicks a button. This element contains the original JavaScript that loads Disqus. When the <code>&lt;script&gt;</code> element is inserted into the page, the Disqus javascript is loaded and the comments appear.</p><p>My exact template might not work for you, but I&apos;d encourage you to think about optionally loading Disqus and other non-required JavaScript. Your readers will thank you.</p><pre><code class="language-html">{% if site.disqus_short_name and page.comments == true %}
  &lt;noscript&gt;Please enable JavaScript to view the &lt;a href=&quot;http://disqus.com/?ref_noscript&quot;&gt;comments powered by Disqus.&lt;/a&gt;&lt;/noscript&gt;
  &lt;div id=&quot;disqus_target&quot;&gt;
    &lt;script&gt;
     var insertDisqus = function() {
       var elem = document.createElement(&apos;script&apos;);
       elem.innerHTML =  &quot;var disqus_shortname = &apos;{{ site.disqus_short_name }}&apos;; var disqus_identifier = &apos;{{ site.url }}{{ page.url }}&apos;; var disqus_url = &apos;{{ site.url }}{{ page.url }}&apos;; (function () {var dsq = document.createElement(&apos;script&apos;); dsq.type = &apos;text/javascript&apos;; dsq.async = true; dsq.src = &apos;//&apos; + disqus_shortname + &apos;.disqus.com/embed.js&apos;; (document.getElementsByTagName(&apos;head&apos;)[0] || document.getElementsByTagName(&apos;body&apos;)[0]).appendChild(dsq);}());&quot;
       var target = document.getElementById(&apos;disqus_target&apos;);
       target.parentNode.replaceChild(elem, target);
     }
    &lt;/script&gt;
    &lt;button class=&quot;comment-button&quot; onclick=&quot;insertDisqus()&quot;&gt;&lt;span&gt;ENABLE COMMENTS AND RECOMMENDED ARTICLES&lt;/span&gt;&lt;/button&gt;
  &lt;/div&gt;
{% endif %}
</code></pre></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2017/05/30/adding-a-json-feed-to-octopress-slash-jekyll/index.html</id>
    <link href="https://jakemccrary.com/blog/2017/05/30/adding-a-json-feed-to-octopress-slash-jekyll/index.html"/>
    <title><![CDATA[Adding a JSON Feed to an Octopress/Jekyll generated site]]></title>
    <updated>2017-05-30T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>I went to a coffee shop this last weekend with the intention of writing up a <a href="/blog/2017/05/29/using-comm-to-verify-matching-content/">quick article</a> on <code>comm</code>. I sat down, sipping my coffee, and wasn’t motivated. I didn’t feel like knocking out a short post, and I didn’t feel like editing a draft I’ve been sitting on for a while. I wanted to do some work though, so I decided to add a <a href="https://jsonfeed.org/">JSON Feed</a> to this site.</p><p>JSON Feed is an alternative to <a href="https://tools.ietf.org/html/rfc4287">Atom</a> and <a href="http://cyber.harvard.edu/rss/rss.html">RSS</a> that uses JSON instead of XML. I figured I could add support for it in less than the time it would take to enjoy my coffee and maybe some readers would find it useful. I’d be shocked if anyone actually finds this useful, but it was a fun little exercise anyway.</p><p>An old version of Octopress (2.something), which uses an old version of Jekyll (2.5.3), generates this site. Despite this, I don’t think the template would need to change much if I moved to a new version. The template below is saved as <a href="https://github.com/jakemcc/jakemccrary.com/blob/00d4b0416ab2591be5702286b735091a3d2e2105/source/feed.json">source/feed.json</a> in my git repository.</p><pre><code class="language-javascript">---
layout: null
---
{
  &quot;version&quot;: &quot;https://jsonfeed.org/version/1&quot;,
  &quot;title&quot;: {{ site.title | jsonify }},
  &quot;home_page_url&quot;: &quot;{{ site.url }}&quot;,
  &quot;feed_url&quot;: &quot;{{site.url}}/feed.json&quot;,
  &quot;favicon&quot;: &quot;{{ site.url }}/favicon.png&quot;,
  &quot;author&quot; : {
      &quot;url&quot; : &quot;https://twitter.com/jakemcc&quot;,
      &quot;name&quot; : &quot;{{ site.author | strip_html }}&quot;
  },
  &quot;user_comment&quot;: &quot;This feed allows you to read the posts from this site in any feed reader that supports the JSON Feed format. To add this feed to your reader, copy the following URL - {{ site.url }}/feed.json - and add it your reader.&quot;,
  &quot;items&quot;: [{% for post in site.posts limit: 20 %}
    {
      &quot;id&quot;: &quot;{{ site.url }}{{ post.id }}&quot;,
      &quot;url&quot;: &quot;{{ site.url }}{{ post.url }}&quot;,
      &quot;date_published&quot;: &quot;{{ post.date | date_to_xmlschema }}&quot;,
      &quot;title&quot;: {% if site.titlecase %}{{ post.title | titlecase | jsonify }}{% else %}{{ post.title | jsonify }}{% endif %},
      {% if post.description %}&quot;summary&quot;: {{ post.description | jsonify }},{% endif %}
      &quot;content_html&quot;: {{ post.content | expand_urls: site.url | jsonify }},
      &quot;author&quot; : {
        &quot;name&quot; : &quot;{{ site.author | strip_html }}&quot;
      }
    }{% if forloop.last == false %},{% endif %}
    {% endfor %}
  ]
}
</code></pre><p>I approached this problem by reading the <a href="https://jsonfeed.org/version/1">JSON Feed Version 1 spec</a> and cribbing values from the template for my Atom feed. The trickiest part was filling in the <code>&quot;content_html&quot;</code> value. It took me a while to find figure out that <code>jsonify</code> needed to be at the end of <code>{{ post.content | expand_urls: site.url | jsonify }}</code>. That translates the post&apos;s HTML content into its JSON representation. You’ll notice that any template expression with <code>jsonify</code> at the end also isn’t wrapped in quotes. This is because <code>jsonify</code> is doing that for me.</p><p>The <code>{% if forloop.last == false %},{% endif %}</code> is also important. Without this, the generated JSON has an extra <code>,</code> after the final element in items. This isn’t valid JSON.</p><p>I caught that by using the command line tool <a href="http://trentm.com/json/">json</a>. If you ever edit JSON by hand or generate it from a template then you should add this tool to your toolbox. It will prevent you from creating invalid JSON.</p><p>How did I use it? I’d make a change in the <code>feed.json</code> template and generate an output file. Then I’d <code>cat</code> that file to <code>json --validate</code>. When there was an error, I’d see a message like below.</p><pre><code class="language-console">0 [last: 5s] 12:43:47 ~/src/jakemcc/blog (master *)
$ cat public/feed.json | json --validate
json: error: input is not JSON: Expected &apos;,&apos; instead of &apos;{&apos; at line 25, column 5:
            {
        ....^
1 [last: 0s] 12:43:49 ~/src/jakemcc/blog (master *)
$
</code></pre><p>And there would be zero output on success.</p><pre><code class="language-console">0 [last: 5s] 12:45:25 ~/src/jakemcc/blog (master)
$ cat public/feed.json | json --validate
0 [last: 0s] 12:45:30 ~/src/jakemcc/blog (master)
$
</code></pre><p>It was pretty straightforward to add a JSON Feed. Was it a good use of my time? <code>¯\_(ツ)_/¯</code>. In the process of adding the feed I learned more about Liquid templating and figured out how to embed liquid tags into a blog post. Even adding redundant features can be a useful exercise.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2017/05/29/using-comm-to-verify-matching-content/index.html</id>
    <link href="https://jakemccrary.com/blog/2017/05/29/using-comm-to-verify-matching-content/index.html"/>
    <title><![CDATA[Using comm to verify file content matches]]></title>
    <updated>2017-05-29T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>I recently found myself in a situation where I needed to confirm that a process took in a tab separated file, did some processing, and then output a new file containing the original columns with some additional ones. The feature I was adding allowed the process to die and restart while processing the input file and pick up where it left off.</p><p>I needed to confirm the output had data for every line in the input. I reached to the command line tool <code>comm</code>.</p><p>Below is a made up input file.</p><pre><code>UNIQUE_ID	USER
1	38101838
2	19183819
3	19123811
4	10348018
5	19881911
6	29182918
</code></pre><p>And here is some made up output.</p><pre><code>UNIQUE_ID	USER	MESSAGE
1	38101838	A01
2	19183819	A05
3	19123811	A02
4	10348018	A01
5	19881911	A02
6	29182918	A05
</code></pre><p>With files this size, it would be easy enough to check visually. In my testing, I was dealing with files that had thousands of lines. This is too many to check by hand. It is a perfect amount for <code>comm</code>.</p><p><a href="https://en.wikipedia.org/wiki/Comm">comm</a> reads two files as input and then outputs three columns. The first column contains lines found only in the first file, the second column contains lines only found in the second, and the last column contains lines in both. If it is easier for you to think about it as set operations, the first two columns are similar to performing two set differences and the third is similar to set intersection. Below is an example adapted from Wikipedia showing its behavior.</p><pre><code>$ cat foo.txt
apple
banana
eggplant
$ cat bar.txt
apple
banana
banana
zucchini
$ comm foo.txt bar.txt
                  apple
                  banana
          banana
eggplant
          zucchini
</code></pre><p>So how is this useful? Well, you can also tell <code>comm</code> to suppress outputting specific columns.  If we send the common columns from the input and output file to <code>comm</code> and suppress <code>comm</code>&apos;s third column then anything printed to the screen is a problem. Anything printed to the screen was found in one of the files and not the other. We&apos;ll select the common columns using cut and, since comm expects input to be sorted, then sort using <code>sort</code>. Let&apos;s see what happens.</p><pre><code>$ comm -3 &lt;(cut -f 1,2 input.txt | sort) &lt;(cut -f 1,2 output.txt | sort)
$
</code></pre><p>Success! Nothing was printed to the console, so there is nothing unique in either file.</p><p><code>comm</code> is a useful tool to have in your command line toolbox.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2017/05/15/send-a-push-notification-when-your-external-ip-address-changes/index.html</id>
    <link href="https://jakemccrary.com/blog/2017/05/15/send-a-push-notification-when-your-external-ip-address-changes/index.html"/>
    <title><![CDATA[Send a push notification when your external IP address changes]]></title>
    <updated>2017-05-15T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>I need to know when my external IP address changes. Whenever it changes, I need to update an IP whitelist and need to re-login to a few sites. I sometimes don&apos;t notice for a couple of days and, during that time, some automatic processes fail.</p><p>After the last time this happened, I whipped up a script that sends me a push notification when my IP address changes.</p><p>The script uses <a href="https://pushover.net/">Pushover</a> to send the push notification. Pushover is great. I have used it for years to get notifications from my headless computers. If you use the below script, replace <code>${PUSHOVER_TOKEN}</code> and <code>${PUSHOVER_USER}</code> with your own details.</p><pre><code class="language-bash">#!/bin/bash

set -e

previous_file=&quot;${HOME}/.previous-external-ip&quot;

if [ ! -e &quot;${previous_file}&quot; ]; then
    dig +short myip.opendns.com @resolver1.opendns.com &gt; &quot;${previous_file}&quot;
fi

current_ip=$(dig +short myip.opendns.com @resolver1.opendns.com)

previous_ip=$(cat &quot;${previous_file}&quot;)

if [ &quot;${current_ip}&quot; != &quot;${previous_ip}&quot; ]; then
    echo &quot;external ip changed&quot;
    curl -s --form-string &quot;token=${PUSHOVER_TOKEN}&quot; \
         --form-string &quot;user=${PUSHOVER_USER}&quot; \
         --form-string &quot;title=External IP address changed&quot; \
         --form-string &quot;message=&apos;${previous_ip}&apos; =&gt; &apos;${current_ip}&apos;&quot; \
         https://api.pushover.net/1/messages.json
fi

echo &quot;${current_ip}&quot; &gt; &quot;${previous_file}&quot;
</code></pre></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2017/04/17/what-are-the-most-used-clojure-libraries/index.html</id>
    <link href="https://jakemccrary.com/blog/2017/04/17/what-are-the-most-used-clojure-libraries/index.html"/>
    <title><![CDATA[What are the most used Clojure libraries?]]></title>
    <updated>2017-04-17T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>In a <a href="/blog/2017/03/31/what-clojure-testing-library-is-most-used/">previous post</a>, we used Google&apos;s BigQuery and the public <a href="https://cloud.google.com/bigquery/public-data/github">GitHub dataset</a> to discover the most used Clojure testing library. The answer wasn&apos;t surprising. The built-in <code>clojure.test</code> was by far the most used.</p><p>Let&apos;s use the dataset to answer a less obvious question, what are the most used libraries in Clojure projects? We&apos;ll measure this by counting references to libraries in <code>project.clj</code> and <code>build.boot</code> files.</p><p>Before we can answer that question, we&apos;ll need to transform the data. First, we create the Clojure subset of the GitHub dataset. I did this by executing the following queries and saving the results to tables<a href="#fn-1" id="fnref1"><sup>1</sup></a>.</p><pre><code class="language-sql">-- Save the results of this query to the clojure.files table
SELECT
  *
FROM
  [bigquery-public-data:github_repos.files]
WHERE
  RIGHT(path, 4) = &apos;.clj&apos;
  OR RIGHT(path, 5) = &apos;.cljc&apos;
  OR RIGHT(path, 5) = &apos;.cljs&apos;
  OR RIGHT(path, 10) = &apos;boot.build&apos;

-- Save the results to clojure.contents
SELECT *
FROM [bigquery-public-data:github_repos.contents]
WHERE id IN (SELECT id FROM clojure.files)
</code></pre><p>Next we extract the dependencies from <code>build.boot</code> and <code>project.clj</code> files. Fortunately for us, both of these files specify dependencies in the same format, so we&apos;re able to use the same regular expression on both types.</p><p>The query below identifies <code>project.clj</code> and <code>build.boot</code> files, splits each file into lines, and extracts referenced library names and versions using a regular expression. Additional filtering is done get rid of some spurious results.</p><pre><code class="language-sql">SELECT
  REGEXP_EXTRACT(line, r&apos;\[+(\S+)\s+&quot;\S+&quot;]&apos;) AS library,
  REGEXP_EXTRACT(line, r&apos;\[+\S+\s+&quot;(\S+)&quot;]&apos;) AS version, 
  COUNT(*) AS count
FROM (
  SELECT
    SPLIT(content, &apos;\n&apos;) AS line
  FROM
    [clojure.contents]
  WHERE
    id IN (
    SELECT
      id
    FROM
      [clojure.files]
    WHERE
      path LIKE &apos;%project.clj&apos;
      OR path LIKE &apos;%build.boot&apos;)
      HAVING line contains &apos;[&apos;)
GROUP BY
  library, version
HAVING library is not null and not library contains &apos;&quot;&apos;
ORDER BY
  count DESC
</code></pre><p>The first five rows from the result are below. Let&apos;s save the entire result to a <code>clojure.libraries</code> table.</p><pre><code>| library             | version | count |
|---------------------+---------+-------|
| org.clojure/clojure | 1.6.0   | 7015  |
| org.clojure/clojure | 1.5.1   | 4251  |
| org.clojure/clojure | 1.7.0   | 4093  |
| org.clojure/clojure | 1.8.0   | 3016  |
| hiccup              | 1.0.5   | 1280  |
</code></pre><p>Now we can start answering all sorts of interesting questions.</p><p>What is the most referenced library put out under the <code>org.clojure</code> group?</p><pre><code>SELECT library, sum(count) count
FROM clojure.libraries
WHERE library CONTAINS &apos;org.clojure&apos;
GROUP BY library
ORDER BY count desc

| Row | library                        | count |
|-----+--------------------------------+-------|
|   1 | org.clojure/clojure            | 20834 |
|   2 | org.clojure/clojurescript      |  3080 |
|   3 | org.clojure/core.async         |  2612 |
|   4 | org.clojure/tools.logging      |  1579 |
|   5 | org.clojure/data.json          |  1546 |
|   6 | org.clojure/tools.nrepl        |  1244 |
|   7 | org.clojure/java.jdbc          |  1064 |
|   8 | org.clojure/tools.cli          |  1053 |
|   9 | org.clojure/tools.namespace    |   982 |
|  10 | org.clojure/test.check         |   603 |
|  11 | org.clojure/core.match         |   578 |
|  12 | org.clojure/math.numeric-tower |   503 |
|  13 | org.clojure/data.csv           |   381 |
|  14 | org.clojure/math.combinatorics |   372 |
|  15 | org.clojure/tools.reader       |   368 |
|  16 | org.clojure/clojure-contrib    |   335 |
|  17 | org.clojure/data.xml           |   289 |
|  18 | org.clojure/tools.trace        |   236 |
|  19 | org.clojure/java.classpath     |   199 |
|  20 | org.clojure/core.cache         |   179 |
</code></pre><p>Clojure and ClojureScript are at the top, which isn&apos;t surprising. I&apos;m surprised to see <code>tools.nrepl</code> in the next five results (rows 3-7). It is the only library out of the top that I haven&apos;t used.</p><p>What testing library is used the most? We already answered this in my <a href="/blog/2017/03/31/what-clojure-testing-library-is-most-used/">last article</a> but let&apos;s see if we get the same answer when we&apos;re counting how many times a library is pulled into a project.</p><pre><code>SELECT library, sum(count) count
FROM [clojure.libraries] 
WHERE library in (&apos;midje&apos;, &apos;expectations&apos;, &apos;speclj&apos;, &apos;smidjen&apos;, &apos;fudje&apos;)
GROUP BY library
ORDER BY count desc

| Row | library                | count |
|-----+------------------------+-------|
|   1 | midje                  |  1122 |
|   2 | speclj                 |   336 |
|   3 | expectations           |   235 |
|   4 | smidjen                |     1 |
</code></pre><p>Those results are close to the previous results. Of the non-clojure.test libraries, midje still ends up on top.</p><p>What groups (as identified by the Maven groupId) have their libraries referenced the most? Top 12 are below but the <a href="https://docs.google.com/a/jakemccrary.com/spreadsheets/d/1QGRRGSo5t5Pnpwizdv_H8negs8NBxtRour6KxWN6hVY/edit?usp=sharing">full result</a> is available.</p><pre><code>SELECT REGEXP_EXTRACT(library, r&apos;(\S+)/\S+&apos;) AS group, sum(count) AS count
FROM [clojure.libraries]
GROUP BY group
HAVING group IS NOT null
ORDER BY count DESC

| Row | group                 | count |
|-----+-----------------------+-------|
|   1 | org.clojure           | 39611 |
|   2 | ring                  |  5817 |
|   3 | com.cemerick          |  2053 |
|   4 | com.taoensso          |  1605 |
|   5 | prismatic             |  1398 |
|   6 | org.slf4j             |  1209 |
|   7 | cljsjs                |   868 |
|   8 | javax.servlet         |   786 |
|   9 | com.stuartsierra      |   642 |
|  10 | com.badlogicgames.gdx |   586 |
|  11 | cider                 |   560 |
|  12 | pjstadig              |   536 |
</code></pre><p>And finally, the question that inspired this article, what is the most used library?</p><pre><code>SELECT library, sum(count) count
FROM [clojure.libraries]
WHERE library != &apos;org.clojure/clojure&apos;
GROUP BY library
ORDER BY count desc

| Row | library                     | count |
|-----+-----------------------------+-------|
|   1 | compojure                   |  3609 |
|   2 | lein-cljsbuild              |  3413 |
|   3 | org.clojure/clojurescript   |  3080 |
|   4 | org.clojure/core.async      |  2612 |
|   5 | lein-ring                   |  1809 |
|   6 | cheshire                    |  1802 |
|   7 | environ                     |  1763 |
|   8 | ring                        |  1678 |
|   9 | clj-http                    |  1648 |
|  10 | clj-time                    |  1613 |
|  11 | hiccup                      |  1591 |
|  12 | lein-figwheel               |  1582 |
|  13 | org.clojure/tools.logging   |  1579 |
|  14 | org.clojure/data.json       |  1546 |
|  15 | http-kit                    |  1423 |
|  16 | lein-environ                |  1325 |
|  17 | ring/ring-defaults          |  1302 |
|  18 | org.clojure/tools.nrepl     |  1244 |
|  19 | midje                       |  1122 |
|  20 | com.cemerick/piggieback     |  1096 |
|  21 | org.clojure/java.jdbc       |  1064 |
|  22 | org.clojure/tools.cli       |  1053 |
|  23 | enlive                      |  1001 |
|  24 | ring/ring-core              |   995 |
|  25 | org.clojure/tools.namespace |   982 |
</code></pre><p><a href="https://github.com/weavejester/compojure">Compojure</a> takes the top slot. <a href="https://docs.google.com/a/jakemccrary.com/spreadsheets/d/1-zmcOVPKLGrdRT_VkTrRUuRFyuxxmXi9eeH6Xzlt7yg/edit?usp=sharing">Full results are available</a>.</p><p>Before doing this research I tried to predict what libraries I&apos;d see in the top 10. I thought that clj-time and clj-http would be up there. I&apos;m happy to see my guess was correct.</p><p>It was pretty pleasant using BigQuery to do this analysis. Queries took at most seconds to execute. This quick feedback let me play around in the web interface without feeling like I was waiting for computers to do work. This made the research into Clojure library usage painless and fun.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>I did this in early March 2017.<a href="#fnref1">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2017/03/31/what-clojure-testing-library-is-most-used/index.html</id>
    <link href="https://jakemccrary.com/blog/2017/03/31/what-clojure-testing-library-is-most-used/index.html"/>
    <title><![CDATA[Which Clojure testing library is most used?]]></title>
    <updated>2017-03-31T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>I&apos;ve always assumed that the built-in <code>clojure.test</code> is the most widely used testing library in the Clojure community. Earlier this month I decided to test this assumption using the Google&apos;s BigQuery <a href="https://cloud.google.com/bigquery/public-data/github">GitHub dataset</a>.</p><p>The BigQuery GitHub dataset contains over three terabytes of source code from more than 2.8 million open source GitHub repositories. BigQuery lets us quickly query this data using SQL.</p><p>Below is a table with the results (done in early March 2017) of my investigation. Surprising no one, <code>clojure.test</code> comes out as the winner and it is a winner by a lot.</p><pre><code>| Library      | # Repos Using |
|--------------+---------------|
| clojure.test |         14304 |
| midje        |          1348 |
| expectations |           429 |
| speclj       |           207 |
| smidjen      |             1 |
| fudje        |             1 |
</code></pre><p>23,243 repositories were identified as containing Clojure (or ClojureScript) code. This means there were about 6,953 repositories that didn&apos;t use any testing library<a href="#fn-1" id="fnref1"><sup>1</sup></a>. This puts the &quot;no tests or an obscure other way of testing&quot; in a pretty solid second place.</p><p>You should take these numbers as ballpark figures and not exact answers. I know from using GitHub&apos;s search interface that there are three public projects using <a href="https://github.com/jimpil/fudje">fudje</a><a href="#fn-2" id="fnref2"><sup>2</sup></a>.</p><p>So, why don&apos;t all three of those projects show up? The dataset only includes projects where Google could identify the project as open source and the GitHub licenses API is used to do that<a href="#fn-3" id="fnref3"><sup>3</sup></a>. Two of those three projects were probably unable to be identified as something with an appropriate license.</p><p>Another small problem is that since <code>expectations</code> is an actual word, it shows up outside of <code>ns</code> declarations. I ended up using a fairly simple query to generate this data and it only knows that <code>expectations</code> shows up somewhere in a file. I experimented with some more restrictive queries but they didn&apos;t drastically change the result and I wasn&apos;t sure they weren&apos;t wrong in other ways. If you subtract a number between 100 and 150 you&apos;ll probably have a more accurate expectations usage count.</p><p>Keep reading if you want to hear more about the steps to come up with the above numbers.</p><p>If you have other Clojure questions you think could be answered by querying this dataset, let me know in the comments or on <a href="https://twitter.com/jakemcc">twitter</a>. I have some more ideas, so I wouldn&apos;t be surprised if at least one more article gets written.</p><h2 id="the-details">The Details</h2><p>The process was pretty straightforward. Most of my time was spent exploring the tables, figuring out what the columns represented, figuring out what queries worked well, and manually confirming some of the results. BigQuery is very fast. Very little of my time was spent waiting for results.</p><h3 id="1.-setup-the-data">1. Setup the data</h3><p>You get 1 TB of free BigQuery usage a month. You can blow through this in a single query. Google provides sample tables that contain less data but I wanted to operate on the full set of Clojure(Script) files, so my first step was to execute some queries to create tables that only contained Clojure data.</p><p>First, I queried the <code>github_repos.files</code> table for all the Clojure(Script) files and saved that to a <code>clojure.files</code> table.</p><pre><code class="language-sql">SELECT
  *
FROM
  [bigquery-public-data:github_repos.files]
WHERE
  (RIGHT(path, 4) = &apos;.clj&apos;
    OR RIGHT(path, 5) = &apos;.cljc&apos;
    OR RIGHT(path, 5) = &apos;.cljs&apos;)
</code></pre><p>The above query took only 9.2 seconds to run and processed 328 GB of data.</p><p>Using the <code>clojure.files</code> table, we can select the source for all the Clojure code from the <code>github_repos.contents</code>. I saved this to a <code>clojure.contents</code> table.</p><pre><code class="language-sql">SELECT *
FROM [bigquery-public-data:github_repos.contents]
WHERE id IN (SELECT id FROM clojure.files)
</code></pre><p>This query processed 1.84 TB of data in 21.5 seconds. So fast. In just under 30 seconds, I&apos;ve blown through the free limit.</p><h3 id="2.-identify-what-testing-library-(or-libraries)-a-repo-uses">2. Identify what testing library (or libraries) a repo uses</h3><p>We can guess that a file uses a testing library if it contains certain string. The strings we&apos;ll search for are the namespaces we&apos;d expect to see required or used in a <code>ns</code> declaration. The below query does this for each file and then rolls up the results by repository. It took 3 seconds to run and processed 611 MB of data.</p><pre><code>SELECT
  files.repo_name,
  MAX(uses_clojure_test) uses_clojure_test,
  MAX(uses_expectations) uses_expectations,
  MAX(uses_midje) uses_midje,
  MAX(uses_speclj) uses_speclj,
  MAX(uses_fudje) uses_fudje,
  MAX(uses_smidjen) uses_smidjen,
FROM (
  SELECT
    id,
    contents.content LIKE &apos;%clojure.test%&apos; uses_clojure_test,
    contents.content LIKE &apos;%expectations%&apos; uses_expectations,
    contents.content LIKE &apos;%midje%&apos; uses_midje,
    contents.content LIKE &apos;%speclj%&apos; uses_speclj,
    contents.content LIKE &apos;%fudje%&apos; uses_fudje,
    contents.content LIKE &apos;%smidjen%&apos; uses_smidjen,
  FROM
    clojure.contents AS contents) x
JOIN
  clojure.files files ON files.id = x.id
GROUP BY
  files.repo_name
</code></pre><p>Below is a screenshot of the first few rows in the result.</p><p><img alt="BigQuery results for test library usage by repo" src="/images/bigquery-testing-library-result.png" title="BigQuery results for test library usage by repo" /></p><h3 id="3.-export-the-data">3. Export the data</h3><p>At this point, we could continue doing the analysis using SQL and the BigQuery UI but I opted to explore the data using Clojure and the repl. There were too many rows to directly download the query results as a csv file, so I ended up having to save the results as a table and then export it to Google&apos;s cloud storage and download from there.</p><p>The first few rows of the file look like this:</p><pre><code>files_repo_name,uses_clojure_test,uses_expectations,uses_midje,uses_speclj,uses_fudje,uses_smidjen
wangchunyang/clojure-liberator-examples,true,false,false,false,false,false
yantonov/rex,false,false,false,false,false,false
</code></pre><h3 id="4.-calculate-some-numbers">4. Calculate some numbers</h3><p>The code takes the csv file and does some transformations. You could do this in Excel or using any language of your choice. I&apos;m not going to include code here, as it isn&apos;t that interesting.</p><h2 id="bigquery-thoughts">BigQuery thoughts</h2><p>This was my first time using Google&apos;s BigQuery. This wasn&apos;t the most difficult analysis to do but I was impressed at the speed and ease of use. The web UI, which I used entirely for this, is neither really great or extremely terrible. It mostly just worked and I rarely had to look up documentation.</p><p>I don&apos;t really feel comfortable making a judgment call on if the cost is expensive or not but this article cost a bit less than seven dollars to write. This doesn&apos;t seem too outrageous to me.</p><p>Based on my limited usage of BigQuery, it is something I&apos;d look into further if I needed its capabilities.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>Probably higher, as projects can and use more than one testing library.<a href="#fnref1">↩</a></p></li><li class="footnote" id="fn-2"><p>And those projects are <a href="https://github.com/jumarko/clojure-random">jumarko/clojure-random</a>, <a href="https://github.com/dpassen1/great-sort">dpassen1/great-sort</a>, and <a href="https://github.com/jimpil/fudje">jimpil/fudje</a>.<a href="#fnref2">↩</a></p></li><li class="footnote" id="fn-3"><p><a href="https://news.ycombinator.com/item?id=12004644">Source is a Google Developer Advocate&apos;s response on old HN post</a><a href="#fnref3">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2017/02/27/using-lein-test-refresh-with-expectations/index.html</id>
    <link href="https://jakemccrary.com/blog/2017/02/27/using-lein-test-refresh-with-expectations/index.html"/>
    <title><![CDATA[Using lein-test-refresh with expectations]]></title>
    <updated>2017-02-27T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>The 2.2.0 release<a href="#fn-1" id="fnref1"><sup>1</sup></a> of <a href="https://github.com/clojure-expectations/expectations/blob/master/CHANGELOG.md#changes-in-version-220">expectations</a> adds a <code>clojure.test</code> <a href="https://clojure-expectations.github.io/clojure-test.html">compatible syntax</a>. The release adds the <code>defexpect</code> macro which forces you to name your test but then generates code that is compatible with <code>clojure.test</code>.</p><p>Why would you want this? Because <code>clojure.test</code> is the built-in testing library for Clojure, an entire ecosystem has been built around it. Tool support for <code>clojure.test</code> is always going to be ahead of support for the original <code>expectations</code>. By using the new <code>clojure.test</code> compatible syntax, <code>expectations</code> can take advantage of all the tools built for <code>clojure.test</code>.</p><h3 id="using-lein-test-refresh-with-expectations">Using lein-test-refresh with expectations</h3><p>If you move to the new <code>clojure.test</code> compatible syntax, you can start using <a href="https://github.com/jakemcc/test-refresh">lein-test-refresh</a> to automatically rerun your tests when your code changes. <code>lein-test-refresh</code> is a fork of the original expectations autorunner, <a href="https://github.com/clojure-expectations/lein-autoexpect">lein-autoexpect</a>, but it has grown to have more features than its original inspiration. Now you can use it with <code>expectations</code><a href="#fn-2" id="fnref2"><sup>2</sup></a>.</p><p>Below is a sample <code>project.clj</code> that uses <code>lein-test-refresh</code> with the latest expectations.</p><pre><code class="language-clojure">(defproject expectations-project &quot;0.1.0-SNAPSHOT&quot;
  :description &quot;Sample project using expectations&quot;
  :dependencies [[org.clojure/clojure &quot;1.8.0&quot;]]
  :plugins [[com.jakemccrary/lein-test-refresh  &quot;0.18.1&quot;]]
  :profiles {:dev {:dependencies [[expectations &quot;2.2.0-beta1&quot;]]}})
</code></pre><p>Here is an example test file.</p><pre><code class="language-clojure">(ns expectations-project.core-test
  (:require [expectations :refer :all]
            [expectations.clojure.test :refer [defexpect]]))

(defexpect two
  2 (+ 1 1))

(defexpect three
  3 (+ 1 1))

(defexpect group
  (expect [1 2] (conj [] 1 5))
  (expect #{1 2} (conj #{} 1 2))
  (expect {1 2} (assoc {} 1 3)))
</code></pre><p>And here is the result of running <code>lein test-refresh</code>.</p><pre><code>$ lein test-refresh
*********************************************
*************** Running tests ***************
:reloading (expectations-project.core-test)

FAIL in (group) (expectations_project/core_test.clj:11)
expected: [1 2]
  actual: [1 5] from (conj [] 1 5)

FAIL in (group) (expectations_project/core_test.clj:11)
expected: {1 2}
  actual: {1 3} from (assoc {} 1 3)

FAIL in (three) (expectations_project/core_test.clj:8)
expected: 3
  actual: 2 from (+ 1 1)

Ran 3 tests containing 5 assertions.n
3 failures, 0 errors.

Failed 3 of 5 assertions
Finished at 11:53:06.281 (run time: 0.270s)
</code></pre><p>After some quick edits to fix the test errors and saving the file, here is the output from the tests re-running.</p><pre><code>*********************************************
*************** Running tests ***************
:reloading (expectations-project.core-test)

Ran 3 tests containing 5 assertions.
0 failures, 0 errors.
:reloading ()

Ran 3 tests containing 5 assertions.
0 failures, 0 errors.

Passed all tests
Finished at 11:53:59.045 (run time: 0.013s)
</code></pre><p>If you&apos;re using <code>expectations</code> and switch to the new <code>clojure.test</code> compatible syntax, I&apos;d encourage you to start using <a href="https://github.com/jakemcc/test-refresh">lein-test-refresh</a>.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>As of 2016-02-27 <code>2.2.0</code> isn&apos;t out yet, but <code>2.2.0-beta1</code> has been released and has the changes.<a href="#fnref1">↩</a></p></li><li class="footnote" id="fn-2"><p>In fact, you have to use it if you use Leiningen and the new syntax and want your tests to run automatically.<a href="#fnref2">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2017/01/04/reading-in-2016/index.html</id>
    <link href="https://jakemccrary.com/blog/2017/01/04/reading-in-2016/index.html"/>
    <title><![CDATA[Reading in 2016]]></title>
    <updated>2017-01-04T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>The time has come for another end-of-year summary of my reading from the previous year. Here are links to my previous end-of-year reflections: <a href="//jakemccrary.com/blog/2014/01/01/using-incanter-to-review-my-2013-reading/">2013</a>, <a href="//jakemccrary.com/blog/2015/01/08/reading-in-2014/">2014</a>, <a href="//jakemccrary.com/blog/2016/03/13/reading-in-2015/">2015</a>.</p><p>I&apos;ve continued to keep track of my reading using <a href="http://goodreads.com">Goodreads</a>. My <a href="https://www.goodreads.com/user/show/3431614-jake-mccrary">profile</a> contains the full list and reviews of books I&apos;ve read since 2010. <a href="https://www.goodreads.com/review/list/3431614-jake-mccrary?read_at=2016">Here</a> is my full 2016 list.</p><h2 id="2016-goal">2016 Goal</h2><p>My 2016 goal was to read one or two biographies. It is a genre that I typically don&apos;t read and I felt like branching out. I&apos;m going to consider this goal achieved. I read <a href="https://www.amazon.com/Open-Autobiography-Andre-Agassi/dp/0307388409">Open</a> (<a href="https://www.goodreads.com/review/show/1679109928">my review</a>) by Andre Agassi (well, ghostwritten for him) and <a href="http://amzn.to/2iQrZkG">Medium Raw</a> by Anthony Bourdain. Both have been tagged as memoirs so in the strictest sense maybe I shouldn&apos;t count them towards my goal but I&apos;m counting them. <em>Open</em> tells Andre Agassi&apos;s life story and claims to have been fact checked so it is pretty close to a biography.</p><p>Of the two, I drastically preferred <em>Open</em>. I would not recommend <em>Medium Raw</em> unless you know you like Bourdain&apos;s writing and the book description sounds interesting to you. <em>Open</em> has a broader appeal and tells an interesting story of a man&apos;s conflict, struggle and success.</p><h2 id="2016-numbers">2016 Numbers</h2><p>I read 59 books in 2016 for a total of 22,397 pages. I also read every issue of Amazon&apos;s <em>Day One</em> weekly periodical (as I have every year since <em>Day One</em> started being published). Overall my rating distribution is pretty similar to 2015, with my three, four, and five star categories all containing two or three more books than the previous year.</p><h2 id="recommendations">Recommendations</h2><p>I awarded twelve books a five star rating. I&apos;ve listed them below in no particular order. The review links to go my review on Goodreads.</p><ul><li>The Brain Audit: Why Customers Buy (And Why They Don&apos;t) (<a href="https://www.goodreads.com/review/show/1534752389">my review</a>)</li><li>The Charisma Myth - Olivia Fox Cabane (<a href="https://www.goodreads.com/review/show/1047168473">my review</a>)</li><li>Lying - Sam Harris (<a href="https://www.goodreads.com/review/show/1518371907">my review</a>)</li><li>To Sell Is Human: The Surprising Truth About Moving Others - Daniel Pink (<a href="https://www.goodreads.com/review/show/622644583">my review</a>)</li><li>Climbing Anchors - John Long and Bob Gains (<a href="https://www.goodreads.com/review/show/1484465759">my review</a>)</li><li>Creativity, Inc. - Ed Catmull and Amy Wallace (<a href="http://www.goodreads.com/review/show/947747190">my review</a>)</li><li>Chasing the Scream - Johann Hari (<a href="https://www.goodreads.com/review/show/1184643637">my review</a>)</li><li>Deep Work - Cal Newport (<a href="https://www.goodreads.com/review/show/1529172855">my review</a>)</li><li>Seveneves - Neal Stephenson (<a href="https://www.goodreads.com/review/show/1295783533">my review</a>)</li><li>A Little Life - Hanya Yanagihara (<a href="https://www.goodreads.com/review/show/1474596789">my review</a>)</li><li>The Paper Menagerie and Other Stories - Ken Liu (<a href="https://www.goodreads.com/review/show/1827462108">my review</a>)</li><li>This Is How You Lose Her - Junot Díaz (<a href="https://www.goodreads.com/review/show/1600396478">my review</a><a href="#fn-1" id="fnref1"><sup>1</sup></a>)</li></ul><p>While I enjoyed all of the above books, a few stand out.</p><h4 id="chasing-the-scream-by-johann-hari"><em>Chasing the Scream</em> by Johann Hari</h4><p>This book is about the war on drugs. It brings together studies, history, and anecdotes to present a compelling read on addiction and drug policy. I think it would be hard to finish this book and not be persuaded to vote for policies of drug decriminalization or legalization and more humane addiction treatments. The <a href="https://www.goodreads.com/book/show/24379960-chasing-the-scream">goodreads reviews</a> of this book are generally extremely positive, with 61% of them being five stars. Go read the book&apos;s summary and a few reviews and you&apos;ll want to read this book.</p><h4 id="deep-work-by-cal-newport"><em>Deep Work</em> by Cal Newport</h4><p>This book is game changer for those of us that have hobbies or work in fields where distraction-free concentration is beneficial or required. Cal Newport makes the argument that the ability to perform deep work is critical for mastering complicated information and producing great results. The book is split in to two parts. The first defines and makes the argument for deep work. The second prescribes rules for enabling yourself to perform more deep work. I highly recommend reading this book <strong>and</strong> implementing the recommendations<a href="#fn-2" id="fnref2"><sup>2</sup></a>.</p><h4 id="a-little-life-by-hanya-yanagihara"><em>A Little Life</em> by Hanya Yanagihara</h4><p>This is a depressing and challenging book. As <a href="https://www.goodreads.com/review/show/1167073305?book_show_action=true">this</a> review says, this book is about a terribly broken character, Jude, and his struggles. The writing is excellent. If you feel up to reading a long, difficult book that exposes you to some terrible experiences then read this book. I&apos;m having a hard time saying this was my favorite fiction book from last year but it is the only five star fiction book I&apos;m specifically calling out.</p><h4 id="moral-tribes-by-joshua-greene"><em>Moral Tribes</em> by Joshua Greene</h4><p>I didn&apos;t give this book five stars but I&apos;m still going to recommend <a href="https://www.amazon.com/Moral-Tribes-Emotion-Reason-Between/dp/0143126059">Moral Tribes</a> by Joshua Greene. This book explains how conflict arises when two moral groups meet and interact (an &quot;Us&quot; vs &quot;Them&quot; situation) and proposes Utilitarianism as a solution to this problem. While some parts of the book were a chore to finish, I&apos;m glad I&apos;ve read this book. <a href="https://www.goodreads.com/review/show/1825978507">My review</a> highlights more of what I found interesting in this book.</p><p>There is a section of the book that presents both sides of the abortion debate which I&apos;ve shown to friends on both side of the debate. All of them seemed to enjoy reading this small section of the book.</p><h2 id="series-read-this-year">Series read this year</h2><p>I finish almost every book I start reading. I read quick enough where I find it worthwhile to finish marginal books just in case they turn out good. With a book series the end of each book provides a checkpoint for me to reevaluate if I want to continue the series. While none of the below series earned five stars, I&apos;m highlighting them because each series represents me making the choice to continue staying in author&apos;s world.</p><p>I continued <a href="https://www.amazon.com/The-Expanse-6-Book-Series/dp/B01J20PMZ6">The Expanse</a> series this year and read books 4-6. This is just great space opera and I&apos;ll continue reading it until it stops being written.</p><p>I started and finished Don Winslow&apos;s <a href="https://www.goodreads.com/series/156704-power-of-the-dog">The Power of the Dog</a> series. It tells the fictionalized tale of drug cartels in Mexico. Parts are brutal and violent and unfortunately often based on real events.</p><p>I read the three main books of Ann Leckie&apos;s <a href="https://www.goodreads.com/series/113751-imperial-radch">Imperial Radch</a>. It is a neat science fiction story that deals with interesting topics. As a warning, I found the first book in the series to be the weakest. Keep pushing and read the second before giving up on this story.</p><p>Scott Meyer&apos;s <a href="https://www.goodreads.com/series/131379-magic-2-0">Magic 2.0</a> was a fun read in which the main character realizes he can edit a file and change reality. It is a silly premise and it delivers good, funny, lighthearted reading.</p><p>I also read Neil Gaiman&apos;s <em>American Gods</em> and <em>Anasazi Boys</em>. I devoured both of these books.</p><h2 id="technical-books-read">Technical books read</h2><p>I didn&apos;t read many books on software in 2016. I read the lowest number of software related books since I&apos;ve started keeping track. I finished Ben Rady&apos;s <a href="https://www.amazon.com/Serverless-Single-Page-Apps-Available/dp/1680501496">Serverless Single Page Apps</a> and Ron Jeffries&apos; <a href="https://www.amazon.com/gp/product/B00VDHRFWU">The Nature of Software Development</a>. I also read most of Google&apos;s Site Reliability Engineering but did not finish it before the end of 2016. I read more non-fiction books than in 2015 but I would have liked to see more software books in the mix.</p><h2 id="more-stats">More stats</h2><p>There were definitely a couple months where my reading took a definite dip. I don&apos;t remember what I was doing during April or September but apparently I wasn&apos;t reading.</p><p><img alt="Chart of reading per month" src="/images/books-and-pages-read-2016.png" title="Chart of reading per month" /></p><p>Unsurprisingly, ebooks continued to be my preferred format. This isn&apos;t surprising and I expect that this trend continues. At this point the only reason I&apos;ll even potentially look at this stat next year is because it forces me to go through and confirm each book had its format recorded correctly.</p><pre><code>|           | 2014 | 2015 | 2016 |
|-----------+------+------+------|
| ebook     |   64 |   47 |   56 |
| hardcover |    1 |    1 |    0 |
| paperback |    4 |    3 |    3 |
</code></pre><p>My average rating was about the same as 2015.</p><pre><code>| Year | Average Rating |
|------+----------------|
| 2011 |           3.84 |
| 2012 |           3.66 |
| 2013 |           3.67 |
| 2014 |           3.48 |
| 2015 |           3.86 |
| 2016 |           3.83 |
</code></pre><p>I read multiple books by quite a few authors. Below is a table summarizing my repeat</p><pre><code>| Author           | Average Rating | Number of Books | Number of Pages |
|------------------+----------------+-----------------+-----------------|
| Ann Leckie       |          3.667 |               3 |            1205 |
| James S.A. Corey |          3.667 |               3 |            1673 |
| Scott Meyer      |              4 |               3 |            1249 |
| Don Winslow      |              4 |               2 |            1182 |
| Junot Díaz       |              4 |               2 |             565 |
| Michael Crichton |            3.5 |               2 |             814 |
| Neil Gaiman      |              4 |               2 |             981 |
</code></pre><h2 id="2017-goals">2017 goals</h2><p>This year I&apos;m planning on revisiting some of my favorite books. I&apos;m not going to set a concrete number for this goal and will just have to trust myself to honestly judge if I accomplish this goal.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>This review is currently blank on Goodreads because this was a book read for a book club I&apos;m part of and we haven&apos;t met to discuss the book yet. We typically don&apos;t post reviews online of books we&apos;ll be discussing. To any of my book club members that are reading this post, I apologize for spoiling the surprise of what star rating I&apos;m planning on giving this book.<a href="#fnref1">↩</a></p></li><li class="footnote" id="fn-2"><p>I&apos;m considering writing up more about this book and some changes I&apos;ve made to help myself do more deep work.<a href="#fnref2">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2016/12/31/measure-what-you-intended-to-measure/index.html</id>
    <link href="https://jakemccrary.com/blog/2016/12/31/measure-what-you-intended-to-measure/index.html"/>
    <title><![CDATA[Making code fast: Measure what you intend to measure]]></title>
    <updated>2016-12-31T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>I’ve spent a significant portion of my career figuring out how to make software run faster. It is a problem I enjoy solving. One of the most important steps in an optimization task is to identify what you are trying to optimize and how you will measure it. Answer these questions wrong and you’ll waste your time solving the wrong problem.</p><p>Recently I joined a teammate on a task that involved identifying a bottleneck in a Clojure code base. We knew the code path we needed to optimize and turned to the  <a href="https://github.com/ptaoussanis/tufte">Tufte</a> library to take timing measurements. This was my first time using Tufte and, with my tiny amount of usage, I like what I see.</p><p>At some point in the process, we had code<a href="#fn-1" id="fnref1"><sup>1</sup></a> that looked similar to the <code>translate</code> function below (lines 20-24).</p><pre><code class="language-clojure">(ns bench.core
  (:require [clojure.string :as string]
            [taoensso.tufte :as tufte]))

(defn raw-&gt;maps [lines]
  (map (fn [line]
         (zipmap [:a :b :c]
                 (map (fn [s] (Long/parseLong s))
                      (string/split line #&quot;\|&quot;))))
       lines))

(defn summarize [maps]
  (reduce (fn [r m]
            (-&gt; r
                (update :a (fnil + 0) (:a m))
                (update :b (fnil + 0) (:b m))
                (update :c (fnil + 0) (:c m))))
          maps))

(defn translate [lines]
  (tufte/profile {}
                 (let [maps (tufte/p ::raw-&gt;maps (raw-&gt;maps lines))
                       summary (tufte/p ::summarize (summarize maps))]
                   summary)))
</code></pre><p>Here is some Tufte output from running some data through <code>translate</code>.</p><pre><code>                  pId      nCalls       Min        Max       MAD      Mean   Time% Time
:bench.core/summarize           1   346.0ms    346.0ms       0ns   346.0ms     100 346.0ms
:bench.core/raw-&gt;maps           1    2.46µs     2.46µs       0ns    2.46µs       0 2.46µs
           Clock Time                                                          100 346.05ms
       Accounted Time                                                          100 346.0ms
</code></pre><p>Notice anything surprising with the output?<a href="#fn-2" id="fnref2"><sup>2</sup></a></p><p>It surprised me that <code>raw-&gt;maps</code> took such a tiny amount of time compared to the <code>summarize</code> function. Then I realized that we had forgotten about Clojure’s lazy sequences. <code>summarize</code> is taking so much of the time because <code>raw-&gt;maps</code> is just creating a lazy sequence; all the work of realizing that sequence happens in <code>summarize</code>. By wrapping the call to <code>raw-&gt;maps</code> with a <code>doall</code> we were able to get the time measurements we intended.</p><p>This example demonstrates an important lesson. When you are profiling code, make sure you are measuring what you think you are measuring. This can be challenging in languages, such as Clojure, that have a concept of laziness. Reflect on your measurement results and perform a gut check that the results make sense with what you intended to measure. If anything feels off, confirm that you’re measuring what you meant to measure.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>Example built using clojure 1.8.0 and tufte 1.1.1. Also, sorry for the terrible names of functions. I was drawing a blank when coming up with this example.<a href="#fnref1">↩</a></p></li><li class="footnote" id="fn-2"><p>Imagine this output having 10 more lines in it. Now imagine it having 20. It starts getting quite a bit more difficult to notice oddities as more and more lines get added to this output. Try not to overwhelm yourself by having too much output.<a href="#fnref2">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2016/11/30/unify-your-project-interfaces/index.html</id>
    <link href="https://jakemccrary.com/blog/2016/11/30/unify-your-project-interfaces/index.html"/>
    <title><![CDATA[Unify your project interfaces]]></title>
    <updated>2016-11-30T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Jeff Ramnani wrote an <a href="https://jefframnani.com/project-build-protocol/">article</a> about unifying your command line interactions across programming projects. I recommend that you read it. The basic gist is that we often find ourselves working on multiple projects at a time. Frequently these projects are in different languages and use different build tools. Remembering the necessary incantations to interact with the various projects is difficult and we&apos;re lazy. We can do better by standardizing an interface to our projects.</p><p>This interface can take many forms. One option is to have a <code>bin</code> or <code>scripts</code> directory in each project and then consistently name the scripts you put in there (examples: <code>run</code>, <code>test</code>, and <code>build</code>). Another option is to use Makefiles with consistently named targets. Either way, your projects now have a standard way of interacting with them. This frees you from having to remember all the various commands and makes onboarding new developers easier.</p><p>I&apos;ve been using a similar approach to Jeff Ramnani for years and highly recommend it. I&apos;m a fan of the Makefile approach but either approach works. The unified targets I use across projects are the following:</p><ul><li><code>up</code> - Brings the system up</li><li><code>status</code> - Is the system up and running?</li><li><code>logs</code> - Show me the logs</li><li><code>local-db</code> - Connect to my local database</li><li><code>build</code> - Build the project</li><li><code>test</code> - Run the tests</li></ul><p>If you haven&apos;t created a common interface for your projects I recommend that you do it. It definitely makes moving between projects easier.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2016/10/28/html-markup-for-better-sharing-on-social-media/index.html</id>
    <link href="https://jakemccrary.com/blog/2016/10/28/html-markup-for-better-sharing-on-social-media/index.html"/>
    <title><![CDATA[HTML markup for better sharing on social media]]></title>
    <updated>2016-10-28T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>For a bit more than a year I worked on a project that crawled the web and indexed articles. Two of our sources of data were articles shared on Facebook and Twitter. After seeing hundreds of article previews on these two social networks, I decided to improve how my own articles were previewed.</p><p>I thought figuring out the markup I needed to add would be a painless experience. Unfortunately, when you search for this information you end up at various SEO optimization and other similar sites where you get the pleasure of experiencing full screen pop-overs trying to get you to sign up for mailing lists and other annoying features of the modern web. Probably unsurprisingly, the least annoying source for this information turned out to be the social networks themselves.</p><p>Below is what you will want to add to the <code>&lt;head&gt;</code> section of your articles&apos; markup. Items in all caps should be values that make sense for your articles. Most fields are pretty self-evident, but check <a href="https://dev.twitter.com/cards/markup">Twitter&apos;s</a> and <a href="https://developers.facebook.com/docs/sharing/webmasters#markup">Facebook&apos;s</a> documentation for more details. The <a href="http://ogp.me/">Open Graph</a> documentation has more details as well.</p><pre><code class="language-html">&lt;!-- Twitter Card data --&gt;
&lt;meta name=&quot;twitter:card&quot; content=&quot;SUMMARY&quot; /&gt;
&lt;meta name=&quot;twitter:site&quot; content=&quot;TWITTER HANDLE OF SITE (@jakemcc for this site)&quot; /&gt;
&lt;meta name=&quot;twitter:creator&quot; content=&quot;YOUR TWITTER HANDLE&quot; /&gt;
&lt;meta name=&quot;twitter:title&quot; content=&quot;ARTICLE TITLE&quot; /&gt;
&lt;meta name=&quot;twitter:description&quot; content=&quot;SHORT DESCRIPTION OF CONTENT&quot; /&gt;
&lt;meta name=&quot;twitter:image&quot; content=&quot;IMAGE THAT SHOWS UP WITH PREVIEW&quot; /&gt;

&lt;!-- Open Graph data --&gt;
&lt;meta property=&quot;og:site_name&quot; content=&quot;SITE TITLE&quot; /&gt;
&lt;meta property=&quot;og:url&quot; content=&quot;CANONICAL URL&quot; /&gt;
&lt;meta property=&quot;og:title&quot; content=&quot;ARTICLE TITLE&quot; /&gt;
&lt;meta property=&quot;og:description&quot; content=&quot;SHORT DESCRIPTION OF CONTENT&quot; /&gt;
&lt;meta property=&quot;og:image&quot; content=&quot;IMAGE THAT SHOWS UP WITH PREVIEW&quot; /&gt;
&lt;meta property=&quot;og:type&quot; content=&quot;article&quot; /&gt;
&lt;meta property=&quot;article:published_time&quot; content=&quot;PUBLISHED DATETIME&quot; /&gt;
</code></pre><p>If you have control of your site&apos;s markup and want better previews of your articles on the various social networks then you should add this markup to your web site <a href="#fn-1" id="fnref1"><sup>1</sup></a>. Hopefully this article has saved you from having a full screen pop-over prompt you to join yet another mailing list.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>You can actually remove the <code>twitter:title</code>, <code>twitter:description</code>, and <code>twitter:image</code> lines since Twitter will fallback to the equivalent Open Graph markup if they missing.<a href="#fnref1">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2016/09/28/better-command-history-in-your-shell/index.html</id>
    <link href="https://jakemccrary.com/blog/2016/09/28/better-command-history-in-your-shell/index.html"/>
    <title><![CDATA[Better command history in your shell]]></title>
    <updated>2016-09-28T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>My ideal command history would let me search the history of every shell but when I hit the up arrow it would only cycle through my current shell&apos;s history. In February, I was able to achieve this setup in large part because of a utility called <a href="https://github.com/dvorka/hstr">hstr</a>.</p><h2 id="what-is-hstr?">What is <code>hstr</code>?</h2><p>hstr is a neat Bash and Zsh utility that lets you easily search, view, and manage your command history. hstr provides a tool named <code>hh</code> that provides a text interface for manipulating your command history. To see what it looks like check out the <a href="https://github.com/dvorka/hstr/blob/master/README.md">README</a> and this <a href="https://www.youtube.com/watch?v=sPF29NyXe2U">video</a> tutorial. If you are running OS X and use Homebrew you can install it by running <code>brew install hh</code>.</p><h2 id="making-global-history-searchable-but-arrows-cycle-through-local-history">Making global history searchable but arrows cycle through local history</h2><p>hstr is a neat tool but my favorite part of my setup is how the global command history is searchable but only a shell&apos;s local history is cycled through with the arrow keys. This is achieved by manipulating where history is written and tweaking some environment variables.</p><p>The first step is to change your <code>$PROMPT_COMMAND</code> to append your shell&apos;s history to a global history file. Below is the snippet that does this from my <code>.bashrc</code> file.</p><pre><code># Whenever a command is executed, write it to a global history
PROMPT_COMMAND=&quot;history -a ~/.bash_history.global; $PROMPT_COMMAND&quot;
</code></pre><p>The next step is to bind a keystroke to run <code>hh</code>, which is what hstr provides, with <code>$HISTFILE</code> pointing to <code>~/.bash_history.global</code>. I wanted to fully replace the default command history searching (and I use Emacs style keyboard shortcuts) so I&apos;ve bound these actions to ctrl-r.</p><pre><code class="language-bash"># On C-r set HISTFILE and run hh
bind -x &apos;&quot;\C-r&quot;: &quot;HISTFILE=~/.bash_history.global hh&quot;&apos;
</code></pre><p>With those two additions to my <code>.bashrc</code> I&apos;ve achieved my ideal command history searching. When I hit ctrl-r I&apos;m searching all of my history and yet I only cycle through a shell&apos;s local history with the arrow keys. This small addition<a href="#fn-1" id="fnref1"><sup>1</sup></a> made my command line productivity higher.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>My setup was inspired by <a href="https://unix.stackexchange.com/questions/200225/search-history-from-multiple-bash-session-only-when-ctrl-r-is-used-not-when-a">this</a> StackExchange post.<a href="#fnref1">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2016/08/28/better-code-reloading-in-a-cloure-web-server/index.html</id>
    <link href="https://jakemccrary.com/blog/2016/08/28/better-code-reloading-in-a-cloure-web-server/index.html"/>
    <title><![CDATA[Better code reloading in a Clojure web server]]></title>
    <updated>2016-08-28T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>A couple weeks ago I released <a href="https://github.com/jakemcc/reload">com.jakemccrary/reload</a>. This tiny library provides a ring middleware that uses <a href="https://github.com/clojure/tools.namespace">org.clojure/tools.namespace</a>  to reload changed Clojure code on incoming http requests.</p><p>This middleware was created because my team was running into problems using ring&apos;s <code>wrap-reload</code> middleware. Unfortunately these problems happened about nine months ago and, since I didn&apos;t write this post back then, I&apos;ve since forgotten these problems. Regardless, this project has been used since the beginning of this year and has helped make my team&apos;s development workflow smoother. If you are running into problems it might help you too.</p><h3 id="usage">Usage</h3><p>If you&apos;d like to give it a shot, then add the <a href="https://clojars.org/com.jakemccrary/reload">latest version</a> (at the time of writing <code>[com.jakemccrary/reload &quot;0.1.0&quot;]</code>) to your project.clj.</p><p>Require <code>com.jakemccrary.middleware.reload</code> and wrap your handler with <code>wrap-reload</code>.</p><pre><code class="language-clojure">(ns example
  (:require
   ;; more deps
   [com.jakemccrary.middleware.reload :as reload]))

;; wherever you are setting up your middleware stack
(reload/wrap-reload routes)
</code></pre><p><code>reload/wrap-reload</code> optionally takes a list of directories to monitor as a second parameter. By default it reloads the <code>src</code> directory.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2016/07/31/aws-elastic-beanstalk-send-a-sqs-message-to-a-specific-route-in-your-worker-environment/index.html</id>
    <link href="https://jakemccrary.com/blog/2016/07/31/aws-elastic-beanstalk-send-a-sqs-message-to-a-specific-route-in-your-worker-environment/index.html"/>
    <title><![CDATA[AWS Elastic Beanstalk: Send a SQS message to a specific route in your worker environment]]></title>
    <updated>2016-07-31T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p><a href="https://lumanu.com">Lumanu</a> uses <a href="https://aws.amazon.com/elasticbeanstalk/">AWS Elastic Beanstalk</a>. Elastic Beanstalk (from now on abbreviated as EB) helps you provision and tie together Amazon services to fairly easily get web applications and services running with push button (or command line) deploys. We&apos;ve been using EB with a multi-container docker deploy for nearly a year now and it pretty much just works.</p><p>EB has a concept of environment tiers and there are two different types; a web tier and a worker tier. Web tier environments provide the configuration and components necessary for serving HTTP requests in a scalable fashion. <a href="http://docs.aws.amazon.com/elasticbeanstalk/latest/dg/using-features-managing-env-tiers.html">Worker environments</a> are designed to run operations that you wouldn&apos;t want performed by your front-end serving web application.</p><p>A major difference between the two environment tiers is that a worker environment provisions a <a href="https://aws.amazon.com/sqs/">SQS</a> queue and provides a daemon that reads from this queue and POSTs messages to an instance of your worker service. This daemon prevents your worker service from having to connect to and manage a SQS queue. By default, the daemon POSTs messages to <code>http://localhost/</code>. You can optionally configure it to POST to a different route.</p><p>It is possible to have different messages POST to different routes. You can do this by setting the <code>beanstalk.sqsd.path</code> attribute on your SQS message. For example, if you want your worker service to receive a message at <code>/trigger-email</code> you would set the <code>beanstalk.sqsd.path</code> attribute to <code>/trigger-email</code>.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2016/06/28/seven-tips-for-successful-remote-meetings/index.html</id>
    <link href="https://jakemccrary.com/blog/2016/06/28/seven-tips-for-successful-remote-meetings/index.html"/>
    <title><![CDATA[7 tips for a successful remote meeting]]></title>
    <updated>2016-06-28T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><blockquote><p>See all of my remote/working-from-home <a href="/blog/categories/remote/">articles here</a>.</p></blockquote><p>As mentioned in my <a href="/blog/2016/06/14/tips-for-working-from-home/">tips for working from home</a> article, I&apos;ve been working remotely for nearly three years. In those three years I&apos;ve been in countless meetings, both productive and unproductive. Meetings, both in-person and remote, are hard. Remote meetings pose some additional challenges but they also offer some unique benefits.</p><p>Below are seven tips that will help you have successful remote meetings. I wrote this article focusing on remote meetings but many of these tips will improve your co-located meetings as well.</p><h3 id="have-an-agenda">Have an agenda</h3><p>If you are organizing a meeting, you should make sure that the meeting has an agenda. The agenda doesn&apos;t have to be a long, complicated document; it can be as simple as a list of topics and goals.</p><p>Why should you have an agenda? An agenda helps focus discussion by providing an outline of what the meeting is designed to cover.</p><p>Send out your agenda with the meeting invite. This gives invitees time to think about the topics and helps prevent people from showing up clueless. It also provides an opportunity for an invitee to excuse themself or suggest an alternative person if they don&apos;t believe they will contribute to the meeting.</p><h3 id="start-and-end-on-time">Start and end on time</h3><p>You should start your meetings on time. You should end your meetings on time or early. If you are not starting and ending on time then you are not being respectful of your attendees time. The lack of punctuality contributes towards people dreading meetings.</p><p>If you are running out of time, wind down the meeting. If more discussions need to happen, reflect and see if more time needs to be scheduled with the entire group or if only a subset of the group is required.</p><h3 id="use-video-chat">Use video chat</h3><p>Even if you don&apos;t work remotely, you&apos;ve probably had to dial in to a group audio chat. This is almost always a terrible experience. Without body language, it is near impossible to tell when someone is about to start speaking and, as a result, there are awkward pauses while everyone waits for someone else to speak and everyone speaks over each other. It is terrible.</p><p>This is why I recommend using video chat. Video chats let you see the other people on the call and this allows you to pick up on physical cues. These cues vastly improve communication in the meeting.</p><h3 id="co-located-attendees-should-use-their-own-device">Co-located attendees should use their own device</h3><p>Sometimes you&apos;ll have a mixed meeting, some attendees are remote and others are together in an office. The co-located attendees should each use their own device to connect to the meeting.</p><p>Co-located attendees sharing a single device is non-optimal for many reasons. It is often hard for all the co-located attendees to be captured by the camera in a way that enables the remote attendees to reliably view them. Sharing a single microphone also makes it so some co-located attendees are easy to hear and others are barely audible.</p><p>Using a single device also makes it harder for all the co-located attendees to view the remote attendees. Without a clear view of the remote attendees, the co-located attendees often accidentally exclude the remote people by focusing on discussions between the co-located group.</p><h3 id="ignore-distractions">Ignore distractions</h3><p>Hopefully you have invited just the right people to the meeting and everyone is engaged in the discussion and paying attention. Realistically this doesn&apos;t happen. Computers are incredibly good at so many things and one of those things is distracting the user.</p><p>When you are attending a remote meeting, minimize what can distract you. Close your email and hide the chat program. Put your phone out of arms reach. Try to focus intently on the meeting. If you find yourself not paying attention and not contributing, take this as a signal that you shouldn&apos;t be in this meeting. If nothing on the rest of the agenda seems like it requires you, then leave the meeting and be more selective about what you join in the future.</p><p>If you notice other attendees not paying attention, gently call them out on it. This can be done by soliciting discussion from them or by being direct.</p><h3 id="have-a-shared-artifact">Have a shared artifact</h3><p>This is one of the more important tips in this list and it is one of the areas where remote meetings have an advantage over in-person meetings.</p><p>When the meeting starts give everyone a link to a shared document that everyone can edit (for example a <a href="https://www.google.com/docs/about/">Google Doc</a>). It can be useful to seed this document with the agenda. This shared document can be used to capture whatever you want. I&apos;ve found it useful to capture options discussed, pros/cons lists, and follow-up actions. Writing in the shared document helps solidify ideas and gives the group a reference both during and after the meeting.</p><p>With in-person meetings, this shared artifact is often a whiteboard. Whiteboards are immensely useful but are barely editable by more than one person at once and are harder to reference after a meeting. I know I&apos;m not the only person who dislikes trying to decipher terrible whiteboard handwriting captured by someone&apos;s phone.</p><p>Except for when drawing diagrams, I&apos;ve found the Google Docs style shared document used during a remote meeting more effective than using a whiteboard in an in-person meeting. You can always use a shared document in an in-person meeting as well but then you are requiring attendees to have a laptop open and that is an invitation for distracted attendees.</p><h3 id="assign-responsibilities">Assign responsibilities</h3><p>Hopefully you are having a meeting to influence an outcome and not just hear everyone talk. As a result, you should be assigning follow-up responsibilities as the meeting progresses. Make the follow-up actions explicit and assigned to an individual. You can capture these responsibilities in your shared artifact.</p><hr /><p>Meetings can be difficult. You should do what you can to make them more successful. If you are being invited to a meeting without an agenda, ask for an agenda. If you&apos;re in a meeting and you can tell someone is constantly distracted, try nicely calling them out on it (either privately or in the group). If there isn&apos;t a shared artifact, make one and suggest it to the group. Meetings don&apos;t have to be terrible. We can make them better.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2016/06/20/my-recommended-clojure-testing-setup/index.html</id>
    <link href="https://jakemccrary.com/blog/2016/06/20/my-recommended-clojure-testing-setup/index.html"/>
    <title><![CDATA[My recommended Clojure testing setup]]></title>
    <updated>2016-06-20T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Occasionally, either on Stack Overflow or in the <a href="http://clojurians.net/">Clojurians</a> Slack group, someone will ask what tools they should use to test Clojure code. Below is what I would currently recommend. I&apos;ve come to this recommendation through observing teams using a variety of testing tools and through my own use them.</p><blockquote><p>Use clojure.test with <a href="https://github.com/pjstadig/humane-test-output">humane-test-output</a> and <a href="https://github.com/jakemcc/test-refresh">lein-test-refresh</a>.</p></blockquote><h3 id="use-clojure.test">Use clojure.test</h3><p>clojure.test is ubiquitous and not a big departure from other languages&apos; testing libraries. It has its warts but your team will be able to understand it quickly and will be able to write maintainable tests.</p><h3 id="use-humane-test-output">Use humane-test-output</h3><p>You should use clojure.test with <a href="https://github.com/pjstadig/humane-test-output">humane-test-output</a>. Together they provide a testing library that has minimal additional syntax and good test failure reporting.</p><h3 id="use-lein-test-refresh">Use lein-test-refresh</h3><p>If you&apos;re not using a tool that reloads and reruns your tests on file changes then you are wasting your time. The delay between changing code and seeing test results is drastically reduced by using a tool like <a href="https://github.com/jakemcc/test-refresh">lein-test-refresh</a>. Nearly everyone I know who tries adding lein-test-refresh to their testing toolbox continues to use it. Many of these converts were not newcomers to Clojure either, they had years of experience and had already developed workflows that worked for them.</p><h3 id="use-lein-test-refresh&apos;s-advanced-features">Use lein-test-refresh&apos;s advanced features</h3><p><a href="https://github.com/jakemcc/test-refresh">lein-test-refresh</a> makes development better even if you don&apos;t change any of its settings. It gets even better if you use some of its advanced features.</p><p>Below is a stripped down version of my <code>~/.lein/profiles.clj</code>. The <code>:test-refresh</code> key points towards my recommended lein-test-refresh settings.</p><pre><code class="language-clojure">{:user {:dependencies [[pjstadig/humane-test-output &quot;0.8.0&quot;]]
        :injections [(require &apos;pjstadig.humane-test-output)
                     (pjstadig.humane-test-output/activate!)]
        :plugins [[com.jakemccrary/lein-test-refresh &quot;0.16.0&quot;]]
        :test-refresh {:notify-command [&quot;terminal-notifier&quot; &quot;-title&quot; &quot;Tests&quot; &quot;-message&quot;]
                       :quiet true
                       :changes-only true}}}
</code></pre><p>These settings turn on notifications when my tests finish running (<code>:notify-command</code> setting), make clojure.test&apos;s output less verbose (<code>:quiet true</code>), and only run tests in namespaces affected by the previous code change (<code>:changes-only true</code>). These three settings give me the quickest feedback possible and free me from having the terminal running <code>lein test-refresh</code> visible.</p><p>Quick feedback lets you make changes faster. If you&apos;re going to write tests, and you should write tests, having them run quickly is powerful. After years of writing Clojure, this is my current go-to for testing Clojure code and getting extremely fast feedback.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2016/06/14/tips-for-working-from-home/index.html</id>
    <link href="https://jakemccrary.com/blog/2016/06/14/tips-for-working-from-home/index.html"/>
    <title><![CDATA[Tips for working from home]]></title>
    <updated>2016-06-14T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><blockquote><p>See all of my remote/working-from-home <a href="/blog/categories/remote/">articles here</a>.</p></blockquote><p>I&apos;ve been working remotely since October 2013. I can barely believe that nearly three years have passed and I&apos;ve probably spent about two weeks in a traditional office.</p><p>It took me a bit of time to adjust to working remotely. I&apos;m better at it now than I was three years ago. The rest of this post describes some of my learnings.</p><p>But first, some background. My remote experience comes from working at <a href="http://outpace.com">Outpace</a> and <a href="https://lumanu.com">Lumanu</a>. Both are remote first companies, almost everyone works remotely with a few people occasionally working in a San Francisco office. The work is remote but it is not asynchronous. Both companies value pair programming and real time collaboration and, as a result, employees tend to work a core set of hours. My observations are probably most applicable in a similar environment.</p><h3 id="setup-a-home-workspace">Setup a home workspace</h3><p>Before working remotely I did not have a great home computing setup. I worked using a 13-inch MacBook Air either sitting on my couch or at my dinner table. This worked fine for the occasional work-from-home day and for evening and weekend programming. It didn&apos;t work fine when I was spending extended hours at a computer every day.</p><p>I&apos;ve written about my setup <a href="/blog/2015/03/31/my-home-work-space/">before</a>. Below is a list of the improvements I made to my home working environment.</p><ol><li>Outpace provided a beefy MacBook Pro and two 27-inch Apple Cinema displays.</li><li>I upgraded my chair to to a <a href="https://www.amazon.com/Setu-Chair-Herman-Miller-Standard/dp/B004VSL6E6">Herman Miller Setu</a>.</li><li>I bought a <a href="http://www.amazon.com/gp/product/B000OOYECC/">mStand Laptop Stand</a> to raise my laptop to a better viewing height.</li><li>I upgraded my desk to a sit-stand desk.</li><li>I built my own <a href="/blog/2014/07/27/building-the-ergodox-keyboard/">ErgoDox keyboard</a>.</li><li>I switched to an adjustable monitor arm so I could adjust my monitor height.</li></ol><p>With each change my working experience improved. Each improvement left me feeling better at the end of my day. Many, if not all, of these improvements are things I&apos;d expect to have in a traditional office. Don&apos;t skimp on your setup because it is in your home. Part of your home is your office now.</p><h3 id="introduce-a-habit-that-delineates-work-from-after-work">Introduce a habit that delineates work from after-work</h3><p>One of the great things about working from home is that you no longer have a commute. You don&apos;t have to dodge cars on your bicycle, squeeze into a train, or sit in traffic while driving. This is a huge benefit.</p><p>A downside of not having a commute is that you lose a forced intermission between work and non-work. My commute was either a 30-minute bicycle ride or a 30-minute public transit ride. That time acted as a forced decompression period where I focused on something that wasn&apos;t computing.</p><p>It took me months to realize that not having this intermission was stressing me out. The intermission helped me shift between working and non-working mindsets.</p><p>I reintroduced a decompression activity between work and non-work and became less stressed and generally happier. I&apos;ve replaced my commute intermission with reading, cooking, or riding my bicycle. I&apos;ve found doing a non-computer activity benefits me the most.</p><h3 id="stop-working">Stop working</h3><p>When I first started working from home I was very guilty of overworking. It was so easy to just keep working. I would get invested in a problem and all sudden realize it was time to go to bed. Or I&apos;d actually stop working, only to find myself checking our application&apos;s monitoring or pulling up the codebase when I originally sat down to do some personal task.</p><p>In an office you have signals; your coworkers leaving, the cleaning crew vacuuming, air conditioning turning off, etc., that provide a hint that you should think about stopping. You don&apos;t have these signals when you are working remotely. There also isn&apos;t that spatial boundary between your office and your home.</p><p>You can search online and find many articles about how overwork is detrimental. You and your employer benefit if you are <strong>not</strong> overworked.</p><h3 id="get-out-of-your-house">Get out of your house</h3><p>I live in Chicago. During the winter the weather is very cold. Chicago is also a big city, so all sorts of food delivery options (both cooked and uncooked) exist. The cold weather combined with food delivery makes it easy to stay inside. During the winter, I&apos;ve realized many times that I haven&apos;t left my apartment for days. My girlfriend can tell when I haven&apos;t gotten outside because I&apos;m grumpier.</p><p>If I get out of the apartment for a while I almost always come back feeling better. It barely matters what I do when I leave, after an extended period of time inside of my home just getting outside and doing anything helps. A change of scenery is good for you.</p><h3 id="don&apos;t-just-talk-about-business-with-your-remote-coworkers">Don&apos;t just talk about business with your remote coworkers</h3><p>When you work in an office you are pretty much forced to have non-work related chats with coworkers. You should do the same with your remote team.</p><p>Having non-work related conversations helps you make better connections with your coworkers. These better connections can lead to better communication, both with voice and text, and humanize the person on the other side of the screen.</p><p>Its even better if you can do this in a video conference. Then you get to learn the facial expressions and tone of voice of your coworker. This can make it easier to interpret text communication in the way they actually mean it.</p><h3 id="meet-in-person-occasionally">Meet in person occasionally</h3><p>It is great that technology and Internet speeds have progressed enough that working remotely works well. If you want, you can make it feel like your pair is sitting right next to you. This is great.</p><p>There are still benefits to meeting in person though. The main one is that it helps you make connections with coworkers. You can eat a meal together or play board games (though, you can do this online as well but it is a different experience). It can also be easier to have certain types of group discussions (video conferences do have limitations).</p><p>When you meet in person, I&apos;d recommend doing something different than your normal day-to-day work. Don&apos;t just exchange remote pairing for local pairing. Try to identify that are difficult to do remotely and do them in person.</p><p>I don&apos;t have a concrete recommendation for how often your remote company should meet but I think it should be infrequent enough where you don&apos;t feel pressure to do normal work.</p><h2 id="end">End</h2><p>Working from home has its challenges but with those challenges come many benefits. It is a different experience than working in an office and that experience isn&apos;t for everyone. The above recommendations are things that have helped me adjust to working remotely. Some of these tips are actionable at an individual level and some require buy in from the company. Hopefully this list can help give guidance towards improving your remote work situation.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2016/05/14/use-google-to-get-a-sites-favicon/index.html</id>
    <link href="https://jakemccrary.com/blog/2016/05/14/use-google-to-get-a-sites-favicon/index.html"/>
    <title><![CDATA[Use Google to get a site's favicon]]></title>
    <updated>2016-05-14T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>A few months ago I was implementing some changes to <a href="https://lumanu.com">Lumanu&apos;s</a> user interface. Lumanu is a tool I&apos;ve been working on that helps its users create, discover, and curate engaging content.</p><p>This interface change was to our discovery view. This is the view that surfaces interesting content to our users. The change involved showing the favicon of content&apos;s origin in our interface.</p><p>I often browse the Internet with the network tab of the Chrome Developer Tools open. I do this because I find it interesting to see what services other web applications are using. I had the network tab open while browsing a site that displayed many favicons and noticed a lot fetches from google.com. This surprised me, so I took a deeper look at the requests and saw they were hitting a URL that appeared to provide favicons. It turns out you can query Google for favicons.</p><h3 id="example">Example</h3><p>Let&apos;s pretend we want to get the favicon for <code>jakemccrary.com</code>. You simply construct a URL that looks like <a href="https://www.google.com/s2/favicons?domain=jakemccrary.com"><code>https://www.google.com/s2/favicons?domain=jakemccrary.com</code></a> and all of a sudden you have the favicon. Just replace <code>jakemccrary.com</code> with the domain you care about and you&apos;ll be rewarded with that domain&apos;s favicon.</p><p><img alt="My favicon from Google" src="https://www.google.com/s2/favicons?domain=jakemccrary.com" title="Favicon from Google" /></p><p>This definitely isn&apos;t a new feature. If you search online you&apos;ll see people talking about it years ago. I had never heard of it before and discovering it saved us an unknown amount of time. It allowed us to iterate on our interface without having to figure out the nuances of favicons. We were able to quickly try out the interface change and then throw it away without costing us too much time.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2016/04/30/speeding-up-my-blog/index.html</id>
    <link href="https://jakemccrary.com/blog/2016/04/30/speeding-up-my-blog/index.html"/>
    <title><![CDATA[Speeding up my blog]]></title>
    <updated>2016-04-30T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>I was recently reading <a href="https://jefframnani.com/about/">Jeff Ramnani&apos;s</a> <em>about</em> page and I was somewhat inspired by it. It loads quickly and links to <a href="http://mattgemmell.com/designing-blogs-for-readers/">Designing blogs for readers</a>, an interesting essay by Matt Gemmmell. Reading that essay inspired me to think about my own site and what experience I want to deliver to readers.</p><p>I can&apos;t imagine what every reader wants but I know what I want to experience when I read an article online. Reading high quality content is my highest priority. Beyond that I enjoy when a page loads fast and the visual design doesn&apos;t get in the way. I think a great example of these two requirements is <a href="http://zenhabits.net/falling/">zen habits</a> (along with Jeff Ramnani&apos;s and Matt Gemmell&apos;s).</p><p>My own site sort of achieves those goals. I like to think I&apos;m writing well-written content that helps others. I know it has helped me. With regards to visual design I think there is room for improvement. I don&apos;t think my site&apos;s design is actively distracting from the content though, so I&apos;ve decided to focus on improving the page load time first.</p><h2 id="the-optimization-process">The optimization process</h2><p>As with any optimization problem it is important figure what you&apos;re going to measure, how you&apos;re going to measure it and your starting point. I decided to focus on my page load time, as measured by <a href="http://www.webpagetest.org">Web Page Test</a>. I used Google&apos;s <a href="https://developers.google.com/speed/pagespeed/insights/">PageSpeed Insights</a> to score and provide helpful tips for improving page speed. Unfortunately I didn&apos;t capture my starting point with PageSpeed Insights but I think I was scoring around a 66/100 for mobile and 79/100 for desktop.</p><p><img alt="Starting point from Web Page Test" src="/images/before-optimizations.png" /></p><p>As measured by Web Page Test, the first load of my main page took five seconds and it wasn&apos;t fully loaded for another second. This is ridiculous. My page is almost entirely static content and most of my assets are served from CloudFlare. It should be blazing fast.</p><p>Next I looked at what was actually being loaded. Google&apos;s PageSpeed Insights identified that I had three render-blocking <code>script</code> tags. The offending scripts were Modernizr, jQuery, and octopress.js. PageSpeed Insights <a href="https://developers.google.com/speed/docs/insights/BlockingJS#overview">recommends</a> inlining JavaScript required to render the page or make loading asynchronous. I decided to go a step further and remove the need for the JavaScript.</p><h3 id="removing-octopress.js">Removing octopress.js</h3><p>It turns out <code>octopress.js</code> was the reason Modernizr and jQuery were required. Most of what <a href="https://github.com/jakemcc/jakemccrary.com/blob/c27a131aef437181dcab9552c3241f8adafb3884/source/javascripts/octopress.js">octopress.js</a> did were things that I don&apos;t need; some sort of <a href="https://github.com/jakemcc/jakemccrary.com/blob/c27a131aef437181dcab9552c3241f8adafb3884/source/javascripts/octopress.js#L111">flash video fallback</a>, adding line numbers to <a href="https://github.com/jakemcc/jakemccrary.com/blob/c27a131aef437181dcab9552c3241f8adafb3884/source/javascripts/octopress.js#L112">GitHub Gists</a>, <a href="https://github.com/jakemcc/jakemccrary.com/blob/c27a131aef437181dcab9552c3241f8adafb3884/source/javascripts/octopress.js#L99">rendering</a> delicious links, and toggling the <a href="https://github.com/jakemcc/jakemccrary.com/blob/c27a131aef437181dcab9552c3241f8adafb3884/source/javascripts/octopress.js#L114">sidebar visibility</a>. I was able to delete all that code.</p><p>Next up was the <a href="https://github.com/jakemcc/jakemccrary.com/blob/c27a131aef437181dcab9552c3241f8adafb3884/source/javascripts/octopress.js#L1-L13">mobile navigation</a> <code>octopress.js</code> provided. This feature enabled navigation through a <code>&lt;select&gt;</code> element when the reader&apos;s view port was tiny. Restyling my navigation bar to fit better on small screens allowed me to remove this feature. <code>ocotpress.js</code> also did some <a href="https://github.com/jakemcc/jakemccrary.com/blob/c27a131aef437181dcab9552c3241f8adafb3884/source/javascripts/octopress.js#L37">feature detection</a> for Modernizr. I stopped using image masks and was able to remove that code as well.</p><p>The remaining code in <code>octopress.js</code> was a workaround for an <a href="https://github.com/jakemcc/jakemccrary.com/blob/c27a131aef437181dcab9552c3241f8adafb3884/source/javascripts/octopress.js#L121-L136">iOS scaling bug</a>. This JavaScript was inlined into my html. At this point <code>octopress.js</code> was empty and with it empty the requirements for jQuery and Modernizer disappeared. This let me remove three render-blocking <code>script</code> tags.</p><h3 id="remaining-javascript">Remaining JavaScript</h3><p>At this point the remaining JavaScript used for my blog was enabling comments with Disqus and showing recent tweets in my sidebar. I still enjoy having comments on my blog so I&apos;m keeping Disqus around. I doubt that readers care what my most recent tweets are so I removed Twitter&apos;s JavaScript. Removing my tweets also cleans up my sidebar and helps keep the focus on my writing.</p><h3 id="nearly-no-javascript,-now-what?">Nearly no JavaScript, now what?</h3><p>At this point Google&apos;s PageSpeed Insight was suggesting that I up my cache times, inline my css, and move my web fonts lower on my page. Bumping up my cache times was trivial; I simply tweaked a CloudFlare setting.</p><p>I opted to not inline my css. This would require me to modify my site&apos;s generation and I just didn&apos;t feel like diving down that rabbit hole. I also didn&apos;t move the web fonts lower on the page. I find fonts re-rendering jarring and as a result kept them loading<a href="#fn-0" id="fnref0"><sup>0</sup></a> in my <code>&lt;head&gt;</code>.</p><h2 id="the-results">The results</h2><p>I used Web Page Test to measure again and now the page load time is down to 2.5 seconds. Page load times are cut in half from the starting point. My PageSpeed Insights scores are also higher; up to 79/100 for mobile and 92/100 for desktop.</p><p><img alt="Web Page Test after optimization" src="/images/after-optimizations.png" /></p><p>Honestly, that number still seems high<a href="#fn-1" id="fnref1"><sup>1</sup></a> to me and I&apos;m sure I could get it lower. But for now it is good enough<a href="#fn-2" id="fnref2"><sup>2</sup></a>. As a result of doing this I&apos;ve learned more about my blogging setup and managed to speed up my page load. Now it is time to focus on researching for future posts (and at some point restyling).</p><h2 id="update-on-2016-05-03">Update on 2016-05-03</h2><p>I completely removed web font loading from my site. Getting rid of the fonts reduced my load time, as measured by Web Page Test, by a second. Google&apos;s PageSpeed Insights now scores this site at 90/100 for mobile and 96/100 for desktop.</p><p><img alt="Web Page Test after font removal" src="/images/after-optimizations2.png" /></p><ol class="footnotes"><li class="footnote" id="fn-0"><p>When I first wrote this I didn&apos;t change anything about my web fonts. After thinking about it for a few days I ended up removing them completely. Details are in the update at the end of the post.<a href="#fnref0">↩</a></p></li><li class="footnote" id="fn-1"><p>I&apos;m asking Web Page Test to load my page using IE10. I get much faster load times using Chrome or Firefox locally which is what most of my readers use. This is good enough for now.<a href="#fnref1">↩</a></p></li><li class="footnote" id="fn-2"><p>I mean, the starting point was probably good enough but if I admitted that then I wouldn&apos;t have had the excuse to dig into my site&apos;s load time.<a href="#fnref2">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2016/04/10/the-usefulness-of-clojures-cond-arrow/index.html</id>
    <link href="https://jakemccrary.com/blog/2016/04/10/the-usefulness-of-clojures-cond-arrow/index.html"/>
    <title><![CDATA[The usefulness of Clojure's cond->]]></title>
    <updated>2016-04-10T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Clojure&apos;s <a href="https://clojure.github.io/clojure/clojure.core-api.html#clojure.core/cond-%3E"><code>cond-&gt;</code></a> (and <code>cond-&gt;&gt;</code>) is a versatile macro. It isn&apos;t a new macro, it has been around since version 1.5, but I finally discovered and started using it sometime last year. It isn&apos;t a workhorse macro, you won&apos;t be using it everyday, but it comes in handy.</p><h3 id="what-is-cond-&gt;?">What is <code>cond-&gt;</code>?</h3><p>Let&apos;s start by looking at the docstring.</p><pre><code>Usage: (cond-&gt; expr &amp; clauses)

Takes an expression and a set of test/form pairs. Threads expr (via -&gt;)
through each form for which the corresponding test
expression is true. Note that, unlike cond branching, cond-&gt; threading does
not short circuit after the first true test expression.
</code></pre><p>So what does the docstring mean? Let&apos;s break it down with an example.</p><pre><code class="language-clojure">(cond-&gt; 10
  false inc)
=&gt; 10
</code></pre><p>In the above example <code>10</code> is the <code>expr</code> mentioned in the docstring and everything after it are the <code>clauses</code>. Each clause is a pair made up of a test and a form. In this example there is a single clause with the value <code>false</code> as the test the function <code>inc</code> as the form. Since the test evaluates to a false value the expression is not threaded into the form. As a result the original expression, <code>10</code>, is returned.</p><p>Let&apos;s look at an example with a truthy test.</p><pre><code class="language-clojure">(cond-&gt; 10
  true (- 2)
=&gt; 8
</code></pre><p>Once again, <code>10</code> is the starting expression. The single clause has a test that evaluates to true so the expression is threaded into the first position of the form <code>(- 2)</code>. The result is <code>8</code> and this is returned.</p><p>Next is an example of a <code>cond-&gt;</code> with multiple clauses. Explanations are inline with the code.</p><pre><code class="language-clojure">(cond-&gt; 10 ; start with 10
  ;; test evaluates to true, so apply inc to 10. Current value is now 11.
  true inc

  ;; (zero? 1) evaluates to false, do not perform action. Current value stays 11.
  (zero? 1) (+ 2)

  ;; (pos? 4) evaluates to true, thread 11 into first position of form.
  (pos? 4) (- 5))
=&gt; 6 ; The result of (- 11 5) is 6.
</code></pre><p>If you understand the above example then you have a good grasp of <code>cond-&gt;</code>. But when is this functionality useful?</p><h3 id="when-do-i-use-cond-&gt;?">When do I use cond-&gt;?</h3><p>Looking through the codebases I work on, I almost primarily see <code>cond-&gt;</code> being used with the initial expression being a hash-map. It is being used in situations where we want to selectively <code>assoc</code>, <code>update</code>, or <code>dissoc</code> something from a map.</p><p>If <code>cond-&gt;</code> did not exist you would accomplish those selective modifications with code similar to below.</p><pre><code class="language-clojure">(if (some-pred? q)
  (assoc m :a-key :a-value)
  m)
</code></pre><p>You can rewrite the above with <code>cond-&gt;</code>.</p><pre><code class="language-clojure">(cond-&gt; m
  (some-pred? q) (assoc :a-key :a-value))
</code></pre><p>If you&apos;re not used to seeing <code>cond-&gt;</code> the above transformation might seem like a step backwards. I know it felt that way to me when I first saw <code>cond-&gt;</code>. Give yourself time to get familiar with it and you&apos;ll be glad you&apos;re using it.</p><p>A meatier example of using <code>cond-&gt;</code> is demonstrated below. Here we&apos;re manipulating data structures designed for use with <a href="https://github.com/jkk/honeysql">honeysql</a> to generate SQL statements. We start with a <code>base-query</code> and selectively modify it based on incoming parameters.</p><pre><code class="language-clojure">(defn query [req-params]
  (let [and-clause (fnil conj [:and])
        base-query {:select [:name :job]
                    :from [:person]}]
    (cond-&gt; base-query
      (:job req-params) (update :where and-clause [:= :job (:job req-params)])
      (:name req-params) (update :where and-clause [:= :name (:name req-params)])
      (:min-age req-params) (update :where and-clause [:&gt; :age (:min-age req-params)]))))
</code></pre><p>Hopefully this gives you a taste of <code>cond-&gt;</code>. I&apos;ve found it to be quite useful. It has a place in every Clojure developer&apos;s toolbox.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2016/04/08/book-review-serverless-single-page-apps/index.html</id>
    <link href="https://jakemccrary.com/blog/2016/04/08/book-review-serverless-single-page-apps/index.html"/>
    <title><![CDATA[Book review: Serverless Single Page Apps]]></title>
    <updated>2016-04-08T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>I&apos;ve read Ben Rady&apos;s <a href="https://pragprog.com/book/brapps/serverless-single-page-apps">Serverless Single Page Apps</a> twice now. As an early technical reviewer, I was able to watch and take part in the book&apos;s evolution. The early draft was good but the most recent near-final draft was better.</p><p><em>Serverless Single Page Apps</em> walks you through building a low-cost, highly-available, serverless single page web application. It does this on top of various Amazon web services (DynamoDB, Cognito, Lambda, API Gateway, S3). If you follow along you&apos;ll end up with a simple web application with authentication.</p><p>The book is very enjoyable. The examples are clear and the book is well written. The book uses JavaScript to implement the serverless application. For the user interface it uses plain JavaScript with a bit of jQuery and for the AWS Lambda functions you dip into some Node.js. <em>Serverless</em> doesn&apos;t distract you from learning about serverless applications by forcing you to learn new JavaScript frameworks or libraries.</p><p>One of my favorite parts of the book is Ben&apos;s use of test driven development. The examples provided give the reader a decent taste of the benefits of test-first development. Having the tests helped me when I made some silly mistakes in later parts of the book.</p><p>Overall I&apos;d recommend this book to developers who are interested in learning what a serverless application might look like. If you follow along you&apos;ll know how to build one by the end and will have a good starting point for diving deeper into the topic.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2016/03/13/reading-in-2015/index.html</id>
    <link href="https://jakemccrary.com/blog/2016/03/13/reading-in-2015/index.html"/>
    <title><![CDATA[Reading in 2015]]></title>
    <updated>2016-03-13T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>At the beginning of the year I generally take the time to reflect on my reading in the previous year. I&apos;m nearly three months but I&apos;m finally taking a look at 2015. Here are my summaries of my <a href="http://jakemccrary.com/blog/2015/01/08/reading-in-2014/">2014</a> and <a href="http://jakemccrary.com/blog/2014/01/01/using-incanter-to-review-my-2013-reading/">2013</a> reading.</p><p>I&apos;ve continued to keep track of my reading by using <a href="http://goodreads.com">Goodreads</a>. My <a href="https://www.goodreads.com/user/show/3431614-jake-mccrary">profile</a> contains the full list and reviews of books I&apos;ve read since 2010. <a href="https://www.goodreads.com/review/list/3431614-jake-mccrary?read_at=2015">Here</a> is my 2015 list.</p><h3 id="2015-goals">2015 Goals</h3><p>2015 did not have an easily measured goal. I set the vague goal of increasing the quality of my reading by attempting to think deeper about what I&apos;ve read.</p><h3 id="2015-results">2015 Results</h3><p>I have no idea if I achieved my goal. Some books have stuck with me and I&apos;ve thought quite a bit about the stories. Others I&apos;ve forgotten already.</p><p>Looking at raw numbers I read 51 books in 2015 for a total of about 21,790 pages. When compared to 2014 these numbers are lower by 19 books and about 1300 pages.</p><p>In terms of star ratings, 2015 was a better year. I had three more five star books and one more four star book. The 19 book difference between 2014 and 2015 is entirely found in two and three star books.</p><h3 id="recommendations">Recommendations</h3><p>I awarded ten books a five star rating. This is more five stars than any other year. Each of the five star books I&apos;d recommend without hesitation. Below is my list of five star books. The <em>my review</em> text links to Goodreads.</p><ul><li>David Foster Wallace - The Pale King (<a href="https://www.goodreads.com/review/show/882190236">my review</a>)</li><li>Donna Tartt - The Goldfinch (<a href="https://www.goodreads.com/review/show/1168264855">my review</a>)</li><li>Donna Tartt - The Secret History (<a href="https://www.goodreads.com/review/show/1225842414">my review</a>)</li><li>James Clavell - Shogun (<a href="https://www.goodreads.com/review/show/1284160882">my review</a>)</li><li>John Williams - Stoner (<a href="https://www.goodreads.com/review/show/1152858937">my review</a>)</li><li>Michael L. Anderson - The Rock Climber&apos;s Training Manual (<a href="https://www.goodreads.com/review/show/1420673577">my review</a>)</li><li>Neal Stephenson - Cryptonomicon (<a href="https://www.goodreads.com/review/show/799153387">my review</a>)</li><li>Neal Stephenson - Reamde (<a href="https://www.goodreads.com/review/show/927872107">my review</a>)</li><li>Neal Stephenson - Snow Crash (<a href="https://www.goodreads.com/review/show/799153414">my review</a>)</li><li>Sam Newman - Building Microservices (<a href="https://www.goodreads.com/review/show/1202873191">my review</a>)</li></ul><p>One of the great things about writing this post is that it forces me to pause and reflect on the previous years books. Its great seeing this list of great books and remembering the stories. Of these ten books the ones I remember most fondly are <em>Stoner</em>, <em>Snow Crash</em>, and <em>The Pale King</em>.</p><p>There were also a ton of great four star books this year. One that stands out is Joseph Heller&apos;s <a href="https://www.amazon.com/SOMETHING-HAPPENED-Joseph-Heller-ebook/dp/B005IQZ894">Something Happened</a> (<a href="https://www.goodreads.com/review/show/189671091">my review</a>). Kurt Vonnegut wrote a brilliant <a href="http://www.nytimes.com/books/98/02/15/home/heller-something.html">review</a> of this book which I encourage you to read.</p><p>Dave MacLeod&apos;s <a href="https://www.amazon.com/Make-Break-Climbing-Injuries-Dictate/dp/0956428134">Make or Break: Don&apos;t Let Climbing Injuries Dictate Your Success</a> (<a href="https://www.goodreads.com/review/show/1236422760">my review</a>) deserves a mention. I highly recommend this book to any climber. We push our bodies hard and this book will help you prevent and recover from injuries. I&apos;ve used it as a reference so many times over the past year. It probably deserves five stars.</p><h3 id="other-stats">Other Stats</h3><p>Unsurprisingly, I&apos;m continuing to mostly read ebooks.</p><pre><code>|           | 2014 | 2015 |
|-----------+------+------|
| ebook     |   64 |   47 |
| hardcover |    1 |    1 |
| paperback |    4 |    3 |
</code></pre><p>My average rating went up.</p><pre><code>| Year | Average Rating |
|------+----------------|
| 2011 |           3.84 |
| 2012 |           3.66 |
| 2013 |           3.67 |
| 2014 |           3.48 |
| 2015 |           3.86 |
</code></pre><p>Last year I had many repeat authors. This year I had fewer. Neal Stephenson and Donna Tart really stood out this year. I read multiple books from both of them and rated every book five stars.</p><pre><code>| Author               | My Average Rating | Number of Pages | Number of books |
|----------------------+-------------------+-----------------+-----------------|
| Neal Stephenson      |                 5 |            2693 |               3 |
| Donna Tartt          |                 5 |            1427 |               2 |
| Paolo Bacigalupi     |       3.666666667 |            1113 |               3 |
| Bret Easton Ellis    |               3.5 |             590 |               2 |
</code></pre><h3 id="2016-goals">2016 Goals</h3><p>In 2016 I&apos;m planning on reading one or two biographies. That isn&apos;t a genre I typically read. It should be a pretty easy goal to hit. If you have any recommendations please leave them in a comment.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2015/12/19/clojurescript-treat-warnings-as-errors/index.html</id>
    <link href="https://jakemccrary.com/blog/2015/12/19/clojurescript-treat-warnings-as-errors/index.html"/>
    <title><![CDATA[ClojureScript: Treat warnings as errors]]></title>
    <updated>2015-12-19T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Recently my team deployed a new version of our ClojureScript UI and it had a minor bug. It was trivial to fix the problem, a ClojureScript build warning pointed us to the cause. As a result we started thinking it would be nice to have build warnings count as errors and fail our ClojureScript build.</p><p>We use <a href="http://leiningen.org/">Leiningen</a> (version 2.5.3) and <a href="https://github.com/emezeske/lein-cljsbuild">lein-cljsbuild</a> (version 1.1.1). After some searching we found that lein-cljsbuild supports <a href="https://github.com/emezeske/lein-cljsbuild#custom-warning-handlers">specifying custom warning handlers</a> as the value to the <code>:warning-handlers</code> key. The lein-cljsbuild README even provides an example, which we took and added a <code>(System/exit 1)</code> to the end of it. This resulted in a build configuration that looked similar to below.</p><pre><code class="language-clojure">{:id &quot;prod&quot;
 :warning-handlers [(fn [warning-type env extra]
                      (when-let [s (cljs.analyzer/error-message warning-type extra)]
                        (binding [*out* *err*]
                          (println &quot;WARNING:&quot; (cljs.analyzer/message env s)))
                        (System/exit 1)))]
 :source-paths [&quot;src/cljc&quot; &quot;src/cljs&quot;]
 :compiler {:output-to &quot;resources/public/js/compiled/ui.js&quot;
            :externs [&quot;resources/intercom-externs.js&quot;
                      &quot;resources/mixpanel-externs.js&quot;]
            :optimizations :advanced}}
</code></pre><p>This worked! Well, it sort of worked. Our build failed whenever there was a warning but now we were seeing spurious warnings. We saw &quot;Use of undeclared Var&quot; warnings when functions created in a <code>letfn</code> where calling each other. Definitely not a situation that warrants a warning and definitely not a build failure.</p><p>We weren&apos;t seeing this warning before so we opened ClojureScript&apos;s source and found the <a href="https://github.com/clojure/clojurescript/blob/452edf43927566cc0ea0a3846706c0294cef235d/src/main/clojure/cljs/analyzer.cljc#L360-L366">default warning handler</a>. The default handler checks that <code>warning-type</code> has a truthy value in the map <code>*cljs-warnings*</code>. Inspired by the default handler we added the same check to the start of our warning handler.</p><pre><code class="language-clojure">:warning-handlers [(fn [warning-type env extra]
                     (when (warning-type cljs.analyzer/*cljs-warnings*)
                       (when-let [s (cljs.analyzer/error-message warning-type extra)]
                         (binding [*out* *err*]
                           (println &quot;WARNING:&quot; (cljs.analyzer/message env s)))
                         (System/exit 1))))]
</code></pre><p>Success! Now we no longer get incorrect warnings when compiling our <code>letfn</code> form and our build still fails if a warning occurs. Now we can build and deploy with a little more confidence.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2015/12/18/even-quicker-feedback-from-your-clojure-tests/index.html</id>
    <link href="https://jakemccrary.com/blog/2015/12/18/even-quicker-feedback-from-your-clojure-tests/index.html"/>
    <title><![CDATA[Even quicker feedback from your Clojure tests]]></title>
    <updated>2015-12-18T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>I was recently inspired by a post on a mailing list to make the TDD cycle with <code>clojure.test</code> and <a href="https://github.com/jakemcc/test-refresh">lein-test-refresh</a> even faster. <code>lein-test-refresh</code> is a Leiningen tool that monitors your Clojure project&apos;s source, reloads changes, and then runs your tests. Tools like it provide some of the fastest feedback cycles possible.</p><p>To make the feedback cycle even faster I added the option to only run tests in changed namespaces. This means you&apos;re running the minimum number of tests after a change. Version 0.12.0 of <code>lein-test-refresh</code> was released earlier this week with this feature.</p><p>To use it add <code>[com.jakemccrary/lein-test-refresh 0.12.0]</code> as a plugin to your profiles.clj or project.clj. An example <a href="https://github.com/jakemcc/test-refresh/blob/master/sample.project.clj#L3">project.clj</a> can be found in the project&apos;s GitHub repo.</p><p>Once you&apos;re on the latest version you can toggle this feature from the command line by providing a <code>:changes-only</code> flag, <code>lein test-refresh :changes-only</code>, or by adding <code>:changes-only true</code> to your <code>:test-refresh</code> configuration section in your project.clj or profiles.clj. When the feature is on you can still run all your tests by hitting <code>enter</code> in the terminal running <code>lein test-refresh</code>.</p><p>Below is an example of the time difference between running all my tests and the tests in a single namespace.</p><pre><code>Ran 49 tests containing 219 assertions.
0 failures, 0 errors.

Passed all tests
Finished at 14:42:41.655 (run time: 2.006s)
*********************************************
*************** Running tests ***************
:reloading (lumanu.utils-test)

Ran 1 tests containing 3 assertions.
0 failures, 0 errors.

Passed all tests
Finished at 14:43:12.648 (run time: 0.085s)
</code></pre><p>I&apos;ve been using this feature for about a week now and am enjoying it. My whole test suite isn&apos;t particularly slow but even still I&apos;ve been enjoying the faster feedback.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2015/11/15/sql-aggregate-a-set-of-values-together/index.html</id>
    <link href="https://jakemccrary.com/blog/2015/11/15/sql-aggregate-a-set-of-values-together/index.html"/>
    <title><![CDATA[SQL: Aggregate a set of values together]]></title>
    <updated>2015-11-15T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Lately I&apos;ve been working on projects that use <a href="http://www.postgresql.org/">Postgres</a> as our relational database. This has allowed us to simplify some of our Clojure code by leaning on some built-in features of Postgres. One SQL function supported by Postgres which has greatly simplified our code is the <code>array_agg</code> <a href="http://www.postgresql.org/docs/9.4/static/functions-aggregate.html">aggregate</a> function.</p><h2 id="what-is-array-agg?">What is <code>array_agg</code>?</h2><p>The <code>array_agg</code> function takes an argument and returns an array of the argument type. That sentence will make more sense after an example. The snippet below shows a simplified schema for a blog&apos;s database. There is a table called <code>blog_posts</code> that contains details about posts, a table called <code>categories</code> that has labels that can be applied to blog posts, and a join table called <code>post_categories</code> that links the two previous tables together.</p><pre><code class="language-sql">blog=# select id, title from blog_posts;
 id |    title
----+--------------
  1 | SQL Post
  2 | Clojure Post

blog=# select * from categories;
 id |   name
----+----------
  1 | sql
  2 | emacs
  3 | clojure
  4 | postgres

blog=# select * from post_categories;
 blog_post_id | category_id
--------------+-------------
            1 |           1
            2 |           2
            1 |           4
            2 |           3
</code></pre><p>Before I learned about <code>array_agg</code>, if I wanted to know how each blog post had been categorized I might have written the following query.</p><pre><code>select title, name as category
  from blog_posts bp
  join post_categories pc on pc.blog_post_id = bp.id
  join categories c on c.id = pc.category_id
  order by title;


    title     | category
--------------+----------
 Clojure Post | emacs
 Clojure Post | clojure
 SQL Post     | sql
 SQL Post     | postgres
</code></pre><p>The result is readable but as the number of posts and categories grow it becomes harder to read. The query also doesn&apos;t answer the question, &quot;How are my posts categorized?&quot;, well. The ideal answer is a single row per post that shows the post&apos;s categories. You can use <code>array_agg</code> to get that ideal answer.</p><pre><code class="language-sql">select title, array_agg(name) as categories
  from blog_posts bp
  join post_categories pc on pc.blog_post_id = bp.id
  join categories c on c.id = pc.category_id
  group by title;

    title     |   categories
--------------+-----------------
 SQL Post     | {sql,postgres}
 Clojure Post | {emacs,clojure}
</code></pre><p>I find the <code>array_agg</code> version much nicer to read. The result answers the question in a very direct fashion and the query expresses the question well. Everything about the query expresses the question, you no longer have an extra <code>order by</code> clause to make the result more readable by human eyes.</p><h2 id="how-did-it-make-my-clojure-code-simpler?">How did it make my Clojure code simpler?</h2><p>The above is great and it makes everything more readable for a human. Most of the time I&apos;m not querying a SQL database so that a human can directly read the results; instead I&apos;m using Clojure to manipulate results of a query. Fortunately, <code>array_agg</code> simplifies my Clojure code as well.</p><p>I&apos;m working with a schema that has many relationships similar to the above relationship. Continuing with the example from above the snippet below shows the data shape we&apos;d get back from <code>clojure.java.jdbc</code> prior to using <code>array_agg</code>. The data shape we actually want follows.</p><pre><code class="language-clojure">;; data shape you get from the non-array_agg query.
[{:title &quot;Clojure Post&quot; :category &quot;emacs&quot;}
 {:title &quot;SQL Post&quot; :category &quot;sql&quot;}
 {:title &quot;Clojure Post&quot; :category &quot;clojure&quot;}
 {:title &quot;SQL Post&quot; :category &quot;postgres&quot;}]

;; data shape you want
[{:title &quot;Clojure Post&quot; :categories [&quot;emacs&quot; &quot;clojure&quot;]}
 {:title &quot;SQL Post&quot; :categories [&quot;sql&quot; &quot;postgres&quot;]}]
</code></pre><p>Since we&apos;re not getting data in our desired shape we need to write code that combines rows. One way of doing that is to use <code>reduce</code> and <code>map</code>.</p><pre><code class="language-clojure">(defn squash-by-title [rows]
  (-&gt;&gt; rows
       (reduce (fn [r row] (update r (:title row) conj (:category row))) {})
       (map (fn [[title categories]] {:title title :categories categories}))))
</code></pre><p>I&apos;ve been writing Clojure for a long time and when I see code like above it still takes me a bit of time to figure out what is happening. Not only that, but eventually your project has different squash operations depending on what data you&apos;re pulling back from the database. They are probably mostly similar and eventually you abstract the differences and feel great. Then you come back months later and have to figure out how it all works. Luckily, if you&apos;re using a database that supports <code>array_agg</code>, there is a better way.</p><p>The first step is to change your queries to use <code>array_agg</code>. The second step is to extend the <code>clojure.java.jdbc/IResultSetReadColumn</code> protocol to the type returned by your jdbc driver. For my project that looks like the following code:</p><pre><code class="language-clojure">;; clojure.java.jdbc has been required as jdbc

(extend-protocol jdbc/IResultSetReadColumn
  org.postgresql.jdbc4.Jdbc4Array
  (result-set-read-column [pgobj metadata i]
    (vec (.getArray pgobj))))
</code></pre><p>By changing my queries to use <code>array_agg</code> and adding those four lines of code I&apos;m able to delete all of my squashing functions and get data from my database in the shape I want. I also end up with easier to understand code and more expressive queries. Awesome.</p><p><em>Thanks to <a href="http://timothypratley.blogspot.com/">Timothy Pratley</a> for providing feedback on earlier versions of this post.</em></p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2015/07/03/github-code-reviews/index.html</id>
    <link href="https://jakemccrary.com/blog/2015/07/03/github-code-reviews/index.html"/>
    <title><![CDATA[GitHub Code Reviews]]></title>
    <updated>2015-07-03T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Last December I wrote about the <a href="/blog/2014/12/09/an-effective-code-review-process/">effective code review process</a> I started at Outpace. The process works well; participants say it is the most effective review process they&apos;ve experienced. The rest of this post is a summary of the process with a bit of an enhancement around setting up the code for review. I&apos;d recommend you read the original <a href="/blog/2014/12/09/an-effective-code-review-process/">post</a> for a bit more color on the process.</p><h3 id="steps-for-github-code-review">Steps for GitHub code review</h3><ol><li>Select the code to review.</li><li>About a week before the review, create a branch and delete the code you&apos;re reviewing.</li><li>Push this branch to GitHub and open a pull request. This pull request provides a location where comments can be made on every line of code.</li><li>Schedule the code review meeting. Make sure participants have two to three days to asynchronously review the code in the pull request.</li><li>Have the code review. Get everyone together (video chat or in person) and go through the comments on the pull request and discuss. Add action items as a comment. The leader of the code review keeps discussion moving.</li></ol><p>It&apos;s a lightweight process. If you&apos;re already using GitHub it doesn&apos;t bring in any other tools and, unlike some dedicated code review software I&apos;ve used, the GitHub pull request interface has good performance.</p><p>One complaint about this process is that the code you&apos;re reviewing appears as deleted in the pull request. It is a superficial complaint but seeing the entire code base as deleted can feel a bit weird.</p><p>For the most recent code review, I figured out how to have all the code appear as added. The snippet below contains the steps and example commands.</p><pre><code class="language-console">
# cd to the repository you are reviewing.
cd blog

# Make a new branch.
git checkout -b empty-repo

# Copy all files in repo to a temporary directory.
rm -rf /tmp/repo &amp;&amp; mkdir /tmp/repo &amp;&amp; cp -R * /tmp/repo

# Remove all files from repository, commit, and push to GitHub.
rm -rf *
git commit -am &apos;remove all files&apos;
git push origin empty-repo

# Create a new branch with the empty-repo as the parent.
git checkout -b code-review

# Copy back in the files and add the files you want to review.
# Commit and push to GitHub.
cp -R /tmp/repo/* .
git add files-to-review
git commit -m &apos;adding files for review&apos;
git push origin code-review

# Now, go to project on GitHub and switch to the code-review branch.
# Open a pull request comparing the empty-repo and the code-review
# branch.

</code></pre><p>Voila, you now have a pull request with every line under review marked as added instead of deleted! It takes a little more than two times the number steps required to open a pull request with the code deleted but you might find it worth it. Seeing code as added instead of removed is a minor thing but minor things can make a process more enjoyable. It is nice to know it is possible.</p><p>If you aren&apos;t doing code reviews or have found them useless in the past, I recommend you try out this process. This post is the abbreviated version but it gives you enough to get started. If you haven&apos;t done one in this style before, I&apos;d highly recommend reading the longer <a href="/blog/2014/12/09/an-effective-code-review-process/">post</a> as it gives some details that I&apos;ve left out here.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2015/06/30/my-favorite-clj-refactor-features/index.html</id>
    <link href="https://jakemccrary.com/blog/2015/06/30/my-favorite-clj-refactor-features/index.html"/>
    <title><![CDATA[My favorite clj-refactor features]]></title>
    <updated>2015-06-30T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>If you write Clojure using Emacs you should check out <a href="https://github.com/clojure-emacs/clj-refactor.el">clj-refactor</a>. It is working better than ever and makes developing Clojure more enjoyable.</p><p>I don&apos;t use all the features in clj-refactor. There are a lot of features I haven&apos;t had the need to use and many I just can&apos;t remember. Below are the features I use consistently.</p><h3 id="favorite-features">Favorite Features</h3><p>My favorite feature of clj-refactor is the <a href="/blog/2015/06/18/emacs-automatically-require-common-namespaces/">magic requires</a>. This feature lets you type a prefix (such as <code>(str/)</code>) and have the namespace automatically added to your <code>ns</code> form (in this example <code>[clojure.string :as str]</code>). It is awesome. You can also add <a href="/blog/2015/06/18/emacs-automatically-require-common-namespaces/">your own</a> prefix mappings.</p><p>My other most frequently used refactorings are <a href="https://github.com/clojure-emacs/clj-refactor.el/wiki/cljr-introduce-let">introduce let</a>, <a href="https://github.com/clojure-emacs/clj-refactor.el/wiki/cljr-expand-let">expand let</a>, and <a href="https://github.com/clojure-emacs/clj-refactor.el/wiki/cljr-move-to-let">move to let</a>. These three are very complementary and are a quick way if introducing named locals.</p><p><a href="https://github.com/clojure-emacs/clj-refactor.el/wiki/cljr-add-missing-libspec">Add missing libspec</a> is a recent discovery of mine. Have you ever paired with a developer who uses Intellij with Cursive and been a bit jealous of the auto-requiring? I have. This refactoring lets you do that. Type whatever symbol you want and clj-refactor tries to resolve it and then require the containing namespace with correct prefix. Recently I broke a massive namespace into a few smaller ones and this refactoring saved me a ton of time.</p><p>I used to use <a href="https://github.com/clojure-emacs/clj-refactor.el/wiki/cljr-move-form">move form</a> when trying to reorganize namespaces but now I pretty much just cut and paste and use <a href="https://github.com/clojure-emacs/clj-refactor.el/wiki/cljr-add-missing-libspec">add missing libspec</a> to fix the requires. I want to use <strong>move form</strong> but I haven&apos;t had a ton of success with it. <strong>Add missing libspec</strong> plus cut and paste is a few more steps but my success rate has been much higher.</p><p><a href="https://github.com/clojure-emacs/clj-refactor.el/wiki/cljr-sort-ns">Sort ns</a> does exactly what it says, it sorts your <code>ns</code> form. Once you get used to keeping your <code>ns</code> forms sorted you won&apos;t go back.</p><p><a href="https://github.com/clojure-emacs/clj-refactor.el/wiki/cljr-extract-function">Extract function</a> is another refactoring I recently stumbled upon. I&apos;ve used it a few times since then and when it works it is pretty awesome. I&apos;ve had unexpected behavior a couple of times but it was unclear if that was my fault or it not handling macros well. If you&apos;re extracting a function you might as well give it a shot.</p><p>The final feature is the <a href="https://github.com/clojure-emacs/clj-refactor.el/wiki#automatic-insertion-of-namespace-declaration">automatic insertion of namespace declarations</a> when you create a new Clojure file. I nearly forgot to highlight this feature because it requires no action on my side and it is amazing. If I never have to type a namespace symbol again I&apos;ll be happy.</p><h3 id="customization">Customization</h3><p>Below is my entire clj-refactor setup from my Emacs init.el. It doesn&apos;t take much to get it to a state I like.</p><pre><code class="language-lisp">(require &apos;clj-refactor)

;; Add custom magic requires.
(dolist (mapping &apos;((&quot;maps&quot; . &quot;outpace.util.maps&quot;)
                   (&quot;seqs&quot; . &quot;outpace.util.seqs&quot;)
                   (&quot;times&quot; . &quot;outpace.util.times&quot;)
                   (&quot;repl&quot; . &quot;outpace.util.repl&quot;)
                   (&quot;time&quot; . &quot;clj-time.core&quot;)
                   (&quot;string&quot; . &quot;clojure.string&quot;)))
  (add-to-list &apos;cljr-magic-require-namespaces mapping t))

(setq cljr-favor-prefix-notation nil)

(add-hook &apos;clojure-mode-hook (lambda ()
                               (clj-refactor-mode 1)
                               (yas/minor-mode 1)
                               (cljr-add-keybindings-with-prefix &quot;C-c C-x&quot;)))
</code></pre><p>If you use Emacs and write Clojure you should check out clj-refactor. There are enough features that consistently work and help keep you in the flow that it is worth using.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2015/06/18/emacs-automatically-require-common-namespaces/index.html</id>
    <link href="https://jakemccrary.com/blog/2015/06/18/emacs-automatically-require-common-namespaces/index.html"/>
    <title><![CDATA[Emacs: automatically require common namespaces]]></title>
    <updated>2015-06-18T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>If you&apos;re writing Clojure in Emacs you should check out <a href="https://github.com/clojure-emacs/clj-refactor.el">clj-refactor</a>. It provides some neat functionality. Some examples include the ability to extract functions, introduce <code>let</code> forms, and inline symbols. It also has a feature called &quot;magic requires&quot; that automatically requires common namespaces when you type their short form.</p><p>Out of the box five short forms are supported. They are <code>io</code> for <code>clojure.java.io</code>, <code>set</code> for <code>clojure.set</code>, <code>str</code> for <code>clojure.string</code>, <code>walk</code> for <code>clojure.walk</code>, and <code>zip</code> for <code>clojure.zip</code>. If you type <code>(str/</code> then <code>(:require [clojure.string :as str])</code> will be added to your <code>ns</code> form. It is pretty awesome. This feature is on by default but you can turn it off by adding <code>(setq cljr-magic-requires nil)</code> to your Emacs configuration.</p><p>This feature is also extensible. You can add your own mappings of short form to namespace. The following snippet of elisp adds mappings for <code>maps</code>, <code>seqs</code>, and <code>string</code>.</p><pre><code>(dolist (mapping &apos;((&quot;maps&quot; . &quot;outpace.util.maps&quot;)
                   (&quot;seqs&quot; . &quot;outpace.util.seqs&quot;)
                   (&quot;string&quot; . &quot;clojure.string&quot;)))
  (add-to-list &apos;cljr-magic-require-namespaces mapping t))
</code></pre><p>It doesn&apos;t take a lot of code but having it is awesome. If there are namespaces you frequently require I highly recommend setting this up.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2015/05/31/use-git-pre-commit-hooks-to-stop-unwanted-commits/index.html</id>
    <link href="https://jakemccrary.com/blog/2015/05/31/use-git-pre-commit-hooks-to-stop-unwanted-commits/index.html"/>
    <title><![CDATA[Use git pre-commit hooks to stop unwanted commits]]></title>
    <updated>2015-05-31T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Sometimes you&apos;ll make a change to some code and not want to commit it. You probably add a comment to the code and hope you&apos;ll either see the comment in the diff before committing or just remember not to check in the change. If you&apos;ve ever done this you&apos;ve probably also committed something you didn&apos;t mean to commit. I know I have.</p><p>Luckily we can do better. Using git pre-commit <a href="https://git-scm.com/docs/githooks">hooks</a> we can make git stop us from committing. Below is a git pre-commit hook that searches for the text <em>nocommit</em> and if found rejects the commit. With it you can stick <em>nocommit</em> in a comment next to the change you don&apos;t want committed and know that it won&apos;t be committed.</p><h3 id="the-code">The code</h3><pre><code class="language-bash">#!/bin/sh

# If you use a GUI for controlling git, you might want to comment out the `tput` commands.
# Some users have had problems with those commands and whatever GUI they are using.

if git rev-parse --verify HEAD &gt;/dev/null 2&gt;&amp;1
then
    against=HEAD
else
    # Initial commit: diff against an empty tree object
    against=$(git hash-object -t tree /dev/null)
fi

patch_filename=$(mktemp -t commit_hook_changes.XXXXXXX)
git diff --exit-code --binary --ignore-submodules --no-color &gt; &quot;$patch_filename&quot;
has_unstaged_changes=$?

if [ $has_unstaged_changes -ne 0 ]; then
    # Unstaged changes have been found
    if [ ! -f &quot;$patch_filename&quot; ]; then
        echo &quot;Failed to create a patch file&quot;
        exit 1
    else
        echo &quot;Stashing unstaged changes in $patch_filename.&quot;
        git checkout -- .
    fi
fi

quit() {
    if [ $has_unstaged_changes -ne 0 ]; then
        git apply &quot;$patch_filename&quot;
        if [ $? -ne 0 ]; then
            git checkout -- .
            git apply --whitespace=nowarn --ignore-whitespace &quot;$patch_filename&quot;
        fi
    fi

    exit $1
}


# Redirect output to stderr.
exec 1&gt;&amp;2

files_with_nocommit=$(git diff --cached --name-only --diff-filter=ACM $against | xargs -I{} grep -i &quot;nocommit&quot; -l {} | tr &apos;\n&apos; &apos; &apos;)

if [ &quot;x${files_with_nocommit}x&quot; != &quot;xx&quot; ]; then
    tput setaf 1
    echo &quot;File being committed with &apos;nocommit&apos; in it:&quot;
    IFS=$&apos;\n&apos;
    for f in $(git diff --cached --name-only --diff-filter=ACM $against | xargs -I{} grep -i &quot;nocommit&quot; -l {}); do
        echo $f
    done
    tput sgr0
    quit 1
fi

quit 0
</code></pre><p>Lines 3-10 figure out what revision to diff against. They can pretty much be ignored.</p><p>Lines 11-30 are all about handling unstaged changes. They create a patch with these changes and revert these changes from the repository. Then, in the function <code>quit</code>, the unstaged changes are reapplied to the repository. All of this is done so that <em>nocommit</em> in a un-committed piece of text doesn&apos;t cause the committed changes to be rejected.</p><p>Some online guides suggest using <code>git stash</code> to achieve what is described above. I started out using <code>git stash</code> but ran into problems where I&apos;d end up in weird states. Unfortunately I didn&apos;t take good notes and I&apos;m unable to describe the various bad things that happened. Trust me when I say bad things did happen and that this way (create patch, revert, apply patch) is much more successful.</p><p>Line 36 figures out what files contain <em>nocommit</em>. Lines 38-44 report what files contain <em>nocommit</em> and then rejects the commit by exiting with a non-zero exit code. The first <code>tput</code> changes the output of the <code>echo</code> commands to colored red and the second <code>tput</code> changes output back to default.</p><blockquote><p>Warning: I know many developers that love using this and have had no problems. I get the occasional report of it not working. If it doesn&apos;t work, and it seems like you&apos;ve lost changes, you can find the patch file wherever mktemp creates files on your local machine. I&apos;d still recommend testing it out on a small changeset so if something doesn&apos;t work on your machine you don&apos;t have to both debug why and recreate your changes.</p></blockquote><h3 id="using-with-a-single-repository">Using with a single repository</h3><p>To enable in a single repository you need to add the above code to a <code>.git/hooks/pre-commit</code> file in your local repository and make that file executable. Once you&apos;ve done that try adding <em>nocommit</em> to a file and then try to commit it. The commit will be rejected if the pre-commit hook is setup properly.</p><h3 id="using-with-multiple-repositories">Using with multiple repositories</h3><p>I want this pre-commit hook enabled in all of my repositories. I use git init templates to do this. <code>git help init</code> or a Google search can help fill in the gaps with setting this up but below are the steps I ended up taking.</p><ol><li><code>git config --global init.templatedir ~/.git-templates</code></li><li><code>mkdir -p ~/.git-templates/hooks</code></li><li><code>touch ~/.git-templates/hooks/pre-commit</code></li><li>Copy and paste the above code into <code>~/.git-templates/hooks/pre-commit</code></li><li><code>chmod +x ~/.git-templates/hooks/pre-commit</code></li></ol><p>After following those steps any repository created by <code>git init</code> will contain the pre-commit hook. To add to an existing repository <code>cd</code> into the repo and run  <code>git init .</code>.</p><h3 id="example-output">Example output</h3><p>If you try to commit some text with <em>nocommit</em> in it you&apos;ll see something similar to the image below and the commit will be rejected.</p><p><img alt="Error message" src="/images/pre-commit-example.png" /></p><p>If you ever need to commit and want to ignore pre-commit hooks (example: If you are writing a blog post that is full of the text <em>nocommit</em>) then you can ignore pre-commit hooks by using <code>git commit --no-verify</code>.</p><p>I&apos;ve found this pre-commit hook really useful. It has saved me from committing numerous times. I&apos;d recommend adopting it.</p><h2 id="errata">Errata</h2><p><em>2015/12/23</em></p><p>I&apos;m updated the code to be more portable. It was brought to my attention by a comment that the original code took advantage of some bash extensions and specific <code>mktemp</code> behavior found in OS X. The pre-commit code has now been tested works in OS X and Ubuntu 14.04. There may be minor changes you need to perform to get it to work on your system.</p><p><em>2017/04/28</em></p><p>Updated code to handle if <code>mktemp</code> fails and if whitespace changes between creating a patch and applying it. Also adds in a change that better handles whitespace in paths.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2015/05/03/put-the-last-commands-run-time-in-your-bash-prompt/index.html</id>
    <link href="https://jakemccrary.com/blog/2015/05/03/put-the-last-commands-run-time-in-your-bash-prompt/index.html"/>
    <title><![CDATA[Put the last command's run time in your Bash prompt]]></title>
    <updated>2015-05-03T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><blockquote><p>An updated version of this post can be found <a href="/blog/2020/04/21/using-bash-preexec-for-monitoring-the-runtime-of-your-last-command/">here</a></p></blockquote><p>I&apos;m fairly certain the following scenario has happened to every terminal user. You run a command and, while it is running, realize you should have prefixed it with <a href="http://linux.die.net/man/1/time"><code>time</code></a>. You momentarily struggle with the thought of killing the command and rerunning it with <code>time</code>. You decide not to and the command finishes without you knowing how long it took. You debate running it again.</p><p>For the last year I&apos;ve lived in a world without this problem. Upon completion, a command&apos;s approximate run time is displayed in my prompt. It is awesome.</p><h2 id="overview">Overview</h2><p>Most of the code below is from a post on <a href="http://stackoverflow.com/a/1862762/491871">Stack Overflow</a>. It has been slightly modified to support having multiple commands in your <code>$PROMPT_COMMAND</code> variable. Below is a minimal snippet that could be included in your <code>.bashrc</code>.</p><pre><code class="language-bash">function timer_start {
  timer=${timer:-$SECONDS}
}

function timer_stop {
  timer_show=$(($SECONDS - $timer))
  unset timer
}

trap &apos;timer_start&apos; DEBUG

if [ &quot;$PROMPT_COMMAND&quot; == &quot;&quot; ]; then
  PROMPT_COMMAND=&quot;timer_stop&quot;
else
  PROMPT_COMMAND=&quot;$PROMPT_COMMAND; timer_stop&quot;
fi

PS1=&apos;[last: ${timer_show}s][\w]$ &apos;
</code></pre><p>Modify your <code>.bashrc</code> to include the above and you&apos;ll have a prompt that looks like the image below. It is a minimal prompt but it includes the time spent on the last command. This is great. No more wondering how long a command took.</p><p><img alt="Example of prompt" src="/images/prompt-timings.png" /></p><h2 id="the-details">The details</h2><p><code>timer_start</code> is a function that sets <code>timer</code> to be its current value or, if <code>timer</code> is unset, sets it to the value of <code>$SECONDS</code>. <code>$SECONDS</code> is a special variable that contains the number of seconds since the shell was started. <code>timer_start</code> is invoked after every simple command as a result of <code>trap &apos;timer_start&apos; DEBUG</code>.</p><p><code>timer_stop</code> calculates the difference between <code>$SECONDS</code> and <code>timer</code> and stores it in <code>timer_show</code>. It also unsets <code>timer</code>. Next time <code>timer_start</code> is invoked <code>timer</code> will be set to the current value of <code>$SECONDS</code>. Because <code>timer_stop</code> is part of the <code>$PROMPT_COMMAND</code> it is executed prior to the prompt being printed.</p><p>It is the interaction between <code>timer_start</code> and <code>timer_stop</code> that captures the run time of commands. It is important that <code>timer_stop</code> is the <strong>last</strong> command in the <code>$PROMPT_COMMAND</code>. If there are other commands after it then those will be executed and their execution might cause <code>timer_start</code> to be called. This results in you timing the length of time between the prior and current prompts being printed.</p><h2 id="my-prompt">My prompt</h2><p>My prompt is a bit more complicated. It shows the last exit code, last run time, time of day, directory, and git information. The run time of the last command is one of the more useful parts of my prompt. I highly recommend you add it to yours.</p><p><img alt="My prompt" src="/images/my-prompt.png" /></p><h2 id="errata">Errata</h2><p><em>2015/5/04</em></p><p><a href="https://twitter.com/gfredericks_">Gary Fredericks</a> noticed that the original code sample broke if you didn&apos;t already have something set as your <code>$PROMPT_COMMAND</code>. I&apos;ve updated the original snippet to reflect his <a href="https://twitter.com/gfredericks_/status/595249998838800384">changes</a>.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2015/04/25/quieter-clojure-dot-test-output/index.html</id>
    <link href="https://jakemccrary.com/blog/2015/04/25/quieter-clojure-dot-test-output/index.html"/>
    <title><![CDATA[Quieter clojure.test output]]></title>
    <updated>2015-04-25T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>If you use <code>clojure.test</code> then there is a good chance you&apos;ve been annoyed by all the <a href="https://github.com/jakemcc/test-refresh/issues/33">output</a> when you run your tests in the terminal. When there is a test failure you have to scroll through pages of output to find the error.</p><p>With release <code>0.9.0</code> of <a href="https://github.com/jakemcc/test-refresh">lein-test-refresh</a> you can minimize the output of <code>clojure.test</code> and <strong>only</strong> see failure and summary messages. To enable this feature add <code>:quiet true</code> to the <code>:test-refresh</code> configuration map in either your project.clj or profiles.clj file. If you configure <code>lein-test-refresh</code> in <code>~/.lein/profiles.clj</code> then turning on this feature looks like the following.<a href="#fn-1" id="fnref1"><sup>1</sup></a></p><pre><code class="language-clojure">{:user {:plugins [[com.jakemccrary/lein-test-refresh &quot;0.9.0&quot;]]
        :test-refresh {:quiet true}}}
</code></pre><p>Setting up your profiles.clj like above allows you to move to Clojure project in your terminal, run <code>lein test-refresh</code>, and have your <code>clojure.test</code>s run whenever a file changes. In addition, your terminal won&apos;t show the usual <em>Testing a.namespace</em> output.</p><p>Below is what you typically see when running <code>clojure.test</code> tests in a terminal. I had to cut most of the <em>Testing a.namespace</em> messages from the picture.</p><p><img alt="Normal view of test output" src="/images/not-quiet-test-output.png" /></p><p>The following picture is with quiet mode turned on in <code>lein-test-refresh</code>. No more <em>Testing a.namespace</em> messages! No more scrolling through all your namespaces to find the failure!</p><p><img alt="Minimal output in console" src="/images/minimal-test-output.png" /></p><p>I just released this feature so i haven&apos;t had a chance to use it too much. I imagine it may evolve to change the output more.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>More configuration options can be found <a href="https://github.com/jakemcc/test-refresh/blob/master/sample.project.clj#L5-L24">here</a><a href="#fnref1">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2015/04/12/making-tmate-and-tmux-play-nice-with-os-x-terminal-notifier/index.html</id>
    <link href="https://jakemccrary.com/blog/2015/04/12/making-tmate-and-tmux-play-nice-with-os-x-terminal-notifier/index.html"/>
    <title><![CDATA[Making tmate and tmux play nice with OS X terminal-notifier]]></title>
    <updated>2015-04-12T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>For nearly the last two years, I&apos;ve been doing most of my development in OS X. Most of that development has been done in Clojure and, whenever possible, using <a href="https://github.com/jakemcc/test-refresh">lein-test-refresh</a> with <a href="https://github.com/julienXX/terminal-notifier">terminal-notifier</a> to have my tests automatically run and a notification shown with the status of the test run. Its a great work flow that gives me a quick feedback cycle and doesn&apos;t pull my attention in different directions.</p><p>Recently I&apos;ve started using <a href="http://tmate.io/">tmate</a> for remote pairing. Unfortunately when I first started using it my quick feedback cycle was broken. <code>lein test-refresh</code> would run my tests but would become hung when sending a notification using <code>terminal-notifier</code>. This was terrible and, if I hadn&apos;t been able to fix it, would have stopped me from using <code>tmate</code>. After some searching I stumbled across <a href="https://github.com/julienXX/terminal-notifier/issues/115">this</a> GitHub issue which helped solve the problem.</p><p>To make <code>tmate</code> work nicely with <code>terminal-notifier</code> you&apos;ll need to install <a href="https://github.com/ChrisJohnsen/tmux-MacOSX-pasteboard">reattach-to-user-namespace</a> and change your <code>tmate</code> configuration to use it. If you use <code>brew</code> you can install by running <code>brew install --with-wrap-pbcopy-and-pbpaste reattach-to-user-namespace</code>. Then open your <code>.tmux.conf</code> or <code>.tmate.conf</code> file and add the line below.</p><pre><code>set-option -g default-command &quot;which reattach-to-user-namespace &gt; /dev/null &amp;&amp; reattach-to-user-namespace -l $SHELL || $SHELL&quot;
</code></pre><p>The above tells <code>tmate</code> to use <code>reattach-to-user-namespace</code> if it is available. Now <code>terminal-notifier</code> no longer hangs when invoked inside <code>tmate</code>. Unsurprisingly, this change also makes <code>tmux</code> place nice with <code>terminal-notifier</code>.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2015/03/31/my-home-work-space/index.html</id>
    <link href="https://jakemccrary.com/blog/2015/03/31/my-home-work-space/index.html"/>
    <title><![CDATA[My home work space]]></title>
    <updated>2015-03-31T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>I&apos;ve been working remotely for about a year and a half. In that time, I&apos;ve worked from many locations but most of my time has been spent working from my apartment in Chicago. During this time I&apos;ve tweaked my environment by building a standing desk, building a keyboard, and changed my monitor stands. Below is a my desk (click for larger image).</p><p><a href="/images/my-desk.png"><img alt="My Desk" src="/images/my-desk-small.png" title="Picture of my desk" /></a></p><h2 id="the-desk">The Desk</h2><p>I built my own desk using the <a href="http://www.ikea.com/us/en/catalog/products/50106773/">Gerton</a> table top from Ikea and the <a href="http://www.ergoprise.com/s2s-height-adjustable-desk-base-new-improved/">S2S Height Adjustable Desk Base</a> from <a href="http://www.ergoprise.com/">Ergoprise</a>. I originally received a defective part from Ergoprise and after a couple emails I was sent a replacement part. Once I had working parts, attaching the legs to the table top was straightforward. The desk legs let me adjust the height of my desk so I can be sitting or standing comfortably.</p><h2 id="the-monitors">The Monitors</h2><p>I have two 27 inch Apple Cinema displays that are usually connected to a 15 inch MacBook Pro. The picture doesn&apos;t show it, but I actively use all the monitors.</p><p>My laptop is raised by a <a href="http://www.amazon.com/gp/product/B000OOYECC">mStand Laptop Stand</a>. While I&apos;m sitting this stand puts the laptop at a comfortable height. I highly recommend getting one.</p><p>The middle monitor, the one I use the most, has had the standard stand (you can see it in the right monitor) replaced with an <a href="http://www.amazon.com/gp/product/B00BZC05WU">ErgoTech Freedom Arm</a>. This lets me raise the monitor to a comfortable height when I&apos;m standing (as seen in this picture). It also allows me to rotate the monitor vertically, though I have only done that once since installing it. Installation of the arm wasn&apos;t trivial, but it wasn&apos;t that difficult.</p><p>I&apos;ve been using the arm for four months now and I&apos;m enjoying it. If you bump the desk the monitor does wobble a bit but I don&apos;t notice it while I&apos;m typing. I haven&apos;t noticed any slippage; the monitor arm seems to hold the monitor in place.</p><p>I&apos;ve decided against getting a second arm for my other monitor. Installing the monitor arm renders your monitor non-portable. It doesn&apos;t happen often, but sometimes I travel and stay at a place for long enough that I want to bring a large monitor.</p><h2 id="the-chair">The Chair</h2><p>My desk chair is a Herman Miller <a href="https://www.amazon.com/Setu-Chair-Herman-Miller-Standard/dp/B004VSL6E6">Setu</a>. It is a very comfortable chair that boasts only a single adjustment. You can only raise or lower it.</p><p>I moved to this chair from a Herman Miller <a href="https://www.amazon.com/Aeron-Task-Chair-Herman-Miller/dp/B0014YGGKE">Aeron</a>. The Aeron had been my primary chair for eight years prior to me buying the Setu.</p><p>They are both great chairs. I haven&apos;t missed the extreme amount of customization the Aeron provides; its actually nice having fewer knobs to tweak. I also find the Setu more visually appealing. The Aeron is sort of a giant black monster of a chair; I prefer seeing the chartreuse Setu in my apartment.</p><h2 id="the-keyboard-and-mouse">The Keyboard and Mouse</h2><p>I built my own keyboard. It is an ErgoDox with Cherry MX Blue key switches and DSA key caps. More details about my build can be found in an earlier <a href="/blog/2014/07/27/building-the-ergodox-keyboard/">post</a>.</p><p>I&apos;ve been using this keyboard for about eight months. It has been rock solid. This is my first keyboard that has mechanical switches. They are nice. It feels great typing on this keyboard.</p><p>The ErgoDox has six keys for each thumb. I originally thought I&apos;d be using the thumb clusters a lot but, in practice, I only actively use two or three keys per thumb.</p><p>The ErgoDox also supports having multiple layers. This means that with the press of a key I can have an entirely different keyboard beneath my finger tips. It turns out this is another feature I don&apos;t frequently use. I really only use layers for controlling my music playback through media keys and for hitting function keys.</p><p>If I were going to build a keyboard again I would not use <a href="http://deskthority.net/wiki/Cherry_MX_Blue">Cherry MX Blues</a> as the key switch. They are very satisfying to use but they are loud. You can hear me type in every room of my one bedroom apartment. When I&apos;m remote pairing with other developers, they can here me type through my microphone.</p><p>For my mouse I use Apple&apos;s Magic Trackpad. I definitely have problems doing precise mouse work (though I rarely find myself needing this) but I really enjoy the gestures in enables. I&apos;ve been using one of these trackpads for years now. I really don&apos;t want to go back to using a mouse.</p><h2 id="other-items">Other Items</h2><p>I&apos;m a fan of using pens and paper to keep track of notes. My tools of choice are <a href="http://www.amazon.com/gp/product/B00IYL90B2">Leuchturm Whitelines</a> notebook with dotted paper and a <a href="http://www.amazon.com/gp/product/B00BT1BLRU">TWSBI 580</a> fountain pen with a fine nib. I&apos;ve been using fountain pens<a href="#fn-1" id="fnref1"><sup>1</sup></a> for a couple years now and find them much more enjoyable to use than other pen styles. The way you glide across the page is amazing. I usually have my pen inked with <a href="http://www.amazon.com/gp/product/B00DJAKBAW">Noodler&apos;s 54th Massachusetts</a>. The ink is a beautiful blue black color and very permanent.</p><p>No desk is complete without a few fun desk toys. My set of toys includes a bobble head of myself (this was a gift from a good friend), a 3d printed <a href="http://www.shapeways.com/product/MLX77XRTT/success-kid">Success Kid</a>, a keyboard switch sampler, a few more 3d printed objects, and some climbing related hand toys.</p><h2 id="end">End</h2><p>That pretty much covers my physical work space. I&apos;ve tweaked it enough where I don&apos;t feel like I need to experiment anymore. The monitor arm is my most recent addition and it really helped bring my environment to the next level. I think I&apos;ll have a hard time improving my physical setup.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>If you want to try out fountain pens I highly recommend the <a href="http://www.amazon.com/gp/product/B009X9Z2FW">Pilot Metropolitan</a>. It is widely regarded as the best introduction to fountain pens. The medium nib is about the same width as my fine. It is a great introduction to fountain pens. Another great intro pen (that includes a smiling face on the nib) is the <a href="http://www.amazon.com/gp/product/B00IOOOBU4">Pilot Kakuno</a>.<a href="#fnref1">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2015/03/24/advanced-leiningen-checkouts-configuring-what-ends-up-on-your-classpath/index.html</id>
    <link href="https://jakemccrary.com/blog/2015/03/24/advanced-leiningen-checkouts-configuring-what-ends-up-on-your-classpath/index.html"/>
    <title><![CDATA[Advanced Leiningen checkouts: configuring what ends up on your classpath]]></title>
    <updated>2015-03-24T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p><a href="http://leiningen.org/">Leiningen</a> checkout dependencies are a useful feature. Checkout dependencies allow you to work on a library and consuming project at the same time. By setting up checkout dependencies you can skip running <code>lein install</code> in the library project; it appears on the classpath of the consuming project. An example of what this looks like can be found in the <a href="https://github.com/technomancy/leiningen/blob/master/doc/TUTORIAL.md#checkout-dependencies">Leiningen documentation</a> or in a <a href="http://jakemccrary.com/blog/2012/03/28/working-on-multiple-clojure-projects-at-once/">previous post</a> of mine.</p><p>By default, Leiningen adds the <code>:source-paths</code>, <code>:test-paths</code>, <code>:resource-paths</code>, and <code>:compile-path</code> directories of the checkout projects to your consuming project&apos;s classpath. It also recurses and adds the checkouts of your checkouts (and keeps recursing).</p><p>You can override what gets added to your classpath by <code>:checkout-deps-shares</code> to your project.clj. This key&apos;s value should be a vector of functions that when applied to your checkouts&apos; project map return the paths that should be included on the classpath. The default values can be found <a href="https://github.com/technomancy/leiningen/blob/ff84da697249184874b528950048981621ac0b61/leiningen-core/src/leiningen/core/project.clj#L488-L492">here</a> and an example of overriding the default behavior can be found in the <a href="https://github.com/technomancy/leiningen/blob/ff84da697249184874b528950048981621ac0b61/sample.project.clj#L320-L321">sample.project.clj</a>.</p><p>I ran into a situation this week where having my checkouts&apos; <code>:test-paths</code> on the classpath caused issues my consuming project. My first pass at fixing this problem was to add <code>:checkout-deps-shares [:source-paths :resource-paths :compile-path]</code> to my project.clj. <strong>This didn&apos;t work</strong>. My project.clj looked like below.</p><pre><code class="language-clojure">(defproject example &quot;1.2.3-SNAPSHOT&quot;
  :dependencies [[library &quot;1.2.2&quot;]
                 [org.clojure/clojure &quot;1.6.0&quot;]]
  :checkout-deps-shares [:source-paths :resource-paths :compile-path])
</code></pre><p>Why didn&apos;t it work? It didn&apos;t work because of how Leiningen merges duplicate keys in the project map. When Leiningen merges the various configuration maps (from merging profiles, merging defaults, etc) and it encounters values that are collections it combines them (more details found in <a href="https://github.com/technomancy/leiningen/blob/master/doc/PROFILES.md#merging">documentation</a>). Using <code>lein pprint :checkout-deps-shares</code> shows what we end up with.</p><pre><code class="language-console">$ lein pprint :checkout-deps-shares
(:source-paths
 :resource-paths
 :compile-path
 :source-paths
 :test-paths
 :resource-paths
 :compile-path
 #&lt;Var@43e3a075:
   #&lt;classpath$checkout_deps_paths leiningen.core.classpath$checkout_deps_paths@6761b44b&gt;&gt;)
</code></pre><p>We&apos;ve ended up with the default values and the values we specified in the project.clj. This isn&apos;t hard to fix. To tell Leiningen to replace the value instead of merging you add the <code>^:replace</code> metadata to the value. Below is the same project.clj but with <code>^:replace</code> added.</p><pre><code class="language-clojure">(defproject example &quot;1.2.3-SNAPSHOT&quot;
  :dependencies [[library &quot;1.2.2&quot;]
                 [org.clojure/clojure &quot;1.6.0&quot;]]
  :checkout-deps-shares ^:replace [:source-paths :resource-paths :compile-path])
</code></pre><p>This solves the problem of <code>:test-paths</code> showing up on the classpath but it introduces another problem. Checkouts&apos; checkout dependencies no longer show up on the classpath. This is because <code>leiningen.core.classpath/checkout-deps-paths</code> is no longer applied to the checkouts.</p><p>Without <code>leiningen.core.classpath/checkout-deps-paths</code> Leiningen stops recursing and, as a result, no longer picks up checkouts&apos; checkout dependencies. My first attempt at fixing this was to modify my project.clj so the <code>:checkout-deps-shares</code> section looked like below.</p><pre><code class="language-clojure">:checkout-deps-shares ^:replace [:source-paths :resource-paths :compile-path
                                 leiningen.core.classpath/checkout-deps-paths]
</code></pre><p>The above fails. It runs but doesn&apos;t actually add the correct directories to the classpath. The next attempt is below.</p><pre><code class="language-clojure">:checkout-deps-shares ^:replace [:source-paths :resource-paths :compile-path
                                 #&apos;leiningen.core.classpath/checkout-deps-paths]
</code></pre><p>This attempt failed quicker. Now an exception is thrown when trying to run Leiningen tasks.</p><p>The next one works. It takes advantage of dynamic eval through <a href="https://github.com/technomancy/leiningen/blob/master/doc/PROFILES.md#dynamic-eval">read-eval</a> syntax. With the below snippet the checkouts&apos; checkouts are added to the classpath.</p><pre><code class="language-clojure">:checkout-deps-shares ^:replace [:source-paths :resource-paths :compile-path
                                 #=(eval leiningen.core.classpath/checkout-deps-paths)]
</code></pre><p>Hopefully this is useful to someone else. It took a bit of digging to figure it out and many incorrect attempts to get correct. The full example project.clj is below.</p><pre><code class="language-clojure">(defproject example &quot;1.2.3-SNAPSHOT&quot;
  :dependencies [[library &quot;1.2.2&quot;]
                 [org.clojure/clojure &quot;1.6.0&quot;]]
  :checkout-deps-shares ^:replace [:source-paths :resource-paths :compile-path
                                   #=(eval leiningen.core.classpath/checkout-deps-paths)])
</code></pre></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2015/01/24/remote-pairing/index.html</id>
    <link href="https://jakemccrary.com/blog/2015/01/24/remote-pairing/index.html"/>
    <title><![CDATA[Remote Pairing]]></title>
    <updated>2015-01-24T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><blockquote><p>See all of my remote/working-from-home <a href="/blog/categories/remote/">articles here</a>.</p></blockquote><p>Over a year ago I joined <a href="http://outpace.com">Outpace</a>. All of Outpace&apos;s developers are remote but we still practice pair programming. As a result I&apos;ve done a lot of remote pairing. I was skeptical before joining that it would work well and I&apos;m happy to report that I was wrong. Remote pairing works.</p><h2 id="why-remote-pairing?">Why remote pairing?</h2><p>The usual pair programming <a href="http://c2.com/cgi/wiki?PairProgrammingBenefits">benefits</a> apply to remote pairing; more people know the code, quality is higher, and it provides an opportunity for mentorship. Another benefit, more beneficial in a remote setting, is that it increases <strong>social interaction</strong>.</p><p>The most common response I receive when I tell someone I work from my apartment is &quot;I&apos;d miss the interaction with co-workers.&quot; When you work remote you do miss out on the usual in office interaction. Pair programming helps replace some of this. It helps you build and maintain relationships with your remote colleagues.</p><h2 id="communication">Communication</h2><p>Communication is an important part of pair programming. When you&apos;re pairing in person you use both physical and vocal communication. When remote pairing you primarily use vocal communication. You can pick up on some physical cues with video chat but it is hard. You will never notice your pair reaching for their keyboard.</p><p>I&apos;ve used Google Hangouts, <a href="http://zoom.us">Zoom</a>, and Skype for communication. Currently I&apos;m primarily using Zoom. It offers high quality video and audio and usually doesn&apos;t consume too many resources.</p><p>I recommend not using your computers built-in microphone. You should use headphones with a mic or a directional microphone. You&apos;ll sound better and you&apos;ll stop your pair from hearing themselves through your computer.</p><p>I use <a href="http://www.amazon.com/gp/product/B005VAORH6">these headphones</a>. They are cheap, light, and open-eared but are wired. I&apos;ve been told I sound the best when I&apos;m using them. I also own these <a href="http://www.amazon.com/gp/product/B003VANOFY">wireless headphones</a>. They are closed-ear, heavier, and wireless. The wireless is great but the closed-ear design causes me to talk differently and by the end of the day my throat is hoarse. Both of these headphones are widely used by my colleagues and I don&apos;t think you can go wrong with either one.</p><p>Some people don&apos;t like wearing headphones all day. If you are one of those I&apos;d recommend picking up a directional microphone. Many of my colleagues use a <a href="http://www.amazon.com/gp/product/B002OO333Q">Snowball</a>.</p><h2 id="connecting-the-machines">Connecting the machines</h2><p>So now you can communicate with your pair. It is time to deal with the main problem in remote pairing. How do you actually work on the same code with someone across the world?</p><p>At Outpace we&apos;ve somewhat cheated and have standardized our development hardware. Everyone has a computer running OS X and, if they want it, at least one 27 inch monitor (mostly Apple 27 inch displays or a <a href="http://www.amazon.com/gp/product/B009H0XQQY">Dell</a>) with a resolution of 2560x1440. Since everyone has nearly identical hardware and software we are able to pair using OS X&apos;s built-in screen sharing. This allows full sharing of the host&apos;s desktop. This full desktop sharing is the best way to emulate working physically next to your pair. This enable the use of any editor and lets you both look at the same browser windows (useful for testing UIs or reading reference material). With decent internet connections both programmers can write code with minimal lag. This is my preferred way of pairing.</p><p>Another option that works well is <a href="http://tmate.io/">tmate</a>. tmate is a fork of tmux that makes remote pairing easy. It makes it dead simple to have remote developer connect to your machine and share your terminal. This means you are stuck using tools that work in a terminal and, if you are working on a user interface, you need to share that some other way. There generally is less lag when the remote developer is typing.</p><p>A third option is to have the host programmer share their screen using screen sharing built-in to Google Hangouts or <a href="http://zoom.us">Zoom</a>. This is a quick way to share a screen and is my preferred way of sharing GUIs with more than one other person. With both Zoom and Google Hangouts the remote developer can control the host&apos;s machine but it isn&apos;t a great experience. If you are pairing this way the remote developer rarely touches the keyboard.</p><h2 id="soloing">Soloing</h2><p>It might seem weird to have a section on soloing in an article about remote pairing. Soloing happens and even in an environment that almost entirely pairs it is important. Not everyone can or wants to pair 100% of the time. Soloing can be recharging. It is important to be self-aware and recognize if you need solo time. Below are a few tips for getting that solo time.</p><p>One way to introduce solo time is to take your lunch at a different time than your pair. This provides both of you and your pair with an opportunity to do a bit of soloing.</p><p>Other short soloing opportunities happen because of meetings and interviews. It isn&apos;t uncommon for half of a pair to leave for a bit to join a meeting, give an interview, or jump over to help out another developer for a bit.</p><p>Soloing also happens as a result of uneven team numbers. If your team is odd numbered than there are plenty of opportunities for being a solo developer. Try to volunteer to be the solo developer but be aware of becoming too isolated.</p><h2 id="conclusion">Conclusion</h2><p>Remote pairing works. Working at <a href="http://outpace.com">Outpace</a> has shown me how well it can work. Reasonably fast Internet paired with modern tools can make it seem like you&apos;re almost in the same room as your pair.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2015/01/11/overview-of-my-leiningen-profiles-dot-clj/index.html</id>
    <link href="https://jakemccrary.com/blog/2015/01/11/overview-of-my-leiningen-profiles-dot-clj/index.html"/>
    <title><![CDATA[Overview of my Leiningen profiles.clj]]></title>
    <updated>2015-01-11T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p><strong>2017-08-27: I&apos;ve published an updated version <a href="/blog/2017/08/27/my-current-leiningen-profiles-dot-clj/">here</a>.</strong></p><p><a href="https://github.com/technomancy/leiningen">Leiningen</a>, a Clojure build tool, has the concept of <a href="https://github.com/technomancy/leiningen/blob/master/doc/PROFILES.md">profiles</a>. One thing profiles are useful for is allowing you to have development tools available to a project without having them as dependencies when you release your project. An example of when you might want to do this is when you are using a testing library like <a href="https://github.com/jaycfields/expectations">expectations</a>.</p><p>Some development tools, such as <a href="https://github.com/jakemcc/test-refresh">lein-test-refresh</a>, are useful to have across most of your Clojure projects. Rather nicely, Leiningen supports adding global profiles to <code>~/.lein/profiles.clj</code>. These profiles are available in all your projects.</p><p>Below is most of my <code>profiles.clj</code>. I&apos;ve removed some sensitive settings and what is left are the development tools that I find useful.</p><pre><code class="language-clojure">{:user {:plugin-repositories [[&quot;private-plugins&quot; {:url &quot;private repo url&quot;}]]
        :dependencies [[pjstadig/humane-test-output &quot;0.6.0&quot;]]
        :injections [(require &apos;pjstadig.humane-test-output)
                     (pjstadig.humane-test-output/activate!)]
        :plugins [[cider/cider-nrepl &quot;0.8.2&quot;]
                  [refactor-nrepl &quot;0.2.2&quot;]
                  [com.jakemccrary/lein-test-refresh &quot;0.5.5&quot;]
                  [lein-autoexpect &quot;1.4.2&quot;]
                  [lein-ancient &quot;0.5.5&quot;]
                  [jonase/eastwood &quot;0.2.1&quot;]
                  [lein-kibit &quot;0.0.8&quot;]
                  [lein-pprint &quot;1.1.2&quot;]]
        :test-refresh {:notify-command [&quot;terminal-notifier&quot; &quot;-title&quot; &quot;Tests&quot; &quot;-message&quot;]}}}
</code></pre><p><code>:plugin-repositories [[&quot;private-plugins&quot; {:url &quot;private repo url&quot;}]]</code> sets a private plugin repository. This allows me to use <a href="http://outpace.com/">Outpace&apos;s</a> private Leiningen templates for setting up new projects for work.</p><p>The next few lines are all related. They setup <a href="https://github.com/pjstadig/humane-test-output">humane-test-output</a>. <code>humane-test-output</code> makes <code>clojure.test</code> output more readable. It makes using <code>clojure.test</code> much more enjoyable. I highly recommend it. Sample output can be found in my <a href="/blog/2014/06/22/comparing-clojure-testing-libraries-output/">Comparing Clojure Testing Libraries</a> post.</p><pre><code class="language-clojure">:dependencies [[pjstadig/humane-test-output &quot;0.6.0&quot;]]
:injections [(require &apos;pjstadig.humane-test-output)
             (pjstadig.humane-test-output/activate!)]
</code></pre><p>Next we get to my <code>:plugins</code> section. This is the bulk of my <code>profiles.clj</code>.</p><pre><code class="language-clojure">:plugins [[cider/cider-nrepl &quot;0.8.2&quot;]
          [refactor-nrepl &quot;0.2.2&quot;]
          [com.jakemccrary/lein-test-refresh &quot;0.5.5&quot;]
          [lein-autoexpect &quot;1.4.2&quot;]
          [lein-ancient &quot;0.5.5&quot;]
          [jonase/eastwood &quot;0.2.1&quot;]
          [lein-kibit &quot;0.0.8&quot;]
          [lein-pprint &quot;1.1.2&quot;]]
</code></pre><p>The first entry is for <code>cider/cider-nrepl</code>. I write Clojure using Emacs and <a href="https://github.com/clojure-emacs/cider">CIDER</a> and much of CIDER&apos;s functionality exists in nrepl middleware found in <code>cider/cider-nrepl</code>. This dependency is required for me to be effective while writing Clojure.</p><p><code>refactor-nrepl</code> is next. <a href="https://github.com/clojure-emacs/clj-refactor.el">clj-refactor.el</a> requires it for some refactorings. I actually don&apos;t use any of those refactorings (I only use move to let, extract to let, and introduce let refactorings) but I still keep it around.</p><p><code>com.jakemccrary/lein-test-refresh</code> is next. This lets me use <a href="https://github.com/jakemcc/test-refresh">lein-test-refresh</a> globally. <code>lein-test-refresh</code> runs your <code>clojure.test</code> tests whenever a file changes in your project. This is another key development tool in my process.</p><p>Up next is <code>lein-autoexpect</code>. It was the first Leiningen plugin I wrote and it enables continuous testing with <a href="https://github.com/jaycfields/expectations">expectations</a>.</p><p>Both <code>lein-autoexpect</code> and <code>lein-test-refresh</code> are projects I created and maintain. Writing <code>lein-autoexpect</code> was my first exposure to continuous testing and it changed how I develop code. I find it frustrating to develop without such a tool.</p><p>Next up is <a href="https://github.com/xsc/lein-ancient">lein-ancient</a>. It checks your project.clj for outdated dependencies and plugins. It isn&apos;t something that gets used every day but it is super useful when you need it.</p><p>The next two entries are for <a href="https://github.com/jonase/eastwood">jonase/eastwood</a> and <a href="https://github.com/jonase/kibit">lein-kibit</a>. They are both tools that look at your Clojure code and report common mistakes. I don&apos;t use either consistently but I do find them useful. I&apos;ve found bugs with eastwood.</p><p>The final plugin is <code>lein-pprint</code>. <a href="https://github.com/technomancy/leiningen/tree/master/lein-pprint">lein-pprint</a> prints out your project map. It is useful for trying to grasp what is going on when messing around with various Leiningen options.</p><p>The final part, seen below, of my <code>profiles.clj</code> is configuration for <code>lein-test-refresh.</code> It configures <code>lein-test-refresh</code> to use <a href="https://github.com/julienXX/terminal-notifier">terminal-notifier</a> to notify me when my tests pass or fail. Using a continuous tester that allows flexible notification is useful. Not having to glance at a terminal to see if your tests are passing or failing is great.</p><pre><code class="language-clojure">:test-refresh {:notify-command [&quot;terminal-notifier&quot; &quot;-title&quot; &quot;Tests&quot; &quot;-message&quot;]}
</code></pre><p>That is my <code>~/.lein/profiles.clj</code>. I don&apos;t think it contains anything mind blowing but it definitely contains a useful collection of Clojure development tools. I encourage you to check out them out and to think about what tools you should be putting into your global <code>:user</code> profile.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2015/01/08/reading-in-2014/index.html</id>
    <link href="https://jakemccrary.com/blog/2015/01/08/reading-in-2014/index.html"/>
    <title><![CDATA[Reading in 2014]]></title>
    <updated>2015-01-08T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>At the beginning of last year I took some time and <a href="http://jakemccrary.com/blog/2014/01/01/using-incanter-to-review-my-2013-reading/">reviewed</a> my 2013 reading using Clojure and Incanter to generate some stats. It was a useful exercise to reflect back on my reading and play around with Incanter again.</p><p>Over the last couple of weeks I&apos;ve taken a similar look at my 2014 reading. The rest of this post highlights some of the top books from the previous year and then posts some numbers at the end.</p><p>I review every book I read using <a href="https://www.goodreads.com/">Goodreads</a>. If you want to see more of what I&apos;ve been reading you can find me <a href="https://www.goodreads.com/user/show/3431614-jake-mccrary">here</a>. I track and review every book I read and have found this practice to be extremely rewarding.</p><h3 id="2014-goals">2014 Goals</h3><p>I entered 2014 without a volume goal. Unlike 2013, I didn&apos;t have a page or book count goal. I entered 2014 with the desire to reread two specific books and the nebulous goal of reading more non-fiction.</p><h3 id="2014-results">2014 Results</h3><p>I ended up setting a new volume record. I read 69 books for a total of almost 23,000 pages. I also read every week of <a href="http://www.amazon.com/gp/product/B00FDWVIHO">Day One</a>, a weekly literary journal containing one short story and one poem from new authors. This doesn&apos;t count towards my page or book count but is reading I enjoy. It exposes me to many different styles.</p><p>More than a third of my reading was non-fiction. I don&apos;t have numbers for 2013 but that feels like an increase. I consider my goal of reading more non-fiction achieved.</p><p>I also reread the two books I had planned on rereading. I wanted to reread <a href="http://www.amazon.com/gp/product/B000S1M9LY">Infinite Jest</a> and <a href="http://www.amazon.com/Hard-Boiled-Wonderland-World-Haruki-Murakami-ebook/dp/B004AP9W1O/ref=sr_1_1?ie=UTF8&amp;qid=1420159468&amp;sr=8-1&amp;keywords=a+hard+boiled+wonderland">Hard-Boiled Wonderland and the End of the World</a> and succeeded in rereading both of them.</p><h3 id="recommendations">Recommendations</h3><p>I awarded seven books a five out of five star rating. I&apos;ve listed them below in (in no particular order). Each book I&apos;d recommend without hesitation. Instead of reworking or copying my previous reviews I&apos;ve provided links to Goodreads. The titles link to Amazon.</p><ul><li><a href="http://www.amazon.com/gp/product/B00QS2HXUO">Working Effectively with Unit Tests</a> by Jay Fields (<a href="https://www.goodreads.com/review/show/984963558">my review</a>)</li><li><a href="http://www.amazon.com/gp/product/B004J35LHQ">The Secrets of Consulting</a> by Gerald Weinberg (<a href="https://www.goodreads.com/review/show/519452572">my review</a>)</li><li><a href="http://www.amazon.com/gp/product/B00N1ZN6C0">Extreme Programming Explained</a> by Kent Beck (<a href="https://www.goodreads.com/review/show/1119479193">my review</a>)</li><li><a href="http://www.amazon.com/gp/product/B000FC1JAI">Meditations</a> by Marcus Aurelious and translated by Gregory Hays (<a href="https://www.goodreads.com/review/show/998010207">my review</a>)</li><li><a href="http://www.amazon.com/gp/product/B000QUELZ4">A Man Without a Country</a> by Kurt Vonnegut (<a href="https://www.goodreads.com/review/show/93929432">my review</a>)</li><li><a href="http://www.amazon.com/gp/product/B000S1M9LY">Infinite Jest</a> by David Foster Wallace (<a href="https://www.goodreads.com/review/show/509610965">my first review</a>, <a href="https://www.goodreads.com/review/show/880349659">second review</a>)</li><li><a href="http://www.amazon.com/gp/product/B00IHMF9KE">The Bone Clocks</a> by David Mitchell (<a href="https://www.goodreads.com/review/show/1030027657">my review</a>)</li></ul><p>I&apos;m recommending a specific translation of <em>Meditations</em>. I attempted to read different one first and it was so painful to read I ended up giving up. The linked translation is modern and contains a useful forward giving you background information on the time.</p><p>I only read one series this year but it was a good one. <a href="http://www.amazon.com/gp/bookseries/B00HUQBPWE">The Magicians</a>, by Lev Grossman, was recommended by a friend who described it as &quot;Harry Potter but with characters battling depression.&quot; I&apos;m not sure that fully captures the feel of the series but it is a start. The series introduces you to a world like our own but with magic. You follow cynical, self-absorbed students as they attend school, graduate, and grow up living in both the magical and non-magical world. The first book in the series is the weakest so if you read that and find it enjoyable you should definitely pick up the next two books.</p><h3 id="2015-goals">2015 Goals</h3><p>2015 isn&apos;t going to have an easily measured goal. I don&apos;t feel the need to set number of books or pages goals any more. I&apos;m hoping to increase the quality of my reading. This is a pretty unclear goal. To me this doesn&apos;t mean increasing the average rating of books I read but instead I want to get more out of what I read. I want to think a bit deeper about the subjects I&apos;m reading.</p><h3 id="2014-measurements">2014 Measurements</h3><p>Below are some random measurements that are probably only interesting to me.</p><p>This year I recorded the format of the books I read. This was the year of the ebook; over 90% of the books I read were electronic. I&apos;d guess that this is a higher percentage of ebooks than previous years. I wish I had recorded the formats read in previous years.</p><pre><code>| Binding   | Number of books |
|-----------+-----------------|
| Hardcover |               1 |
| Paperback |               4 |
| Kindle    |              64 |
</code></pre><p>My average rating has been going down over the last four years.</p><pre><code>| Year | Average Rating |
|------+----------------|
| 2011 | 3.84           |
| 2012 | 3.66           |
| 2013 | 3.67           |
| 2014 | 3.48           |
</code></pre><p>In 2014, three authors composed nearly 25% of my reading (by page count). The top six authors by page count are below.</p><pre><code>| Author               | My Average Rating | Number of Books | Number of Pages | Percent of Total Page Count |
|----------------------+-------------------+-----------------+-----------------+-----------------------------|
| David Mitchell       |                 4 |               5 |            2334 |                      10.19% |
| David Foster Wallace |       4.333333333 |               3 |            1753 |                       7.65% |
| Lev Grossman         |       3.666666667 |               3 |            1244 |                       5.43% |
| Marisha Pessl        |               3.5 |               2 |            1153 |                       5.03% |
| Haruki Murakami      |               3.5 |               2 |             768 |                       3.35% |
| Cormac McCarthy      |               3.5 |               2 |             650 |                       2.84% |
</code></pre><p>My top six authors by average rating (with ties broken by number of books) are below.</p><pre><code>| Author               | My Average Rating | Number of Books | Number of Pages | Percent of Total Page Count |
|----------------------+-------------------+-----------------+-----------------+-----------------------------|
| Gerald M. Weinberg   |                 5 |               1 |             228 |                       1.00% |
| Kent Beck            |                 5 |               1 |             224 |                       0.98% |
| Jay Fields           |                 5 |               1 |             204 |                       0.89% |
| Kurt Vonnegut        |               4.5 |               2 |             377 |                       1.65% |
| David Foster Wallace |       4.333333333 |               3 |            1753 |                       7.65% |
| David Mitchell       |                 4 |               5 |            2334 |                      10.19% |
</code></pre><p>I did top six for both of these because otherwise David Mitchell would not have been in the second one. I&apos;ve devoured his writing in the last year and a half for a reason. I&apos;m consistently rating his books highly.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2014/12/21/restricting-access-to-certain-routes/index.html</id>
    <link href="https://jakemccrary.com/blog/2014/12/21/restricting-access-to-certain-routes/index.html"/>
    <title><![CDATA[Restricting access to certain routes]]></title>
    <updated>2014-12-21T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Recently I&apos;ve been working on adding authentication and authorization to a Clojure web service. The project uses <a href="https://github.com/weavejester/compojure">compojure</a> for routing and <a href="https://github.com/cemerick/friend">friend</a> for authentication and authorization. My pair and I wanted to restrict access to specific routes while leaving some routes completely public. It took a few tries until we figured out how to do this in a way that made us happy.</p><p>The rest of this post shows the approximate path we took to our current solution. It focuses on using friend to restrict access to specific routes. It does not go into details about adding authentication to your web service.</p><p>Below is an example of the routes before adding authorization checks.</p><pre><code class="language-clojure">(ns example.server
  (:require [compojure.core :refer [GET defroutes] :as compojure]
            [compojure.route :as route]))

(defroutes app
  (GET &quot;/status&quot; _ (status))
  (GET &quot;/cars&quot; _ (fetch-cars))
  (GET &quot;/attributes&quot; _ (fetch-attributes))
  (GET &quot;/drivers&quot; _ (fetch-drivers))
  (route/not-found &quot;NOT FOUND&quot;))
</code></pre><p>We wanted to make <code>/cars</code>, <code>/attributes</code>, and <code>/drivers</code> require that the request satisfies the <code>:example.server/user</code> role. Requesting <code>/status</code> should not require authorization. The first attempt left us with the following code.</p><pre><code class="language-clojure">(ns example.server
  (:require [compojure.core :refer [GET defroutes] :as compojure]
            [compojure.route :as route]
            [cemerick.friend :as friend]))

(defroutes app
  (GET &quot;/status&quot; _ (status))
  (GET &quot;/cars&quot; _
       (friend/authorize #{::user}
                         (fetch-cars)))
  (GET &quot;/attributes&quot; _
       (friend/authorize #{::user}
                         (fetch-attributes)))
  (GET &quot;/drivers&quot; _
       (friend/authorize #{::user}
                         (fetch-drivers)))
  (route/not-found &quot;NOT FOUND&quot;))
</code></pre><p>The above works but it suffers from repetition. You could write a macro to minimize the repetition but we thought there must be a better way.</p><p>After reading more of <a href="https://github.com/cemerick/friend">friend</a>&apos;s documentation we discovered <code>friend/wrap-authorize</code>. This is middleware that only allows requests through if the request satisfies the required roles. Our first pass at using <code>friend/wrap-authorize</code> looked like the following example.</p><pre><code class="language-clojure">(ns example.server
  (:require [compojure.core :refer [GET defroutes] :as compojure]
            [compojure.route :as route]
            [cemerick.friend :as friend]))

(defroutes protected-routes
  (GET &quot;/cars&quot; _ (fetch-cars))
  (GET &quot;/attributes&quot; _ (fetch-attributes))
  (GET &quot;/drivers&quot; _ (fetch-drivers)))

(defroutes app
  (GET &quot;/status&quot; _ (status))
  (friend/wrap-authorize protected-routes #{::user})
  (route/not-found &quot;NOT FOUND&quot;))
</code></pre><p>This is much nicer. The repetition is removed by extracting routes that require authorization into a separate <code>defroutes</code> and wrapping it with <code>friend/wrap-authorize</code>.</p><p>This introduces a subtle bug. A response with status code 404 is no longer returned if a non-existent resource is requested and the request is unauthorized. This is because the authorization check happens <em>before</em> matching a route. friend&apos;s documentation warns against this and suggests using <code>compojure/context</code> to scope usage of <code>friend/wrap-authorize</code>. This doesn&apos;t solve the problem but it at least narrows its scope. We can do better.</p><p>Compojure <a href="https://github.com/weavejester/compojure/blob/master/HISTORY.md">1.2.0</a> introduced the function <code>wrap-routes</code>. <code>wrap-routes</code> applies middleware <em>after</em> a route is matched. By using this we can have all of the benefits of using <code>friend/wrap-authorize</code> without breaking returning 404 responses.</p><pre><code class="language-clojure">(ns example.server
  (:require [compojure.core :refer [GET defroutes] :as compojure]
            [compojure.route :as route]
            [cemerick.friend :as friend]))

(defroutes protected-routes
  (GET &quot;/cars&quot; _ (fetch-cars))
  (GET &quot;/attributes&quot; _ (fetch-attributes))
  (GET &quot;/drivers&quot; _ (fetch-drivers)))

(defroutes app
  (GET &quot;/status&quot; _ (status))
  (compojure/wrap-routes protected-routes
                         friend/wrap-authorize
                         #{::user})
  (route/not-found &quot;NOT FOUND&quot;))
</code></pre><p>There we have it. A solution without duplication that still responds properly to requests for non-existent resources. <code>compojure/wrap-routes</code> is a useful function to know about.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2014/12/09/an-effective-code-review-process/index.html</id>
    <link href="https://jakemccrary.com/blog/2014/12/09/an-effective-code-review-process/index.html"/>
    <title><![CDATA[An Effective Code Review Process]]></title>
    <updated>2014-12-09T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><blockquote><p>See all of my remote/working-from-home <a href="/blog/categories/remote/">articles here</a>.</p></blockquote><p><a href="https://twitter.com/searls/status/540603801955471360"><img alt="The way most organizations implement code reviews in teams is usually more harmful than helpful. I generally recommend going without them. - Justin Searls" src="/images/justin-searls-code-reviews-harmful.png" title="The way most organizations implement code reviews in teams is usually more harmful than helpful. I generally recommend going without them. - Justin Searls" /></a></p><p>The above was tweeted<a href="#fn-1" id="fnref1"><sup>1</sup></a> recently and it resulted in some decent discussion about code reviews. In the past six months at <a href="http://www.outpace.com">Outpace</a>, I&apos;ve been part of a handful of code review sessions that have been extremely productive. After the reviews many developers have expressed shock at the effectiveness of the process. A tweet-sized overview of the process we&apos;ve followed can be found in <a href="https://twitter.com/gigasquid">Carin Meier</a>&apos;s <a href="https://twitter.com/gigasquid/status/540606002547425281">responses</a> to the above tweet. Since you can&apos;t fit details into tweets the rest of this post expands on our code review process.</p><p>Some background before we dive into the details. <a href="http://www.outpace.com">Outpace</a> is a software company that practices, despite every programmer working remotely, nearly 100% pair programming. In addition, the team Carin and I are on do most of our work through GitHub pull requests. Before merging with master, the pull requests are reviewed by other teammates. Between pairing and pull requests many eyes see every line of code as changes are made.</p><p>Even with all this, we&apos;ve found value in having more traditional code reviews. We&apos;ve found that different feedback and action items emerge from reviewing code that we already have than from reviews of code changes (e.g., pull requests).</p><p>In addition to working for the team described above, the process below has been successfully used to review an internal library where the reviewers where mostly interested users with a couple contributors. It has also been successful on teams that were not adherent to doing work through reviewed pull requests.</p><h3 id="the-code-review-process">The Code Review Process</h3><h4 id="step-1:-select-the-code-to-review">Step 1: Select the code to review</h4><p>Typically we do this between a week and two weeks before the code review. Here we identify the code we want to review and create a two-hour meeting on a Friday at the end of day.</p><p>Having the meeting late on Friday helps create a relaxed environment. The review becomes a time to unwind, enjoy a beverage of choice, and talk about code. I haven&apos;t met a developer that doesn&apos;t enjoy discussing how to make code better and this lets everyone finish the week doing just that. The code review becomes an uplifting way to finish a week.</p><h4 id="step-2:-open-the-code-review">Step 2: Open the code review</h4><p>A few days (typically late Tuesday or early Wednesday) before the Friday code review meeting we start the review. We do this by opening a <a href="https://github.com">GitHub</a> pull request. The following steps will create a pull request where you can comment every line of code being reviewed.</p><ol><li>Create a local branch.</li><li>Delete the code being reviewed and commit locally.</li><li>Push the branch to GitHub.</li><li>Open a pull request.</li></ol><p>These steps are necessary because GitHub pull requests only let you view code that has changed. This process marks every line as deleted, which causes every line to appear the <em>Files changed</em> tab.</p><p>Opening the pull request a few days before the review meeting provides a location for pre-meeting comments to be added. This lets reviewers spend a couple days thinking about and commenting on the code. Comments on the pull request indicate a conversation should happen during the code review meeting.</p><h4 id="step-3:-the-code-review-meeting">Step 3: The code review meeting</h4><p>Its finally Friday and time to review the code as a group. Everyone joins a video conference and someone volunteers to lead the code review. At least one other person volunteers to be a note taker.</p><p>The leader directs the code review and keeps it moving forward. To do this the leader shares their screen with the video conference and scrolls through the <em>Files changed</em> view of the pull request. When a comment appears on screen the leader stops scrolling and discussion starts.</p><p>The comments are read (often silently) and discussion happens. The leader tries to recognize when a conclusion has been reached or when further discussion, outside of the code review, needs to happen. When a conclusion is reached someone (often the leader) states a quick summary and a note taker records the next steps. The next steps are added as additional comments in the comment thread being discussed. As the next steps are recorded the leader moves on to the next comment.</p><p>This continues until either time runs out or the group runs out of things to discuss.</p><p>After the code review a volunteer turns the next steps comments into Trello cards and we take care of the code review items as part of our usual work.</p><h3 id="results">Results</h3><p>We&apos;ve seen impressive improvements to code quality in the projects that have undergone this style of code review. Both small and large changes have happened as a result. Code has become simpler, clearer, and better understood. Additionally, the feeling of collective code ownership has increased.</p><p>Teammates have been surprised at how well this process has worked. More than a couple have said that historically they have not found code reviews useful but that these were.</p><p>This style of code review has worked in a few different settings and I encourage you to give it a shot.</p><ol class="footnotes"><li class="footnote" id="fn-1"><p>Reading through the discussion on Twitter after this tweet can give some hints as to what it takes to have an effective code review.<a href="#fnref1">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2014/09/07/ergodox-turn-on-led-when-not-on-the-main-layer/index.html</id>
    <link href="https://jakemccrary.com/blog/2014/09/07/ergodox-turn-on-led-when-not-on-the-main-layer/index.html"/>
    <title><![CDATA[ErgoDox: Turn on an LED When Not on the Main Layer]]></title>
    <updated>2014-09-07T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>The <a href="http://jakemccrary.com/blog/2014/07/27/building-the-ergodox-keyboard/">ErgoDox</a> is a great keyboard. One its appeals is that you can build your own firmware. This makes it possible to rearrange the keys however you want and tweak other functionality. The firmware is fairly advanced and allows you to have multiple layers to your keyboard.</p><p>Multiple layers allow the ErgoDox to have fewer physical keys than traditional keyboards. How often do you use an F key? If you are like me the answer is almost never. Why bother having a dedicated key?</p><p>Another benefit of multiple layers is that your keyboard is multiple keyboards in one. Do you use the Dvorak layout and your roommate use Qwerty? Program both a Dvorak layer and a Qwerty layer into your keyboard and switch between them with the push of a button.</p><p>The only downside I&apos;ve noticed of multiple layers is that I&apos;ll switch between them by accident. This is frustrating as all of a sudden your keyboard works differently and there is no indication that you are on a different layer.</p><p>The ErgoDox has a <a href="https://github.com/benblazak/ergodox-firmware/blob/513b82d585fdc7175db736163340af3ed6c6f38b/src/main.c#L124-L133">few LEDs</a> in it that I have never used. I don&apos;t even have the needed keys as part of my keyboard layout (Caps lock? Who uses caps lock? I don&apos;t need to shout that often). I decided to repurpose the num lock LED as an indicator that I&apos;m off the main layer.</p><p>This was a straight forward change. In the firmware there is a <a href="https://github.com/benblazak/ergodox-firmware/blob/513b82d585fdc7175db736163340af3ed6c6f38b/src/main.c#L171">variable</a> that holds what keyboard layer is active. All I had to do to get the num lock LED on when I changed layers was to move the <code>layers_head</code> variable higher in <code>main.c</code> and then change the conditional to turn on the num lock LED when <code>layers_head != 0</code>. <a href="https://github.com/jakemcc/ergodox-firmware/commit/383f16a3f091b4e2dd031d098007c4289cc1a261">This</a> is the commit that does this change. It could have been done as a three line change.</p><p>I highly recommend making this change. Now I just need to find a transparent DSA keycaps so I can see the LED easier.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2014/09/01/book-review-haskell-data-analysis-cookbook/index.html</id>
    <link href="https://jakemccrary.com/blog/2014/09/01/book-review-haskell-data-analysis-cookbook/index.html"/>
    <title><![CDATA[Book Review: Haskell Data Analysis Cookbook]]></title>
    <updated>2014-09-01T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Packt Publishing recently asked me to write a review of the book <a href="http://bit.ly/X0YQaL">Haskell Data Analysis Cookbook</a> by Nishant Shukla. The book is broken into small sections that show you how to do a particular task related to data analysis. These tasks vary from reading a csv file or parsing json to listening to a stream of tweets.</p><p>I&apos;m not a Haskell programmer. My Haskell experience is limited to reading some books (<a href="http://learnyouahaskell.com/">Learn You a Haskell for Great Good</a> and most of <a href="http://realworldhaskell.org/">Real World Haskell</a>) and solving some toy problems. All of reading and programming happened years ago though so I&apos;m out of practice.</p><p>This book is not for a programmer that is unfamiliar with Haskell. If you&apos;ve never studied it before you&apos;ll find yourself turning towards documentation. If you enter this book with a solid understanding of functional programming you can get by with a smaller understanding of Haskell but you will not get much from the book.</p><p>I&apos;ve only read a few cookbook style books and this one followed the usual format. It will be more useful as a quick reference than as something you would read through. It doesn&apos;t dive deep into any topic but does point you toward libraries for various tasks and shows a short example of using them.</p><p>A common critic I have of most code examples applies to this book. Most examples do not do qualified imports of namespaces or selective imports of functions from namespaces. This is especially useful when your examples might be read by people who are not be familiar with the languages standard libraries. Reading code and immediately knowing where a function comes from is incredibly useful to understanding.</p><p>The code for this book is available on <a href="https://github.com/BinRoot/Haskell-Data-Analysis-Cookbook">GitHub</a>. It is useful to look at the full example for a section. The examples in the book are broken into parts with English explanations and I found that made it hard to fully understand how the code fit together. Looking at the examples in the GitHub repo helped.</p><h4 id="recommendation">Recommendation</h4><p>I&apos;d recommend this book for Haskell programmers who find the table of contents interesting. If you read the table of contents and think it would be useful to have a shallow introduction to the topics listed then you&apos;ll find this book useful. It doesn&apos;t give a detailed dive into anything but at least gives you a starting point.</p><p>If you either learning Haskell or using Haskell then this book doesn&apos;t have much to offer you.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2014/07/27/building-the-ergodox-keyboard/index.html</id>
    <link href="https://jakemccrary.com/blog/2014/07/27/building-the-ergodox-keyboard/index.html"/>
    <title><![CDATA[Building the ErgoDox Keyboard]]></title>
    <updated>2014-07-27T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Earlier this year I built an <a href="https://deskthority.net/wiki/ErgoDox">ErgoDox</a>. The ErgoDox is a split hand mechanical keyboard whose design has been released under the GNU GPLv3. There are a few standard <a href="#fn-1" id="fnref1"><sup>1</sup></a> ways of getting the parts. It basically comes down to sourcing all the parts yourself or buying a bundle from <a href="https://www.massdrop.com/buy/ergodox">Massdrop</a>. I opted to wait until Massdrop was selling them and bought a kit from them.</p><p><img alt="My ErgoDox" src="/images/my-keyboard.jpg" title="My ErgoDox" /></p><h3 id="why?">Why?</h3><ol><li>I&apos;ve used an ergonomic keyboard for years and was intrigued by the split hand design.</li><li>I wanted to try out Cherry MX key switches.</li><li>Using your thumb for more than just space bar made a lot of sense to me.</li><li>The firmware lets you have multiple layers. I thought this could be really useful.</li><li>The project sounded fun. I used to make physical devices and this seemed like a good way to do that again.</li></ol><h3 id="buying">Buying</h3><p>As mentioned earlier I bought my parts from Massdrop. In the buy I participated in I had the option of a full hand case or the traditional case and I opted for the full hand. As part of the buy I also bought additional aluminum top layers, a blank set of DSA <a href="#fn-2" id="fnref2"><sup>2</sup></a> keycaps, and Cherry MX blue key switches.</p><p>If I were doing it again I would not buy the extra aluminum top layer. I built one of my hands using the aluminum and the other with the basic acrylic top. I enjoy both the look and feel of the acrylic hand better.</p><p>I would also not buy the set of DSA keycaps from Massdrop. It was convenient and a bit cheaper to buy from them but had I known I could get different <a href="http://keyshop.pimpmykeyboard.com/products/full-keysets/dsa-blank-sets-1">colors</a> from <a href="http://www.keycapsdirect.com/">Signature Plastics</a> I would have done that.</p><p>I also bought eight &quot;deep-dish&quot; DSA keys direct from Signature Plastics. These keys feel different which lets me know when my fingers are above the home row. I&apos;d recommend doing this. You can order from <a href="http://www.keycapsdirect.com/key-capsinventory.php">this</a> page.</p><p>For key switches I bought Cherry MX Blues through Massdrop. Blues are extremely clicky. You can easily hear me typing in every room of my apartment. It is very satisfying.</p><p>After using the keyboard for about a week I also ended up buying some <a href="http://www.amazon.com/gp/product/B00897D3OQ">pads</a> for my wrists. I occasionally rest my wrists on the keyboard and the keyboard&apos;s edge would dig into me.</p><h3 id="building">Building</h3><p>I followed Massdrop&apos;s step-by-step <a href="https://www.massdrop.com/ext/ergodox/assembly.php">guide</a> and <a href="https://www.youtube.com/watch?v=x1irVrAl3Ts">this</a> YouTube video. Another great resource is the community at <a href="http://geekhack.org/index.php?topic=22780.0">GeekHack</a>. I&apos;d recommend reading and watching as much as possible before starting your build.</p><p>I built this using a cheap soldering iron I&apos;ve had for years, very thin solder, solder wick, and a multimeter. I don&apos;t know if this would have been easier with better tools or not but those got the job done.</p><p>While soldering the surface mount diodes I was in the zone and soldered a few locations that didn&apos;t actually need to be soldered. When you are soldering the diodes you should only be soldering them to the locations that have the key silk screen.</p><p>My system for minimizing errors while soldering the diodes is the following five steps.</p><ol><li>Lay down some solder on one of the pads.</li><li>Put the correct end of the diode on top of that solder, reheat and push down.</li><li>Test the connection with a multimeter.</li><li>Solder the other half of the diode.</li><li>Test the connection.</li></ol><p>I batched up the steps. I&apos;d do a whole row of the first step, then move to the second for the entire row, then do the third, etc. Being rigorous about testing every connection is important. Catching mistakes early makes it easier to fix the mistakes.</p><p>If you solder a diode on the wrong way there is a huge difference (at least for me using solder wick) between the difficulty of fixing the error when only one pad has been soldered versus two pads. I soldered more than one diode backwards and a mistake noticed after soldering only one pad was easy to fix. After soldering both pads it took serious effort.</p><p>Eventually you&apos;ll need to cut open a USB cable. I ended up removing the plastic housing using a Dremel. When soldering the wires to the USB holes I was too concerned with it looking pretty and did not leave plenty of wire. This made it harder to solder and as a result I ended up doing a poor job that resulted in a short. After desoldering and destroying another cable, but leaving more wire, I managed to do a better job. I originally noticed the short because I kept getting warnings from my computer about my USB Keyboard drawing too much power.</p><p>I&apos;ve <a href="https://www.evernote.com/shard/s68/sh/4f51c3b2-b50b-47d3-8219-ea155cf5fef5/df239167726bcebf06cc2b5101ac8e42/">annotated a copy</a> of Massdrop&apos;s instructions using Evernote. It contains the above tips inline.</p><h3 id="firmware">Firmware</h3><p>After you physically build your keyboard you need to build the firmware. There are a few different firmwares that can work and you can discover those on GeekHack. I&apos;m using a fork of what Massdrop&apos;s <a href="https://www.massdrop.com/ext/ergodox">graphical configuration</a> tool uses. It is based off <a href="https://github.com/benblazak/ergodox-firmware">benblazak/ergodox-firmware</a>.</p><p>One of the exciting things about the ErgoDox is tweaking the firmware. I took the base firmware and modified it to have media key support and <a href="https://github.com/jakemcc/ergodox-firmware/commit/383f16a3f091b4e2dd031d098007c4289cc1a261">light up the LEDs</a> when I&apos;m on any layer besides the base. Some people have added the ability to record keyboard macros and other neat features. I encourage you to take a look at the source even if you use the graphical configuration tool. I haven&apos;t explored beyond <a href="https://github.com/benblazak/ergodox-firmware">benblazak/ergodox-firmware</a> so I can&apos;t compare it to other firmwares.</p><h3 id="conclusion">Conclusion</h3><p>I really enjoy it. Building it was both fun and frustrating <a href="#fn-3" id="fnref3"><sup>3</sup></a>.</p><p>After using the keyboard for a few months I&apos;ve found that I really only use three (on each hand) of the thumb cluster keys. I also don&apos;t use the keyboard layers too often. I have three layers programmed and I always stay on the main one unless I want to hit a media key.</p><p>Would I recommend building your own ErgoDox? If you already can or are willing to learn to solder and this sounds at all interesting to you I would recommend it. The project can be frustrating but the result is great.</p><h3 id="the-future">The Future</h3><p>There is still a lot left to explore in the custom keyboard space. Even so I have no plans on leaving the ErgoDox anytime soon. In terms of improving my ErgoDox, I plan on poking around the different firmwares at some point. I&apos;d also like to explore <a href="http://geekhack.org/index.php?topic=22780.msg1405792#msg1405792">tenting</a> <a href="https://github.com/adereth/ergodox-tent">options</a>.</p><h3 id="resources">Resources</h3><ul><li><a href="http://geekhack.org/index.php?topic=22780.0">GeekHack ErgoDox thread</a></li><li><a href="http://geekhack.org/index.php?topic=40501.0">GeekHack FAQ</a> Useful for general information about keyboard topics.</li><li><a href="http://geekhack.org/index.php?topic=40501.0#post_DD">GeekHack Cherry actuation forces</a></li><li><a href="https://www.massdrop.com/buy/ergodox">Massdrop Buy</a></li><li><a href="https://www.massdrop.com/ext/ergodox">Massdrop Configuration Tool</a></li><li><a href="https://github.com/benblazak/ergodox-firmware">benblazak/ergodox-firmware</a></li><li><a href="http://geekhack.org/index.php?topic=48106.0">TMK firmware for ErgoDox</a> One of the alternative firmwares.</li></ul><ol class="footnotes"><li class="footnote" id="fn-1"><p>I feel a bit odd using the word standard to describe acquiring parts to build a keyboard.<a href="#fnref1">↩</a></p></li><li class="footnote" id="fn-2"><p><a href="http://keycapsdirect.com/key-caps.php">This</a> page has diagrams that shows the different keycap families.<a href="#fnref2">↩</a></p></li><li class="footnote" id="fn-3"><p>Those surface mount diodes are so tiny.<a href="#fnref3">↩</a></p></li></ol></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2014/07/04/using-emacs-to-explore-an-http-api/index.html</id>
    <link href="https://jakemccrary.com/blog/2014/07/04/using-emacs-to-explore-an-http-api/index.html"/>
    <title><![CDATA[Using Emacs to Explore an HTTP API]]></title>
    <updated>2014-07-04T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>Recently I rediscovered an Emacs package that allows you to interact with HTTP endpoints from the comfort of an Emacs buffer. <a href="https://github.com/pashky/restclient.el">restclient.el</a> provides <code>restclient-mode</code>. This mode allows you to write and execute HTTP requests in an Emacs buffer. This package can be found in <a href="http://melpa.milkbox.net/#/restclient">MELPA</a>.</p><p>Below is an example buffer that touches the GitHub API.</p><pre><code class="language-ruby">:github = https://api.github.com

# get users orgs

GET :github/users/jakemcc/orgs

# rendor markdown

POST :github/markdown

{
  &quot;text&quot; : &quot;## Title&quot;
}

# rendor markdown raw

POST :github/markdown/raw
Content-Type: text/plain

Title
-----
</code></pre><p>The example above has a few interesting snippets. <code>:github</code> is an example of a variable. Lines 8-14 show an example of posting json to an endpoint. You put the data you want to send below the query. The last POST shows how to set headers for a request.</p><p>The location of your cursor decides what query to execute. Comments start with <code>#</code> and break your document into sections. The query in the same section as your cursor is the one that is executed. If the cursor is anywhere on lines 3-6 and I hit <code>C-c C-c</code> then Emacs queries GitHub for my organizations. Below is what pops up in a buffer.</p><pre><code class="language-javascript">
[
    {
        &quot;avatar_url&quot;: &quot;https:\/\/avatars.githubusercontent.com\/u\/1826953?&quot;,
        &quot;public_members_url&quot;: &quot;https:\/\/api.github.com\/orgs\/speakerconf\/public_members{\/member}&quot;,
        &quot;members_url&quot;: &quot;https:\/\/api.github.com\/orgs\/speakerconf\/members{\/member}&quot;,
        &quot;events_url&quot;: &quot;https:\/\/api.github.com\/orgs\/speakerconf\/events&quot;,
        &quot;repos_url&quot;: &quot;https:\/\/api.github.com\/orgs\/speakerconf\/repos&quot;,
        &quot;url&quot;: &quot;https:\/\/api.github.com\/orgs\/speakerconf&quot;,
        &quot;id&quot;: 1826953,
        &quot;login&quot;: &quot;speakerconf&quot;
    },
    {
        &quot;avatar_url&quot;: &quot;https:\/\/avatars.githubusercontent.com\/u\/4711436?&quot;,
        &quot;public_members_url&quot;: &quot;https:\/\/api.github.com\/orgs\/outpace\/public_members{\/member}&quot;,
        &quot;members_url&quot;: &quot;https:\/\/api.github.com\/orgs\/outpace\/members{\/member}&quot;,
        &quot;events_url&quot;: &quot;https:\/\/api.github.com\/orgs\/outpace\/events&quot;,
        &quot;repos_url&quot;: &quot;https:\/\/api.github.com\/orgs\/outpace\/repos&quot;,
        &quot;url&quot;: &quot;https:\/\/api.github.com\/orgs\/outpace&quot;,
        &quot;id&quot;: 4711436,
        &quot;login&quot;: &quot;outpace&quot;
    }
]
// HTTP/1.1 200 OK
// Server: GitHub.com
// Date: Fri, 04 Jul 2014 17:34:26 GMT
// Content-Type: application/json; charset=utf-8
// other headers removed for space consideration on blog

</code></pre><p><code>C-c C-c</code> triggers <code>restclient-http-send-current</code> which runs a query and pretty prints the result. I could have used <code>C-c C-r</code> to trigger <code>restclient-http-send-current-raw</code> which executes a query and shows the raw result.</p><p>It isn&apos;t a perfect mode. One issue I&apos;ve come across is that queries targeting <code>localhost</code> fail. The solution is to query <code>127.0.0.1</code>.</p><p><code>restclient-mode</code> makes Emacs a useful tool for exploring and testing HTTP APIs. Since it operates on a simple text format it allows you to easily share executable documentation with others. I highly recommend <strong>restclient.el</strong>.</p></div>]]></content>
  </entry>
  <entry>
    <id>https://jakemccrary.com/blog/2014/06/22/comparing-clojure-testing-libraries-output/index.html</id>
    <link href="https://jakemccrary.com/blog/2014/06/22/comparing-clojure-testing-libraries-output/index.html"/>
    <title><![CDATA[Comparing Clojure Testing Libraries: Output]]></title>
    <updated>2014-06-22T23:59:59+00:00</updated>
    <content type="html"><![CDATA[<div><p>I recently became interested in how Clojure testing libraries help you when there is a test failure. This interest resulted in me <a href="https://github.com/jakemcc/clojure-test-bed">exploring</a> different Clojure testing libraries. I created the same tests using <strong>clojure.test</strong> (with and without <a href="https://github.com/pjstadig/humane-test-output">humane-test-output</a>), <a href="http://jayfields.com/expectations/">expectations</a>, <a href="https://github.com/marick/Midje">Midje</a>, and <a href="http://speclj.com/">Speclj</a> and looked at the output.</p><p>I ran all of these examples using Leiningen. <strong>Midje</strong>, <strong>Speclj</strong>, and <strong>expectations</strong> color their output but I&apos;m not going to try to reproduce that here. The color added by <strong>Midje</strong> and <strong>expectations</strong> is useful. <strong>Speclj</strong> color hurt its readability. I use a dark colored terminal and <strong>Speclj</strong> colors the line that tells where the failure occurs black. This made it hard to read.</p><p>I&apos;m not going to show what the tests look like for each testing library past the first comparison. How a test in expressed is important but not what I want to focus on in this post.</p><h2 id="comparing-strings">Comparing Strings</h2><p>Going to start off with a basic string comparison. The failing test compares two strings that only differ by one character.</p><h5 id="clojure.test">clojure.test</h5><p>Most (hopefully all) Clojure programmers should be familiar with <strong>clojure.test</strong>. It is the testing library that is included with Clojure.</p><pre><code class="language-clojure">(ns example.string-test
  (:require [clojure.test :refer :all]))

(deftest string-comparisons
  (is (= &quot;strings equal&quot; &quot;strings equal&quot;))
  (is (= &quot;space&quot; &quot;spice&quot;)))
</code></pre><p>The output below is what you get when the above test runs. Even in this simple example it isn&apos;t the easiest to read. It doesn&apos;t make it easy to find the expected or actual values.</p><pre><code class="language-console">FAIL in (string-comparisons) (string_test.clj:6)
expected: (= &quot;space&quot; &quot;spice&quot;)
  actual: (not (= &quot;space&quot; &quot;spice&quot;))
</code></pre><p>Below is the same test but with <strong>humane-test-output</strong> enabled. It is easy to read the output and see the expected and actual value. It even provides a diff between them although in this situation it isn&apos;t that useful.</p><pre><code class="language-console">FAIL in (string-comparisons) (string_test.clj:6)
expected: &quot;space&quot;
  actual: &quot;spice&quot;
    diff: - &quot;space&quot;
          + &quot;spice&quot;
</code></pre><h5 id="expectations">expectations</h5><p>Another testing library is Jay Field&apos;s <a href="http://jayfields.com/expectations/">expectations</a>. You can see from the example that it has a fairly minimal syntax.</p><pre><code class="language-clojure">(ns example.string-expectations
  (:require [expectations :refer :all]))

(expect &quot;strings equal&quot; &quot;strings equal&quot;)
(expect &quot;space&quot; &quot;spice&quot;)
</code></pre><pre><code class="language-console">failure in (string_expectations.clj:5) : example.string-expectations
(expect &quot;space&quot; &quot;spice&quot;)

           expected: &quot;space&quot;
                was: &quot;spice&quot;

           matches: &quot;sp&quot;
           diverges: &quot;ace&quot;
                  &amp;: &quot;ice&quot;
</code></pre><p>The output from <strong>expectations</strong> is very readable. You can easily pick out the expected and actual values. It also shows you where the string starts to diverge.</p><h5 id="speclj">Speclj</h5><p>Before writing this post I had zero experience with Micah Martin&apos;s <a href="http://speclj.com/">Speclj</a>. Below is my translation of the failing string test and its output.</p><pre><code class="language-clojure">(ns example.string-spec
  (:require [speclj.core :refer :all]))

(describe &quot;String comparisons&quot;
  (it &quot;have nice error message&quot;
      (should= &quot;space&quot; &quot;spice&quot;)))
</code></pre><pre><code class="language-console">9) String comparisons have nice error message
   Expected: &quot;space&quot;
        got: &quot;spice&quot; (using =)
   /Users/jake/src/jakemcc/example/spec/example/string_spec.clj:7
</code></pre><p><strong>Speclj</strong>&apos;s test output above is an improvement over <strong>clojure.test</strong>. You can easily find the expected and actual values. It doesn&apos;t provide any help with diagnosing how those values are different.</p><h5 id="midje">Midje</h5><p>I have a little bit of experience with Brian Marick&apos;s <a href="https://github.com/marick/Midje">Midje</a>. Unlike the other libraries it switches up the assertion syntax. In <strong>Midje</strong> the expected value is on the right side of <code>=&gt;</code>.</p><pre><code class="language-clojure">(ns example.string-test
  (:require [midje.sweet :refer :all]))

(fact &quot;strings are equal&quot;
  &quot;string is equal&quot; =&gt; &quot;string is equal&quot;)

(fact &quot;strings not equal&quot;
   &quot;spice&quot; =&gt; &quot;space&quot;)
</code></pre><pre><code class="language-console">FAIL &quot;strings not equal&quot; at (string_test.clj:8)
    Expected: &quot;space&quot;
      Actual: &quot;spice&quot;
</code></pre><p><strong>Midje</strong>&apos;s output is similar to <strong>Speclj</strong>&apos;s. You can quickly find the expected and actual values but it doesn&apos;t help you spot the difference.</p><h3 id="string-comparison-winner">String Comparison Winner</h3><p><strong>expectations</strong> wins for best output. You can easily spot the expected and actual values and it also helps you find the difference between the strings.</p><p>The worst output comes from <strong>clojure.test</strong>. It doesn&apos;t make it easy to spot the difference or even find the expected and actual values.</p><h2 id="comparing-maps">Comparing Maps</h2><p>For maps I&apos;ve setup three assertions. The first has an extra key-value pair in the actual value. The second has an extra in the expected value. The final assertion has a different value for the <code>:cheese</code> key. The <strong>clojure.test</strong> example is below.</p><pre><code class="language-clojure">(deftest map-comparisons
  (is (= {:sheep 1} {:cheese 1 :sheep 1}))
  (is (= {:sheep 1 :cheese 1} {:sheep 1}))
  (is (= {:sheep 1 :cheese 1} {:sheep 1 :cheese 5})))
</code></pre><pre><code class="language-console">FAIL in (map-comparisons) (map_test.clj:5)
expected: (= {:sheep 1} {:cheese 1, :sheep 1})
  actual: (not (= {:sheep 1} {:cheese 1, :sheep 1}))

FAIL in (map-comparisons) (map_test.clj:6)
expected: (= {:sheep 1, :cheese 1} {:sheep 1})
  actual: (not (= {:cheese 1, :sheep 1} {:sheep 1}))

FAIL in (map-comparisons) (map_test.clj:7)
expected: (= {:sheep 1, :cheese 1} {:sheep 1, :cheese 5})
  actual: (not (= {:cheese 1, :sheep 1} {:cheese 5, :sheep 1}))
</code></pre><p>Unsurprisingly the default <strong>clojure.test</strong> output for maps suffers from the same problems found in the string comparisons. To find the actual and expected values you need to manually parse the output.</p><pre><code class="language-console">FAIL in (map-comparisons) (map_test.clj:5)
expected: {:sheep 1}
  actual: {:cheese 1, :sheep 1}
    diff: + {:cheese 1}

FAIL in (map-comparisons) (map_test.clj:6)
expected: {:cheese 1, :sheep 1}
  actual: {:sheep 1}
    diff: - {:cheese 1}

FAIL in (map-comparisons) (map_test.clj:7)
expected: {:cheese 1, :sheep 1}
  actual: {:cheese 5, :sheep 1}
    diff: - {:cheese 1}
          + {:cheese 5}
</code></pre><p>Above is the output of using <strong>clojure.test</strong> with <strong>humane-test-output</strong>. It is a big improvement over the default <strong>clojure.test</strong>. You can quickly see the expected and actual values. Unlike with the string assertions the diff view is actually helpful. The diffs do a good job of helping you identify the error.</p><pre><code class="language-console">failure in (map_expectations.clj:6) : example.map-expectations
(expect {:sheep 1} {:sheep 1, :cheese 1})

           expected: {:sheep 1}
                was: {:cheese 1, :sheep 1}

           in expected, not actual: null
           in actual, not expected: {:cheese 1}

failure in (map_expectations.clj:7) : example.map-expectations
(expect {:sheep 1, :cheese 1} {:sheep 1})

           expected: {:cheese 1, :sheep 1}
                was: {:sheep 1}

           in expected, not actual: {:cheese 1}
           in actual, not expected: null

failure in (map_expectations.clj:8) : example.map-expectations
(expect {:sheep 1, :cheese 5} {:sheep 1, :cheese 1})

           expected: {:cheese 5, :sheep 1}
                was: {:cheese 1, :sheep 1}

           in expected, not actual: {:cheese 5}
           in actual, not expected: {:cheese 1}
</code></pre><p><strong>expectations</strong> does a pretty good job helping you as well. As before, you can clearly read the expected and actual values. <strong>expectations</strong> also provides some hint as to what is different between the maps. I find the English descriptions a bit easier to read than <strong>humane-test-output</strong>&apos;s diff view. Still seeing lines like line 7 (<code>in expected, not actual: null</code>) is a bit confusing and the output would be improved if it was suppressed.</p><p>I&apos;m just going to lump <strong>Speclj</strong> and <strong>Midje</strong> together. The output for each is below. They both improve over <strong>clojure.test</strong> by making it easy to see the expected and actual value. They both don&apos;t do anything beyond that.</p><pre><code class="language-console">4) map comparisons have nice error messages when extra entries keys present
   Expected: {:sheep 1}
        got: {:cheese 1, :sheep 1} (using =)
   /Users/jake/src/jakemcc/example/spec/example/map_spec.clj:7

5) map comparisons have nice error messages when missing an entry
   Expected: {:cheese 1, :sheep 1}
        got: {:sheep 1} (using =)
   /Users/jake/src/jakemcc/example/spec/example/map_spec.clj:9

6) map comparisons have nice error messages when mismatched values
   Expected: {:cheese 5, :sheep 1}
        got: {:cheese 1, :sheep 1} (using =)
   /Users/jake/src/jakemcc/example/spec/example/map_spec.clj:11
</code></pre><pre><code class="language-console">FAIL &quot;map is missing an entry&quot; at (map_test.clj:5)
    Expected: {:cheese 1, :sheep 1}
      Actual: {:sheep 1}

FAIL &quot;map has an extra entry&quot; at (map_test.clj:8)
    Expected: {:sheep 1}
      Actual: {:cheese 1, :sheep 1}

FAIL &quot;map has a different value&quot; at (map_test.clj:11)
    Expected: {:cheese 5, :sheep 1}
      Actual: {:cheese 1, :sheep 1}
</code></pre><h3 id="map-comparison-winner">Map Comparison Winner</h3><p>Tie between <strong>humane-test-output</strong> and <strong>expectations</strong>. Both do a good job of helping the reader spot the difference.</p><h2 id="comparing-sets">Comparing Sets</h2><p>Next up are sets. Only two assertions for this section. One with the actual value having an extra member and one test where it is missing a member.</p><pre><code class="language-clojure">(ns example.set-test
  (:require [clojure.test :refer :all]))

(deftest set-comparisons
  (is (= #{:a :b} #{:a :b :c}))
  (is (= #{:a :b :c} #{:a :b})))
</code></pre><p>First up is the basic <strong>clojure.test</strong> output. It suffers from the same problem it has suffered this entire time. It doesn&apos;t make it easy to read the expected and actual values.</p><pre><code class="language-console">FAIL in (set-comparisons) (set_test.clj:5)
expected: (= #{:b :a} #{:c :b :a})
  actual: (not (= #{:b :a} #{:c :b :a}))

FAIL in (set-comparisons) (set_test.clj:6)
expected: (= #{:c :b :a} #{:b :a})
  actual: (not (= #{:c :b :a} #{:b :a}))
</code></pre><p>No surprises with <strong>humane-test-output</strong>. It improves the <strong>clojure.test</strong> output by making it easy to read the expected and actual values. The diff view also helps figure out what is causing the assertion to fail.</p><pre><code class="language-console">FAIL in (set-comparisons) (set_test.clj:5)
expected: #{:b :a}
  actual: #{:c :b :a}
    diff: + #{:c}

FAIL in (set-comparisons) (set_test.clj:6)
expected: #{:c :b :a}
  actual: #{:b :a}
    diff: - #{:c}
</code></pre><p><strong>expectations</strong> once again delivers nice output. It continues to be easy to find the expected and actual values and helps you spot the differences with a diff view.</p><pre><code class="language-console">failure in (set_expectations.clj:4) : example.set-expectations
(expect #{:b :a} #{:c :b :a})

           expected: #{:b :a}
                was: #{:c :b :a}

           in expected, not actual: null
           in actual, not expected: #{:c}

failure in (set_expectations.clj:5) : example.set-expectations
(expect #{:c :b :a} #{:b :a})

           expected: #{:c :b :a}
                was: #{:b :a}

           in expected, not actual: #{:c}
           in actual, not expected: null
</code></pre><p><strong>Speclj</strong> and <strong>Midje</strong> both have better output than the basic <strong>clojure.test</strong>.</p><pre><code class="language-console">7) set comparisons have nice error messages when missing item
   Expected: #{:b :a}
        got: #{:c :b :a} (using =)
   /Users/jake/src/jakemcc/example/spec/example/set_spec.clj:9

8) set comparisons have nice error messages when more items
   Expected: #{:c :b :a}
        got: #{:b :a} (using =)
   /Users/jake/src/jakemcc/example/spec/example/set_spec.clj:11
</code></pre><pre><code class="language-console">FAIL &quot;set is superset of expected&quot; at (set_test.clj:5)
    Expected: #{:a :b}
      Actual: #{:a :b :c}

FAIL &quot;set is subset of expected&quot; at (set_test.clj:8)
    Expected: #{:a :b :c}
      Actual: #{:a :b}
</code></pre><h3 id="set-comparison-winner">Set Comparison Winner</h3><p>Similar to the winner of the map comparisons I&apos;m going to split the victory between <strong>expectations</strong> and <strong>humane-test-output</strong>.</p><h2 id="comparing-lists">Comparing Lists</h2><p>Next up we compare lists (and lists to vectors). There are three comparisons; one with an extra element, one with same length but a mismatched element, and one comparing a vector and list with drastically different contents.</p><pre><code class="language-clojure">(ns example.seq-test
  (:require [clojure.test :refer :all]))

(deftest list-comparisons
  (is (= &apos;(1 2 3) &apos;(1 2 3 4)))
  (is (= &apos;(1 2 4) &apos;(1 2 3)))
  (is (= &apos;(9 8 7) [1 2 3])))
</code></pre><p>First up <strong>clojure.test</strong>. Same issues as with all the previous comparisons.</p><pre><code class="language-console">FAIL in (list-comparisons) (seq_test.clj:5)
expected: (= (quote (1 2 3)) (quote (1 2 3 4)))
  actual: (not (= (1 2 3) (1 2 3 4)))

FAIL in (list-comparisons) (seq_test.clj:6)
expected: (= (quote (1 2 4)) (quote (1 2 3)))
  actual: (not (= (1 2 4) (1 2 3)))

FAIL in (list-comparisons) (seq_test.clj:7)
expected: (= (quote (9 8 7)) [1 2 3])
  actual: (not (= (9 8 7) [1 2 3]))
</code></pre><p>Once again <strong>humane-test-output</strong> improves upon <strong>clojure.test</strong>. Only interesting difference from previous comparisons is that the diff view ends up having <code>nil</code> values in it where the elements are the same.</p><pre><code class="language-console">FAIL in (list-comparisons) (seq_test.clj:5)
expected: (1 2 3)
  actual: (1 2 3 4)
    diff: + [nil nil nil 4]

FAIL in (list-comparisons) (seq_test.clj:6)
expected: (1 2 4)
  actual: (1 2 3)
    diff: - [nil nil 4]
          + [nil nil 3]

FAIL in (list-comparisons) (seq_test.clj:7)
expected: (9 8 7)
  actual: [1 2 3]
    diff: - [9 8 7]
          + [1 2 3]
</code></pre><p><strong>expectations</strong> continues to have good output. It tries to help you out as well. You&apos;ll notice that it also has <code>nil</code> values inserted where the lists are the same.</p><pre><code class="language-console">failure in (list_expectations.clj:4) : example.list-expectations
(expect &apos;(1 2 3) &apos;(1 2 3 4))

           expected: (1 2 3)
                was: (1 2 3 4)

           in expected, not actual: null
           in actual, not expected: [nil nil nil 4]
           actual is larger than expected

failure in (list_expectations.clj:5) : example.list-expectations
(expect &apos;(1 2 4) &apos;(1 2 3))

           expected: (1 2 4)
                was: (1 2 3)

           in expected, not actual: [nil nil 4]
           in actual, not expected: [nil nil 3]

failure in (list_expectations.clj:6) : example.list-expectations
(expect &apos;(9 8 7) [1 2 3])

           expected: (9 8 7)
                was: [1 2 3]

           in expected, not actual: [9 8 7]
           in actual, not expected: [1 2 3]
</code></pre><p>Unsurprisingly, <strong>Speclj</strong> and <strong>Midje</strong> are better than <strong>clojure.test</strong> but again don&apos;t go beyond making easy to find the expected and actual values.</p><pre><code class="language-console">1) List/vector comparisons when there is an extra element
   Expected: (1 2 3)
        got: (1 2 3 4) (using =)
   /Users/jake/src/jakemcc/example/spec/example/string_spec.clj:7

2) List/vector comparisons when there is a mismatched element
   Expected: (1 2 4)
        got: (1 2 3) (using =)
   /Users/jake/src/jakemcc/example/spec/example/string_spec.clj:9

3) List/vector comparisons when comparing different types
   Expected: (9 8 7)
        got: [1 2 3] (using =)
   /Users/jake/src/jakemcc/example/spec/example/string_spec.clj:11
</code></pre><pre><code class="language-console">FAIL &quot;lists are different sizes&quot; at (seq_test.clj:5)
    Expected: (1 2 3)
      Actual: (1 2 3 4)

FAIL &quot;lists have different entries&quot; at (seq_test.clj:8)
    Expected: (1 2 4)
      Actual: (1 2 3)

FAIL &quot;compare very different list like values&quot; at (seq_test.clj:14)
    Expected: (9 8 7)
      Actual: [1 2 3]
</code></pre><h3 id="list-comparison-winner">List Comparison Winner</h3><p>I find the <strong>clojure.test</strong> with <strong>humane-test-output</strong> to be a bit easier to read than <strong>expectations</strong>. Both have better output than the basic <strong>clojure.test</strong>, <strong>Speclj</strong>, and <strong>Midje</strong>.</p><h2 id="overall-winner">Overall Winner</h2><p>If I were picking a testing library based entirely on what a failing test looks like I would use <strong>expectations</strong>. My second pick would be <strong>clojure.test</strong> with <strong>humane-test-output</strong>.</p><p>It is great that Clojure ships with <strong>clojure.test</strong>. It is unfortunate that it does so little to help you read a failing test. Every library I tried has better output than <strong>clojure.test</strong>.</p><h3 id="addendum">Addendum</h3><p><em>Added 2014/06/23</em></p><p>Colin Jones <a href="http://jakemccrary.com/blog/2014/06/22/comparing-clojure-testing-libraries-output/#comment-1449451549">points out</a> that Speclj provides <code>should==</code>. <code>should==</code> checks that the expected and actual value have the same contents. He provided a <a href="https://gist.github.com/trptcolin/7e1ece5179581085730f">gist</a> that shows the difference.</p></div>]]></content>
  </entry>
</feed>
