|||

Rake Routes

by Stephen Ball

A subtle Go bug that types cannot help with

I ran into this bug while going through the highly excellent Powerful Command-Line Applications in Go” by Ricardo Gerardi

The following startInterval function contains a nested periodic function that will be called during a pomodoro timer. Great!

The problem is that while the app is running nothing updates the application interface. It’s as though the timer isn’t running, but if I pause and start the timer the application UI updates once and then remains frozen. What’s up?

Debugging (why yes I am a PRINTS debugger) shows that the periodic function is indeed being called as designed by a ticker channel getting a value every second. What’s the problem? Can you see it?

startInterval := func() {
  i, err := pomodoro.GetInterval(config)
  errorCh <- err

  start := func(i pomodoro.Interval) {
    message := "Take a break"
    if i.Category == pomodoro.CategoryPomodoro {
      message = "Focus on your task"
    }

    w.update([]int{}, i.Category, message, "", redrawCh)
  }

  end := func(pomodoro.Interval) {
    w.update([]int{}, "", "Nothing running...", "", redrawCh)
  }

  periodic := func(pomodoro.Interval) {
    w.update(
      []int{int(i.ActualDuration), int(i.PlannedDuration)},
      "", "",
      fmt.Sprint(i.PlannedDuration-i.ActualDuration),
      redrawCh,
    )
  }

  errorCh <- i.Start(ctx, config, start, periodic, end)
}

The problem is that the periodic function accepts a pomodoro.Interval but does not assign it to a variable. The type matches (hooray small type victories) but nothing ensures or requires that the type be assigned.

Within the periodic function the i value then silently refers to the i created in the outer function scope.

i, err := pomodoro.GetInterval(config) // <-- this i

That means the function works, and is indeed called on the appropriate ticker intervals. But the application UI never actually sees changes because the values within i all remain how they were when the interval started.

The fix? A ONE character change to assign that pomodoro.Interval to i

periodic := func(i pomodoro.Interval) {
  w.update(
    []int{int(i.ActualDuration), int(i.PlannedDuration)},
    "", "",
    fmt.Sprint(i.PlannedDuration-i.ActualDuration),
    redrawCh,
  )
}

With that change the book’s code example works as intended.

A better approach: don’t shadow the variable name

For me a better approach would be to not shadow the outer i value at all and instead give the periodic function its own variable name for the interval it expects.

periodic := func(pomodoro.Interval) {
  w.update(
    []int{int(activeInterval.ActualDuration), int(activeInterval.PlannedDuration)},
    "", "",
    fmt.Sprint(activeInterval.PlannedDuration-activeInterval.ActualDuration),
    redrawCh,
  )
}

With that change then the Go compiler immediately yells about the problem: nothing has set activeInterval! Easy fix.

periodic := func(activeInterval pomodoro.Interval) {
  w.update(
    []int{int(activeInterval.ActualDuration), int(activeInterval.PlannedDuration)},
    "", "",
    fmt.Sprint(activeInterval.PlannedDuration-activeInterval.ActualDuration),
    redrawCh,
  )
}

Alternatives

Go could warn about unassigned function arguments. It could even require that function arguments be assigned. In that case if you wanted to only match the signature and really for sure don’t care to actually use the function argument itself then you could assign it to _ which is explicitly an ignore this variable” declaration in Go.

Even nicer would be to take that to Elixir levels which allows giving ignored variables a name to indicate what’s being ignored e.g. _interval

Up next swapcase with the tr command Want to easily swap the case of some string on the *nix command line? The tr command can do that! You may already know the tr command can switch Where did the recent Elixir posts go? Hey, you might say, where are all of your recent Elixir posts? Well I’ll tell ya. I started this blog back in 2012 when I was a fulltime Rails dev.
Latest posts Where did the recent Elixir posts go? A subtle Go bug that types cannot help with swapcase with the tr command nice go test output See where vim settings came from Containers in the real world and backpressure in distributed systems Elixir Phoenix and “role postgres does not exist” From awk to a Dockerized Ruby Script Finding leap years with the cal command The Problem of State Clojure Functions in Four Ways See Some Clojure A simple language spec isn’t a feature when you’re building applications The Fastest Possible Tests Shrink your data into bitfields (and out again) Every “if” statement is an object waiting to be extracted Choose Generic Tools Hyperlinks you might find interesting — #4 Running bundle install on rails master Use tldr for command line examples Friday Lunch Links — #3 Friday Lunch Links — #2 Logical Solver: Turn facts into conclusions Programming with jq Command line tools - jq Friday Lunch Links — #1 Why diversity matters Music for coding - October 2019 Code puzzles are a poor way to gauge technical candidates Add vim to a pipeline with vipe Connecting Objects with Observable