Hugo's Dictionary API post

Squashed commit of the following:

commit b507cf8be2ca46ce4b88e3292ef173a0b5e3606f
Author: Eryn Wells <eryn@erynwells.me>
Date:   Sun Nov 6 00:23:05 2022 -0700

    Wrap up this blog post

commit 6ed0c777e7c33b0819f7d7a896fce60f75a13fe3
Author: Eryn Wells <eryn@erynwells.me>
Date:   Sat Oct 15 10:20:03 2022 -0700

    WIP Hugo Dictionary API post
This commit is contained in:
Eryn Wells 2022-11-06 00:23:24 -07:00
parent c23711db55
commit e6d4e0cf0f

View file

@ -0,0 +1,151 @@
---
title: "Hugo's Dictionary API"
date: 2022-10-13T10:19:02-07:00
categories: ["Tech"]
tags: ["Hugo", "Web", "API Design"]
series: "Erynwells.me Development"
---
Hugo's templating system has support for dictionaries. Unfortunately the API for
working with them is, frankly, awful. While working on developing some new
templates for this site, I had to figure out how to build up dictionary data
structures and it took me a _long_ time to figure out how to do some basic
operations with them.
Here's a quick summary of what I found.
## Creating Dictionaries
The function to create a dictionary is called [`dict`][dict] and it takes a
variable number of arguments that alternate between keys and values. It reminds
me of this [bizarre and backwards NSDictionary API][nsdictionary-init] in Apple's
Foundation framework. Keys must be strings (or string slices) and values can be
anything. So this:
{{< figures/code >}}
```go-html-template
{{ $d := dict "a" 1 "b" 2 "c" 3 }}
```
{{< /figures/code >}}
creates a structure that looks like this JSON object:
{{< figures/code >}}
```json
{ "a": 1, "b": 2, "c": 3 }
```
{{< /figures/code >}}
You can also create an empty dictionary by calling `dict` with no arguments.
{{< figures/code >}}
```go-html-template
{{ $d := dict }}
```
{{< /figures/code >}}
## Accessing Keys and Values
Statically, you can get a single item in a dictionary with dot syntax. Below,
`$item` will get the value 1.
{{< figures/code >}}
```go-html-template
{{ $item := (dict "a" 1 "b" 2 "c" 3).a }}
```
{{< /figures/code >}}
If you want to get a value with a key you get at render time, you can use the
[`index`][index] function. In the snippet below, `$item` will get the value of
`"b"`, which is 2.
{{< figures/code >}}
```go-html-template
{{ $key := "b" }}
{{ $item := index $key (dict "a" 1 "b" 2 "c" 3) }}
```
{{< /figures/code >}}
`index` doesn't make much sense to me as a verb for accessing values in a
dictionary. It sounds more like an array function, and indeed it's the function
that gives you access to items in arrays. I would like to see another function
with a more dictionary-sounding name, like `get` or `value` or `item`, even if
it were just an alias for `index` underneath.
## Adding Items to a Dictionary
This is a bit complex because, as far as I can tell, dictionaries are immutable.
So, if you want to update a dictionary, you need to combine two dictionaries and
then save it back to the original variable. The [`merge`][merge] function does
that. Here's a snippet:
{{< figures/code >}}
```go-html-template
{{ $d := dict "a" 1 "b" 2 "c" 3 }}
{{ $d = merge $d (dict "b" 4) }}
{{ $item = index "b" $d }}
```
{{< /figures/code >}}
`merge` takes a variable number of arguments, and merges dictionaries left to
right. So, items in dictionaries later in the argument list will override items
in dictionaries earlier in the list.
Just to underscore, you have to set the update dictionary back to the original
variable to complete the update, hence the `$d = ...`.
All that is to say: at the end of that snippet, `$item` will get the value 4.
## A Complex Example: A Dictionary of Arrays
For the previously mentioned template changes I was making, I was updating the
`terms` template for my category taxonomy. For each category, I wanted to show
one section per tag, and a list of all the posts with that tag underneath.
My categories are high level groups like "Tech," "Music," and "Travel." Tags are
more specific topics for the post like "Web" or "Compositions." Pages only ever
have one category but they can have multiple tags.
A `terms` template lets you access an array of terms, and the pages associated
with those terms. You can access the tags attached to a page with the
`.GetTerms` function. Here's what I did, and then I'll talk through it:
{{< figures/code >}}
```go-html-template
{{- $pagesByTag := dict -}}
{{- range $page := .Pages -}}
{{- range $tag := .GetTerms "tags" -}}
{{- $tagName := $tag.Name -}}
{{- if not (in $pagesByTag $tagName) -}}
{{- $pagesByTag = merge $pagesByTag
(dict $tagName (slice $page)) -}}
{{- else -}}
{{- $pagesForTag := index $pagesByTag $tagName -}}
{{- $pagesForTag = $pagesForTag | append $page -}}
{{- $pagesByTag = merge $pagesByTag
(dict $tagName $pagesForTag) -}}
{{- end -}}
{{- end -}}
{{- end -}}
```
{{< /figures/code >}}
`$pagesByTag` is my empty dictionary. It will hold tag names as keys, each
pointing to a slice (array) of page objects. For each page, I get its list of
tags. For each tag, I check `$pagesByTag` to see if it already has a key/value
pair for that tag. If not, I create a new entry in `$pagesByTag` with `merge`.
If it does already, I get the slice for that tag with `index`, add the Page to
the slice with `append`, and then merge the updated slice back into
`$pagesByTag` with `merge`.
It's not too bad once it's all spelled out, but it does feel like more work than
it should take for such simple operations.
I think this API could be improved substantially with some new functions that
operate specifically on dictionaries and that have clear names that describe
what they do.
[dict]: https://gohugo.io/functions/dict/
[index]: https://gohugo.io/functions/index-function/
[merge]: https://gohugo.io/functions/merge/
[nsdictionary-init]: https://developer.apple.com/documentation/foundation/nsdictionary/1574181-dictionarywithobjectsandkeys?language=objc