Skip to main content

Michael Uloth

The full content of my 5 most recent posts

  1. Converting a list of JS objects into a parent-child tree

    My previous approach to displaying my notes on this site was to render them as a tree of infinitely nested topics and subtopics:

    A screenshot of a web page showing a list of topic links where subtopics are nested under parent topics.

    Parents and children

    To achieve that nesting, my solution was to manually give each note that represented a subtopic (or subsubsubtopic) a parent value (via markdown frontmatter) and then move any child notes (i.e. notes with a parent value) into their parent’s children array:

    /**
     * Given an array of collection items, returns the array with child items nested under their parents.
     */
    function nestChildren(collection: Note[]): Note[] {
      // Step 1: Create a mapping of item IDs to item data 
      const slugToNodeMap = collection.reduce(
        (nodesBySlug, item): Record<string, Note> => {
          // Add an empty children array to the item's existing data
          nodesBySlug[item.id.toLowerCase()] = { ...item, data: { ...item.data, children: [] } }
          return nodesBySlug
        },
        {} as Record<string, Note>,
      )
     
      // Step 2: Build the nested item tree
      const tree = collection.reduce((roots, item): Note[] => {
        // Find the node matching the current collection item
        const node = slugToNodeMap[item.id.toLowerCase()]
     
        if (item.data.parent) {
          // If the note has a parent...
          const parentNode = slugToNodeMap[item.data.parent.toLowerCase()]
     
          if (parentNode) {
            // ...add the item's data to the parent's children array
            parentNode.data.children.push(node)
          } else {
            console.error(`Parent slug "${item.data.parent}" not found (this should never happen).`)
          }
        } else {
          // If the item has no parent, treat it as a new root-level note
          roots.push(node)
        }
     
        // Return the updated tree and keep iterating
        return roots
      }, [] as Note[])
     
      // Return the final, nested tree
      return tree
    }
     
    /**
     * Returns all notes with child notes nested under their parents.
     */
    export const getNestedNotes = async (): Promise<Note[]> =>
      nestChildren(await getCollection('writing', note => isNote(note)))

    Incremental left padding

    My solution for visually indicating how those notes related to each other was to multiply each note’s left-padding by 1.4x its descendent level:

    <ul class="list-notes">
      {
        notes.map(note => {
          const getLinkText = (item: Note): string => item.data.title || item.id
     
          const getChildren = (item: Note, level: number) =>
            item.data.children && (
              <ul>
                {item.data.children
                  .sort((a: Note, b: Note) => a.data.title.localeCompare(b.data.title))
                  .map((child: Note) => (
                    <li
                      class="before:content-['└'] before:pe-[0.3rem] mt-[0.1rem]"
                      style={`padding-left: ${level === 0 ? 0 : 1.4}rem`}
                    >
                      <a href={`/${child.slug}/`} class="link">
                        {getLinkText(child)}
                      </a>
     
                      {/* Recursively render grandchildren, etc */}
                      {getChildren(child, level + 1)}
                    </li>
                  ))}
              </ul>
            )
     
          return (
            <li class="mb-2 break-inside-avoid-column">
              <a href={`/${note.slug}/`} class="link">
                {getLinkText(note)}
              </a>
     
              {getChildren(note, 0)}
            </li>
          )
        })
      }
    </ul>

    Maintaining note hierarchies is a chore

    That was a fun programming puzzle to solve, but unfortunately it made my note-taking more difficult by forcing me to decide where each new thought belonged in the hierarchy before I could jot it down.

    To remove that friction, I’ve adopted to a flat (and eventually searchable and filterable) approach to note-taking instead. But I wanted to share this former nested-topics solution in case anyone else finds it useful.

  2. Tmux's version command is -V

    If you want to see which version of tmux is installed, you’re looking for

    tmux -V

    Not tmux -v.

    Not tmux --version.

    Not tmux version.

    Why you gotta be so special, tmux?

  3. SELECT DISTINCT outputs a column's unique values

    Here’s how to see every value in a column with SQL:

    SELECT DISTINCT column
    FROM table_name

    I found that helpful today when I wanted to understand the significance of a value I was looking at by comparing it to the alternatives.

  4. It's tricky to statically type a "pipe" function in Python

    I wanted a type-safe pipe utility to help me write Python in a more functional style, but unfortunately toolz.pipe and returns.pipelines.flow both output Any.

    Happily, it turns out creating your own pipe mechanism is a one-liner:

    from functools import reduce
     
    result = reduce(lambda acc, f: f(acc), (fn1, fn2, fn3), value)

    Which you can reuse by wrapping it in a function:

    from functools import reduce
    from typing import Callable, TypeVar
     
    _A = TypeVar("A")
    _B = TypeVar("B")
     
    def pipe(value: _A, *functions: Callable[[_A], _A]) -> _A:
        """Pass a value through a series of functions that expect one argument of the same type."""
        return reduce(lambda acc, f: f(acc), functions, value)

    And calling it with any number of functions:

    assert pipe("1", int, float, str) == "1.0"
    # => i.e. str(float(int('1')))
    # => i.e. int("1") -> float(1) -> str(1.0) -> "1.0"

    So you can stop thinking up names for throwaway variables like these:

    def str_to_float_str(value: string):
        as_integer = int(value)
        as_float = float(as_integer)
        as_string = str(as_float)
        return as_string
     
    assert str_to_float_str("1") == "1.0"

    Credit to Statically-Typed Functional Lists in Python with Mypy by James Earl Douglas and the returns.pipeline.flow source code for the inspiration.

    Update: Nov 29, 2024

    While the pipe function above with the Callable[[A], A] type hint works fine if every function in the pipeline outputs the same type (A), the example I showed above doesn’t actually work out very well! Mypy notices that some of the functions (int and float) output a different type than we started with (str), so we aren’t actually passing A all the way through.

    After trying a number of workarounds (and getting some good advice on Reddit), I learned that you can either tell Mypy what’s going on by painstakingly articulating every possible overload:

    _A = TypeVar("A")
    _B = TypeVar("B")
    _C = TypeVar("C")
    _D = TypeVar("D")
    _E = TypeVar("E")
     
    @overload
    def pipe(value: _A) -> _A: ...
     
    @overload
    def pipe(value: _A, f1: Callable[[_A], _B]) -> _B: ...
     
    @overload
    def pipe(
        value: _A, 
        f1: Callable[[_A], _B], 
        f2: Callable[[_B], _C]
    ) -> _C: ...
     
    @overload
    def pipe(
        value: _A, 
        f1: Callable[[_A], _B], 
        f2: Callable[[_B], _C], 
        f3: Callable[[_C], _D]
    ) -> _D: ...
     
    @overload
    def pipe(
        value: _A,
        f1: Callable[[_A], _B],
        f2: Callable[[_B], _C],
        f3: Callable[[_C], _D],
        f4: Callable[[_D], _E],
    ) -> _E: ...
     
     
    def pipe(value: Any, *functions: Callable[[Any], Any]) -> Any:
        return reduce(lambda acc, f: f(acc), functions, value)

    Or, you can just use expression, which already does this for you.

    I’m going to do the latter, but this was a fun exercise in the meantime. 😎

Recent Posts ✍️

  1. An iOS Shortcut can add data to a Google Sheet
  2. How to query a BigQuery table from Python
  3. CloudFlare sells domain names at cost
  4. Homebrew packages might install dependencies
  5. Shell functions don't need parentheses
  6. You can run shell scripts in tmux.conf
  7. Why Unknown Types Are Useful
  8. Using "object" as an Unknown Type in Python
  9. Defining a Custom Unknown Type in Python
  10. Undoing a Merge to Your Main Git Branch
  11. Switching Configs in Neovim
  12. Adding a Pull Request Template to Your GitHub Repo
  13. Dramatically Reducing Video File Size Using FFmpeg
  14. The translateZ Trick
  15. What is a Factory Function?
  16. Writing Great Alt Text
  17. Levels of Abstraction in Testing
  18. Using Slack to Report Data Entry Errors to Content Editors
  19. The filter(Boolean) Trick
  20. Using GraphQL with Gatsby
  21. Adding Content to a Gatsby Project
  22. Writing CSS-in-JS in a Gatsby Project
  23. Writing CSS in a Gatsby Project
  24. Wrapping Pages in a Layout Component
  25. Adding Pages to a Gatsby Project
  26. Writing HTML in a Gatsby Project
  27. Gatsby's Default Files and Folders
  28. Starting a New Gatsby Project
  29. What is Gatsby?
  30. Introducing Gatsby Tutorials
  31. How to Set Up a Mac for Web Development