From e6d4e0cf0ffc35fda7810948c289ca56d452c116 Mon Sep 17 00:00:00 2001 From: Eryn Wells Date: Sun, 6 Nov 2022 00:23:24 -0700 Subject: [PATCH] Hugo's Dictionary API post Squashed commit of the following: commit b507cf8be2ca46ce4b88e3292ef173a0b5e3606f Author: Eryn Wells Date: Sun Nov 6 00:23:05 2022 -0700 Wrap up this blog post commit 6ed0c777e7c33b0819f7d7a896fce60f75a13fe3 Author: Eryn Wells Date: Sat Oct 15 10:20:03 2022 -0700 WIP Hugo Dictionary API post --- .../blog/2022/10/hugo-dictionary-api/index.md | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 content/blog/2022/10/hugo-dictionary-api/index.md diff --git a/content/blog/2022/10/hugo-dictionary-api/index.md b/content/blog/2022/10/hugo-dictionary-api/index.md new file mode 100644 index 0000000..b615b18 --- /dev/null +++ b/content/blog/2022/10/hugo-dictionary-api/index.md @@ -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