Dear Pixel Customer #894675,
Thank you for the sketch and written description. This blueprint of your device aligns with our technical abilities and we believe that our fabrication team will have no problem building the casing and power instrumentation per your specifications.
Based on your diagram, we've determined that the following components need to be built:
Our engineers have provided a basic prototype that should work as the foundation for your new system.
As you can see, it includes ventilation, data storage, user interfaces and network connectivity. We even included the blinkenlights you asked for in the description you sent.
There are also several external backup power supplies on the sides and a SCADA controller for driving large, external stepper motors for those turrets you needed for your homebase.
Unfortunately, we were confused about the description you provided for the functionality, punched decals and payment options.
When you say "a missile targeting system" are you describing your need for real time guidance controls or pre-calculated trajectory forecasting for MAD? While, we are quite capable of providing either, we would like to make you aware that the cost is substantially higher with real-time control calculations.
We were also confused by your description of some of the symbols you wished stamped into the casing on the front and side panels. Our interpretation of "two forward-leaning, rectangular S-shapes that intersect each other at 90 degrees" seems to have generated something that has made our team a bit uneasy. Could you clarify with an actual drawing of the symbol? A quick text of the image to me will suffice. 555-668-6294
Our design team has also provided some feedback to me that I'm forwarding to you on the aesthetics you desired. The green military tones specified shouldn't be a problem and I have included an image for your review.
You'll see that it includes a horizontal bar graph to count those very important logistic issues you are facing. We had to shrink the voids a bit, but I think you'll find our solution much better!
Unfortunately, one of our in-house mythical beings has decided to make a home in your machine and we were unable to get him out. Miniature black unicorns are really hard to get to in tight, dark spaces.
I hope this is OK. I promise he won't break anything.
If that situation won't work for you, we'd like to propose an existing system our design teams have recently created that closely aligns with your expectations. It's called "Let Freedom Ring". Sounds good, right?
We've also included a preview of it for your review. Please note the various patriotic themes we've added. We hope they will inspire you and your associates to climb large geographical features and plant flags at the top of them.
We'd love to know what you think!
Now to payment. While the promise of land and leadership opportunities in your organization's "new dawn" are tempting, we prefer the tender of the dollar to IOUs that we can't use to pay our business expenses. With so many positions supporting your grand leader, I'm sure you can understand that large ventures take large amounts of capital.
Thank you for your understanding. I await your prompt response.
Peter Pixelship
Every so often I get curious about my shell command usage on Arch
Linux so that I can start thinking of ways to improve my command-line
efficiency. I had already recorded a few thousand lines in
~/.zsh_history
on this particular computer so I knew I could extract
some useful data from which to learn.
I am not well-versed in zsh beyond minor customizations, so I wasn’t aware if zsh had any built-in statistics that would give me a summary and I didn’t feel like writing a parser to analyze the file so I looked on the internet. I eventually found that atuin could generate a summary that would work for my research.
After installing it, I ran atuin stats
which presented my top 10
commands:
[▮▮▮▮▮▮▮▮▮▮] 1418 cd
[▮▮▮▮▮▮▮ ] 1041 ls
[▮▮▮▮ ] 673 exit
[▮▮ ] 331 iex
[▮ ] 263 cargo run
[▮ ] 235 git status
[▮ ] 211 emacs
[▮ ] 205 killall
[▮ ] 181 mix
[▮ ] 163 pacman
Total commands: 7818
Unique commands: 1610
I suspected the first 3 commands would be near the top, but the rest surprised me a little until I thought about the recent write-compile-repeat cycle I had been on some software projects lately.
These results manifested multiple questions within me:
I decided I needed to focus on cd
, but in order for me to do that, I
need to find out all the directories I was visiting and how often I
was visiting them.
I remembered reading that atuin
extracts the history data from my
shell and stores it in a SQLite database somewhere. After some
hunting in my local dotfiles, I found a history.db
file in
~/.local/share/atuin
.
So I ran sqlite3 history.db
, got my prompt and started investigating
more:
SQLite version 3.45.0 2024-01-15 17:01:13
Enter ".help" for usage hints.
sqlite> .tables
_sqlx_migrations history
sqlite> .schema history
CREATE TABLE history (
id text primary key,
timestamp integer not null,
duration integer not null,
exit integer not null,
command text not null,
cwd text not null,
session text not null,
hostname text not null, deleted_at integer,
unique(timestamp, cwd, command)
);
CREATE INDEX idx_history_timestamp on history(timestamp);
CREATE INDEX idx_history_command on history(command);
CREATE INDEX idx_history_command_timestamp on history(
command,
timestamp
);
sqlite>
This was excellent. Now, I just needed to see what the data looks like:
SELECT *
FROM history
LIMIT 1;
-- Results
018d32cffd8e7c1599e40599343ba55f|1694721762001000000|0|-1|cat .zsh|unknown|018d32cffd8e785dbeab027db0d677a1|killship:angelo|
I cared only about timestamp
and command
, so I SELECT
ed them only:
SELECT timestamp, command
FROM history
ORDER BY timestamp ASC
LIMIT 1;
-- Results
1694721762001000000|cat .zsh
I dont’t understand unix timestamp epochs intuitively, so after some
googling, I found the Date and Time
Functions page for SQLite
which pointed me to the datetime
function:
SELECT datetime(timestamp/1000000000, 'unixepoch'), command
FROM history
ORDER BY timestamp ASC
LIMIT 1;
-- Results
2023-09-14 20:02:42|cat .zsh
This was so much better. Now I just needed some filtering…
SELECT datetime(timestamp/1000000000, 'unixepoch'), command
FROM history
WHERE command LIKE 'cd%'
ORDER BY timestamp ASC
LIMIT 1
-- Results
2023-09-14 20:27:17|cd code/aur
But this didn’t quite give me what I wanted. I really want to see my
most frequent cd
arguments and I don’t really care about the
timestamps, so I dropped that part of the query and the LIMIT
, then
I added a COUNT
and GROUP BY
clause to tally up the most frequent
arguments:
SELECT command, COUNT(command)
FROM history
WHERE command LIKE 'cd%'
GROUP BY command
ORDER BY count(command) DESC;
-- Results
cd ..|238
cd code|109
cd projects|93
cd|73
cd totemic|60
cd tada-beam|38
cd tada|35
cd code/projects|30
cd alakra|28
cd Downloads|27
cd code/aur|25
cd dotfiles|19
cd written|18
cd learn|15
cd github|14
...
And because I wanted to be fancy, I asked ChatGPT to generate the output a bar chart using ASCII with the following prompt:
Write a SQL query that generates a ASCII-based bar chart within sqlite from a table with 2 fields. The two fields are as follows: the first field is a name and is of type string and the second field is a number is of type integer. Please give an example of the expected output.
This gave me some explanations and the following query:
WITH RECURSIVE
cnt(x) AS (
SELECT 1
UNION ALL
SELECT x+1 FROM cnt
LIMIT (SELECT MAX(number) FROM your_table_name)
),
bar_chart AS (
SELECT
name,
number,
(SELECT GROUP_CONCAT('▮', '') FROM cnt WHERE x <= number) AS bar
FROM your_table_name
)
SELECT name, bar FROM bar_chart;
I then modified this with my own query:
WITH RECURSIVE
cnt(x) AS (
SELECT 1
UNION ALL
SELECT x+1 FROM cnt
LIMIT (SELECT COUNT(command) FROM history WHERE command LIKE 'cd%')
),
bar_chart AS (
SELECT
command,
COUNT(command),
(SELECT GROUP_CONCAT('▮', '') FROM cnt WHERE x <= COUNT(command)) AS bar
FROM history
WHERE command LIKE 'cd%'
GROUP BY command
ORDER BY COUNT(command) DESC
)
SELECT command, bar FROM bar_chart;
And got these results (reformatted for clarity):
| command | bar |
|--------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| cd .. | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd code | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd projects | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd totemic | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd tada-beam | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd tada | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd code/projects | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd alakra | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd Downloads | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd code/aur | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd dotfiles | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd written | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd learn | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd github | ▮▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd angelolakra | ▮▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd discord_arch_electron | ▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd go | ▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd notes | ▮▮▮▮▮▮▮▮▮▮▮▮ |
| cd notes/pain | ▮▮▮▮▮▮▮▮▮▮▮ |
| ... | ... |
Most of these paths live under ~/code/projects
on my system and that
directory tree at 2-levels of depth (via tree -d -L 2
) looks like:
├── alakra
│ ├── ditl
│ ├── dotfiles
│ ├── haikus
│ ├── journal
│ ├── scraps
│ └── todo
├── angelolakra
│ ├── devops
│ ├── diatheke
│ ├── time_tracking
│ ├── written
│ └── www
├── data
│ ├── analysis
│ ├── astronomical-objects
│ ├── closed
│ ├── generated
│ ├── jobs
│ ├── lists
│ └── mastermind
├── eve
│ ├── assets
│ ├── data
│ ├── digiship
│ ├── junk
│ ├── notebooks
│ └── reports
├── fpga
│ ├── ecet-4730
│ └── test
├── games
│ └── ticket-to-ride-core
├── jobs
│ ├── interview-project-api-battleship_Angelo-Lakra
│ └── sturdy-eureka
├── junk
│ ├── aquaduct
│ ├── extraction
│ ├── gettit
│ ├── royal
│ └── scraps
├── learn
│ ├── advent-of-code
│ ├── build-a-text-editor
│ ├── cl
│ ├── coursera
│ ├── exercism
│ ├── mazes
│ ├── my-app
│ ├── my-redis
│ ├── nasati
│ ├── programming-pearls
│ ├── project-euler
│ ├── quote-editor
│ ├── runit
│ └── scheme
├── masterminds
│ └── masterminds
├── roguepowered
│ ├── astroheart
│ ├── ditl
│ ├── imports-to-games
│ ├── interstellar-stuff
│ ├── the-coat
│ ├── the-hour-of-tower
│ ├── the-list
│ └── workspace
└── totemic
├── app
├── derts
├── devops
├── notes
├── reboot
├── rogue
├── rogue-strategies
├── rust-test
├── safari
├── site
├── standard
├── tada
├── tada-elixir
├── tada-grammars
├── tada-old
├── tools
├── tribal
├── winnow
└── www
So from the tree
output and the SQL query I realized that:
After thinking about this a bit, I thought about my sock drawer. I organize everything in there by my frequency of access. My top drawer contains white undershirts, socks and underwear. I use these all the time and they are very accessible. What if I treated my projects folder like this too?
So I decided to:
~/
~/
I think this small change will be a little faster for me to access things, but maybe I’m kidding myself because it’s only a few more characters. I felt like I realized this anecdotally, but this was a fun exercise to prove it to myself. I did get some good benefits out of this experience:
I changed my mind recently and decided to rework my website with Tailwind CSS and use its built in grid and flexbox properties. [2024-1-17]
I was developing in a world full of CSS hacks going back from 2000 to 2010. I was used to floating, inline-blocking and clearfixes to manage layout. I had tried to learn to use flexbox a few years ago, but it was when it was first introduced and most of the properties were browser-prefixed and didn’t always work consistently. So I just gave up and put down front-end development down for a while I waited for things to mature.
I avoided the pain by subscribing to the camps that used things like Bootstrap, Foundation and Skeleton. These were safe harbors where I could build applications quickly and focus on providing solid backends.
My interest was sparked again when I walked over to my coworker and asked him what he was working on. He showed me two tutorials of CSS grid and flex and gave me a quick proof-of-concept of what they both could do together and I was blown away.
I decided to build a page without a CSS framework only using grid and flexbox. I have to say that I am very pleased with how straightforward and simple it was to make a functional layout without worrying too much about the structure of my HTML.
Here’s the HTML
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="">
<meta name="author" content="">
<title>Test Grid Layouts</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<div class="container">
<header>
<nav>
<div id="logo">Using Grid</div>
<ul>
<li><a href="#">About</a></li>
<li><a href="#">Blog</a></li>
<li><a href="#">Contact</a></li>
</ul>
</nav>
</header>
<main>
<div class="content">
<section class="hero">
<h3>Color Me Black</h3>
</section>
<article>
<h1>Title</h1>
<p>I love content!</p>
</article>
</div>
</main>
<footer>
<nav>
<div class="siteinfo">Made by Angelo</div>
</nav>
</footer>
</div>
</body>
</html>
And the CSS
:
html, body {
margin: 0;
padding: 0;
height: 100%;
font-family: 'Roboto', 'Helvetica', sans-serif;
}
.container {
display: grid;
min-height: 100%;
grid-template-rows: 48px [stage] auto 48px;
grid-template-columns: 100%;
}
#logo {
color: #FFFFFF;
font-size: 1.5em;
}
header {
display: grid;
grid-template-columns: auto [head-center] 832px auto;
height: 48px;
background-color: #000000;
}
header > nav {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-start;
align-items: center;
grid-column-start: head-center;
grid-column-end: span 1;
}
header > nav ul {
margin: 0 0 0 auto;
}
header > nav ul li {
display: inline;
padding-left: 8px;
}
header > nav ul li a {
color: #FFFFFF;
text-decoration: none;
text-transform: uppercase;
}
header > nav ul li a:hover {
color: #AAAAAA;
}
main {
display: grid;
grid-template-columns: auto [main] 832px auto;
grid-template-rows: auto;
grid-row-start: stage;
grid-row-end: span 1;
}
main > div.content {
grid-column-start: main;
grid-column-end: span 1;
}
main > div.content .hero {
height: 300px;
width: 100%;
background-color: #000000;
color: #FFFFFF;
}
main > div.content .hero h3 {
padding: 16px;
}
footer {
display: grid;
grid-template-columns: auto [foot-center] 832px auto;
grid-template-rows: 48px;
background-color: #000000;
}
footer > nav {
display: flex;
flex-direction: row;
flex-wrap: nowrap;
justify-content: flex-end;
align-items: center;
grid-column-start: foot-center;
grid-column-end: span 1;
}
footer > nav > .siteinfo {
color: #FFFFFF;
}
I have some other nifty things in here to make things look pleasant,
but we should really focus on where display: grid
and display: flex
is used.
The .container
element is our top-level grid:
.container {
display: grid;
min-height: 100%;
grid-template-rows: 48px [stage] auto 48px;
grid-template-columns: 100%;
}
Here, we use grid-template-rows
to define our top-level rows (from
top to bottom). These can be a fixed size, a percentage or
auto
. Each specification under the property is mapped to a child
item in the order of definition. For example, the first 48px
entry
is my first row, auto
(which is aliased to stage
) is my second
row, and 48px
is my final and last row. I’ve also specified that I
want the grid-template-columns
to expand the full width of my
container.
Now that I’ve done this, I have to think about the direct child elements that I create from that grid element because all the properties I applied are to the direct children of this parent element.
My child elements (my grid items) from this parent of .container
are
header
, main
and footer
. I define properties on these items to
tell them where they should go on the grid.
Let’s take a look at main
:
main {
display: grid;
grid-template-columns: auto [main] 832px auto;
grid-template-rows: auto;
grid-row-start: stage;
grid-row-end: span 1;
}
I’m doing several things here. I’m defining another grid under the
top-level .container
grid and I’m telling this grid item to start
where the stage
row begins and then extend (or span
) 1
cell
space down from it. If I had not aliased the starting row to stage
,
then I would have to count the number of rows down I would want to
start from and enter that number (in this case, it would have been
2
).
This is basically all there is to it. While this is not like the
HTML <table>
tag, it’s mental model of placement is very
similar. You can think of colspan
and rowspan
working very much
like the span
property above.
To me, the two most important concepts to get are the parent element
where the grid
layout is defined and the child elements where each
“item” is defined. An item is a DOM element that is mapped as
content to a certain location in the grid.
You can nest grid
to get more complex layouts. I did this so I
could extend the backgrounds of my header and footer across the width
of my page, but also maintain an “internal” grid on the main
element
where content would be centered with margins that have an automatic
width.
While I used grid
to manage the layout, I used flex
to manage
the positioning behavior of child elements within the layout. This
is not just vertical and horizontal positioning, but relative spacing
between children and even ordering. Flex is great at this sort of
thing and can (and has) been used for layouts, but in the end, it
starts to really feel hacky as your DOM gets more complex. So use
grid
if you are trying to design big picture components.
For grid
, there are also more things to think about like lines,
tracks, areas and cells. I’ve also made heavy use of aliases. These
are all things you should play with and understand.
So it’s best to get experimenting. Go ahead and copy the HTML
directly to a file and save it as index.html
and then save the CSS
as styles.css
and plop them in a folder.
Assuming you have Python 3 installed and you have a unix shell available you can serve these up:
$ cd YOUR_FOLDER
$ python3 -m http.server
Serving HTTP on 0.0.0.0 port 8000 (http://0.0.0.0:8000/) ...
Then open up a browser to http://localhost:8000 and start adjusting CSS values using the built-in developer console in your browser or edit the files directly on disk and hit refresh on the browser for every change you want to make.
Also, try it with some of the mobile views. You’ll quickly see the width is fixed. Can you make it work in a fluid or responsive way?
Here’s a hint: Look at the HTML and CSS of this blog site.
There are lots of things you can do with grid. You can throw in media queries and you can make your layouts completely fluid if you wish. Most modern browsers support grid and flex.
Take a look at the grid guide and the flexbox guide or at the spec itself to get more information and most importantly – throw away your hacks!
There are some very simple rules to follow that make code easy to read, reason about and change when writing software.
I like how the Phoenix Framework encourages the use of Contexts to describe how to organize code into specific modules and functions. I even like their naming conventions.
I think these ideas can be used in any project that models logic that we encounter in the world. In fact, I used many of the ideas in my Ticket to Ride implementation.
This is always a challenge for me when I don’t know the full breadth of what I am building. It’s like writing a draft for a story or blog post. I experiment with words and code organization and only after some time do things start to fall into the right places.
In general, I try not to stress too much about it because I stick to a few tenets:
verb-noun
style function names manager
, strategy
, factory
, etc.) unless they are explicitly
described in your domain. typespecs
for every public function (even if it is a
resource under your domain) struct
. In Ticket to Ride, I use the following verb prefixes:
This is a lot of verbs, but they identify a specific action. That
specificity helps me understand what a function is going to do. If I
only had the noun as the function name, does it indicate to me what is
going to happen or what side effects will take place? If I use a vague
verb like perform
, should I consider renaming it to something else?
I stay away from corporate sounding module names like strategy
,
manager
, etc. because I see them as misleading patterns that
distract from a module’s purpose. The module name should indicate the
domain or resource and nothing else. Let the function names define
what its abilities and purposes are.
I think Paul Graham was onto something when he said this in Revenge of the Nerds:
When I see patterns in my programs, I consider it a sign of trouble. The shape of a program should reflect only the problem it needs to solve. Any other regularity in the code is a sign, to me at least, that I’m using abstractions that aren’t powerful enough– often that I’m generating by hand the expansions of some macro that I need to write.
I see this language feature is as powerful as writing tests. Not because they correct me when I do something wrong, but they show me that I have gone onto a path of complexity that feels like it may cause me trouble later. For example:
@spec claim_route(State.t, User.id, Route.t, TrainCard.t, cost()) ::
{:ok, State.t} | {:error, :unavailable}
This is from TtrCore.Mechanics.claim_route/5.
When I write a typespec
that looks like this (more than 4 arguments
with varying types), I start to feel uncomfortable and I try to find
ways to simplify it.
What I do like about this is that it shows me exactly how varied my types are and I can reason about either removing them or combining them before writing any code. It’s harder to do this without type information.
In addition, I can also reason about my possible outputs. Currently, these look reasonable.
After combining a bunch of types into some compound type (like a map
or tuple
), I will pass that around in my internal APIs to get work
done.
If the amount of work being done with the that data has two or more domain-based operations, then I feel like my data should be formalized into some named type.
Sometimes this data has to leave a domain and has to go into another one and I think that’s OK as long as the operations in the foreign domain aren’t trying to act directly on the data in a way that cause its meaning to change.
See Returning Ecto structures from context APIs as one explanation for this type of reasoning.
If it is needed to read that data in order to derive new data in a foreign domain, then that seems OK because that new data is relevant to that foreign domain and can be formalized as a type there.
The place this becomes dangerous is when a type needs to get associated with “extra” data and the “extra” data is merged directly onto the compound data type across multiple domains and still gets called by the same named type.
This is wrong. It begets all kinds of misunderstanding of what is happening in the code. If there is a need to do this type of association, a new type needs to be created to encapsulate both of these pieces of information or the original type needs to be modified.
I tend for the first option because it prevents a single type from become a “god” type that knows about too many things.
I like to know what my dependencies are in a module at the very beginning. Aliasing offers this as an excellent side-effect to shortening names used in module.
When I see lots of aliases in a particular module, can I assume that my module knows about too many things? Is it a sign that a new domain or resource should be created to handle a portion of these concerns?
None of these tenets are silver bullets. They won’t save me from poor performing code or convoluted logic that exists in the real world, but they do shield me from my own disorganization and give me enough principles to manage code bases over many years.
A while back I decided that using macros to define the routes and tickets for my implementation of ticket to ride in Elixir would be a good idea because of the data structure that I had envisioned at the time.
The macro usage looks like this:
defmodule TtrCore.Board.Routes do
use TtrCore.Board.Router
defroute Atlanta, to: Charleston, distance: 2
defroute Atlanta, to: Miami, distance: 5, trains: [:passenger]
defroute Atlanta, to: Raleigh, distance: 2, trains: [:any, :any]
defroute Atlanta, to: Nashville, distance: 1
defroute Atlanta, to: New.Orleans, distance: 4, trains: [:box, :tanker]
defroute Boston, to: Montreal, distance: 2, trains: [:any, :any]
defroute Boston, to: New.York, distance: 1, trains: [:coal, :box]
# And more routes...
end
I realized that my implementation was far too complex for the calculations needed of the game, so I scrapped the data structure for a much flatter and simpler one.
Here’s the original:
defmodule TicketToRide.Router do
alias TicketToRide.{NoOriginFoundError,
NoDestinationSpecifiedError,
NoDistanceSpecifiedError}
defmodule Origin do
defstruct [
name: nil,
destinations: %{}
]
end
defmodule Destination do
defstruct [
name: nil,
distance: 0,
trains: MapSet.new
]
end
defmacro __using__(_opts) do
quote do
import unquote(__MODULE__)
Module.register_attribute __MODULE__, :origins, accumulate: false, persist: false
Module.put_attribute __MODULE__, :origins, %{}
@before_compile unquote(__MODULE__)
end
end
# API
defmacro defroute(name, args \\ []) do
quote do
@origins Map.merge(@origins, update_origins(@origins, unquote(name), unquote(args)))
end
end
defmacro __before_compile__(_env) do
quote do
def all, do: @origins
def get(name), do: Map.fetch(@origins, name)
end
end
def update_origins(origins, name, args) do
source = get(origins, name, autocreate: true)
destination = get(origins, args[:to], autocreate: true)
destination_options = [
to: name,
distance: args[:distance],
trains: args[:trains]
]
%{ name => update(source, args),
args[:to] => update(destination, destination_options) }
end
# Private
defp get(origins, name, opts \\ [autocreate: false]) do
case Map.fetch(origins, name) do
{:ok, origin} -> origin
:error -> get_on_error(name, opts[:autocreate])
end
end
defp get_on_error(name, autocreate) do
if autocreate do
%Origin{name: name}
else
raise NoOriginFoundError, name: name
end
end
defp update(origin, args) do
destination_name = extract_destination_name(origin, args)
trains = extract_trains(args)
destination = update_destination(origin, destination_name, trains, args)
destinations = Map.put(origin.destinations, destination_name, destination)
%{origin | destinations: destinations}
end
defp update_destination(origin, destination, trains, args) do
case Map.fetch(origin.destinations, destination) do
{:ok, dest} ->
%{dest | trains: MapSet.union(dest.trains, trains)}
:error ->
distance = extract_distance(origin, args)
%Destination{name: destination, distance: distance, trains: trains}
end
end
defp extract_destination_name(origin, args) do
case args[:to] do
nil -> raise NoDestinationSpecifiedError, from: origin.name
destination -> destination
end
end
defp extract_distance(origin, args) do
case args[:distance] do
nil -> raise NoDistanceSpecifiedError, from: origin.name, to: args[:to]
distance -> distance
end
end
defp extract_trains(args) do
case args[:trains] do
nil -> MapSet.new([:any])
trains -> MapSet.new(trains)
end
end
end
And the new:
defmodule TtrCore.Board.Router do
@moduledoc false
alias TtrCore.Board.Route
defmacro __using__(_opts) do
quote do
import unquote(__MODULE__)
Module.register_attribute __MODULE__, :routes, accumulate: true, persist: true
@before_compile unquote(__MODULE__)
end
end
defmacro defroute(from, args \\ []) do
to = args[:to]
distance = args[:distance]
trains = args[:trains] || [:any]
quote do
Enum.each(unquote(trains), fn train ->
@routes {unquote(from), unquote(to), unquote(distance), train}
end)
end
end
defmacro __before_compile__(_env) do
quote do
@spec get_routes() :: [Route.t]
def get_routes, do: @routes
@spec get_claimable_routes([Route.t]) :: [Route.t]
def get_claimable_routes(claimed), do: @routes -- claimed
end
end
end
The main changes were around the data structure to build up the module
attribute @routes
. I was using a nested map within a list and now I am
justing using a list of tuples.
The reason I was trying to use a nested map was because I wanted to map every city to a list of possible destinations. This sounded like a good idea for figuring out which cities on the map are connected to another to calculate the final score, but turned out to be irrelevant for most of the game.
There were also features for custom error checking at compile time. If I did not follow the format of the macro, the macro would throw me a relevant error message about what I did wrong. But since the data is simple, the compiler’s AST checking was more than adequate to indicate where I went awry.
I only needed to calculate the longest route and ticket points when I
calculated the final score. In both the old and new implementation, a
map/reduce
would be needed to calculate which player got the longest
route. I reasoned that less data complexity would lead to less
algorithmic complexity.
And that’s why I changed it.