Iterative Square Root

I saw a toot celebrating a short, clean implementation of a square root finding algorithm and wanted to dig a bit deeper into how it works, with a diversion into some APL.

This was the toot from Jim Gardner

Doubly pleased with myself.

Been doing the Tour of Go. Got to the section where you make a square root function, which should return once the calculated value stops changing. Struggled for ages. Trimmed and trimmed. Until finally… this!

The calculation for z was given, and I don’t understand it at all. But I don’t care. It was a total mess when I started and has turned out quite neat. I’m very satisfied.

But why “doubly pleased”? Because I’ve been solely using Neovim so far for Go!!

package main 

import ( 
	"fmt" 
) 

func Sqrt(x float64) float64 {
	z := 1.0 
	for { 
		y := z 
		z -= (z*z - x) / (2 * z) 
		
		if z == y { 
			return z 
		} 
	}
} 

func main() { 
	fmt.Println(Sqrt(16)) 
} 

It’s a nice, not-too-complicated algorithm to play with, and I agree it’s hard to see why it works for this application, so I thought it would be neat to walk through that.

What we’re trying to solve here is the function \(y = x^2\) which we could write as \(f(x) = x^2 - y\) for which we want the value \(x\) where \(f(x) = 0\).

Newton’s Method is an iterative method for solving equations of this type (not all equations, mind you - I have an entire chapter of my PhD thesis discussing exactly why it can’t be used to solve the equations I was solving that my supervisor insisted it could). It works by using the slope (derivative) at a point to guide towards a solution. The formula for the updated value \(x_{n+1}\) given some guess \(x_n\) is

\[ x_{n+1} = x_n - \frac{f(x_n)}{f'(x_n)} \] where \(f'(x)\) is the derivative of the function \(f\) at the point \(x\). For \(f(x) = x^2 - y\) the derivative is \(f'(x) = 2x\) so we can substitute this and \(f(x)\) into the above formula

\[ x_{n+1}​=x_n​−\frac{x_n^2-y}{2x_n} \] This is what the Go code calculates; given an initial guess of \(x_n = 1\) it calculates the next value as

x = x - (x*x - y) / (2 * x) 

where, here, y is the value we’re finding the square root of.

In R this could be written as

SQRT <- function(x) {
  z <- 1
  while (TRUE) {
    y <- z
    z <- z - (z*z - x)/(2*z)
    if (abs(y -z) < 0.0001) return(z)
  }
}

(since base::sqrt is already defined) where I’ve used a tolerance rather than relying on exact numerical equality. The while(TRUE) construct is equivalent to Go’s for {} syntax; an infinite loop.

R actually has another way to write that which is even closer; repeat {}

SQRT <- function(x) {
  z <- 1
  repeat {
    y <- z
    z <- z - (z*z - x)/(2*z)
    if (abs(y -z) < 0.0001) return(z)
  }
}

One might notice that this approach requires essentially squaring a value, which is hardly expensive, but we can simplify and cancel out \(x_n\), so

\[ x_{n+1}​=\frac{x_n-\frac{y}{x_n}}{2} \] in which case we have

SQRT <- function(x) {
  z <- 1
  repeat {
    y <- z
    z <- (z + x/z)/2
    if (abs(y -z) < 0.0001) return(z)
  }
}

One of the reasons I wanted to dig into this was the fact that it’s a convergence…

In APL the power operator ( aplwi) applies a function some specified number of times, so

    f ⍣n x

applies f to x n times, i.e. (f⍣3)x produces f(f(f(x))).

It can also be used as ⍣= where it will continue to apply the function until the output no longer changes (is equal). A classic example is the golden ratio; take the reciprocal then add 1 until it converges, i.e. 

\[ x_{n+1} = 1+\frac{1}{x_n} \]

which you can try for yourself here

    1+∘÷⍣=1
1.618033989

In this, +∘÷ is the (tacit) function created by composing () ‘addition of 1’ (1+, a partial application of a function) and ‘reciprocal’ (÷), which is iterated until it no longer changes (within machine precision).

Iterating until convergence is exactly what we want, since we’re looking for the value satisfying

\[ x_n = x_{n+1}​=\frac{x_n-\frac{y}{x_n}}{2} \] APL uses as the right argument placeholder and as the left, so the function we want to apply repeatedly to the right argument is

    {⍵-(((⍵×⍵)-⍺)÷(2×⍵))}

If we provide 1 as the right argument (the start value) and 16 as the left argument, we get

    16{⍵-(((⍵×⍵)-⍺)÷(2×⍵))}⍣=1
4

You can try this out yourself at tryapl.org (link should load that expression).

We can turn that into a function, once again using the argument placeholder

    sqrt←{⍵{⍵-(((⍵×⍵)-⍺)÷(2×⍵))}⍣=1}
    sqrt 25
5
    sqrt 81
9

Taking the simplification above, we can write this a bit shorter as

      sqrt←{⍵{(⍵+(⍺÷⍵))÷2}⍣=1}
      sqrt 144
12

As clean as the Go code looks, I think there’s a certain beauty to being able to write this in just 20 characters. It’s not for everyone, I get that.

I love these opportunities to learn a bit more about languages.

If you have comments, suggestions, or improvements, as always, feel free to use the comment section below, or hit me up on Mastodon.


devtools::session_info()
## ─ Session info ───────────────────────────────────────────────────────────────
##  setting  value
##  version  R version 4.3.3 (2024-02-29)
##  os       Pop!_OS 22.04 LTS
##  system   x86_64, linux-gnu
##  ui       X11
##  language (EN)
##  collate  en_AU.UTF-8
##  ctype    en_AU.UTF-8
##  tz       Australia/Adelaide
##  date     2024-05-29
##  pandoc   3.1.11 @ /usr/lib/rstudio/resources/app/bin/quarto/bin/tools/x86_64/ (via rmarkdown)
## 
## ─ Packages ───────────────────────────────────────────────────────────────────
##  package     * version date (UTC) lib source
##  blogdown      1.18    2023-06-19 [1] CRAN (R 4.3.2)
##  bookdown      0.36    2023-10-16 [1] CRAN (R 4.3.2)
##  bslib         0.6.1   2023-11-28 [3] CRAN (R 4.3.2)
##  cachem        1.0.8   2023-05-01 [3] CRAN (R 4.3.0)
##  callr         3.7.3   2022-11-02 [3] CRAN (R 4.2.2)
##  cli           3.6.2   2023-12-11 [3] CRAN (R 4.3.2)
##  crayon        1.5.2   2022-09-29 [3] CRAN (R 4.2.1)
##  devtools      2.4.5   2022-10-11 [1] CRAN (R 4.3.2)
##  digest        0.6.34  2024-01-11 [3] CRAN (R 4.3.2)
##  ellipsis      0.3.2   2021-04-29 [3] CRAN (R 4.1.1)
##  evaluate      0.23    2023-11-01 [3] CRAN (R 4.3.2)
##  fastmap       1.1.1   2023-02-24 [3] CRAN (R 4.2.2)
##  fs            1.6.3   2023-07-20 [3] CRAN (R 4.3.1)
##  glue          1.7.0   2024-01-09 [3] CRAN (R 4.3.2)
##  htmltools     0.5.7   2023-11-03 [3] CRAN (R 4.3.2)
##  htmlwidgets   1.6.2   2023-03-17 [1] CRAN (R 4.3.2)
##  httpuv        1.6.12  2023-10-23 [1] CRAN (R 4.3.2)
##  icecream      0.2.1   2023-09-27 [1] CRAN (R 4.3.2)
##  jquerylib     0.1.4   2021-04-26 [3] CRAN (R 4.1.2)
##  jsonlite      1.8.8   2023-12-04 [3] CRAN (R 4.3.2)
##  knitr         1.45    2023-10-30 [3] CRAN (R 4.3.2)
##  later         1.3.1   2023-05-02 [1] CRAN (R 4.3.2)
##  lifecycle     1.0.4   2023-11-07 [3] CRAN (R 4.3.2)
##  magrittr      2.0.3   2022-03-30 [3] CRAN (R 4.2.0)
##  memoise       2.0.1   2021-11-26 [3] CRAN (R 4.2.0)
##  mime          0.12    2021-09-28 [3] CRAN (R 4.2.0)
##  miniUI        0.1.1.1 2018-05-18 [1] CRAN (R 4.3.2)
##  pkgbuild      1.4.2   2023-06-26 [1] CRAN (R 4.3.2)
##  pkgload       1.3.3   2023-09-22 [1] CRAN (R 4.3.2)
##  prettyunits   1.2.0   2023-09-24 [3] CRAN (R 4.3.1)
##  processx      3.8.3   2023-12-10 [3] CRAN (R 4.3.2)
##  profvis       0.3.8   2023-05-02 [1] CRAN (R 4.3.2)
##  promises      1.2.1   2023-08-10 [1] CRAN (R 4.3.2)
##  ps            1.7.6   2024-01-18 [3] CRAN (R 4.3.2)
##  purrr         1.0.2   2023-08-10 [3] CRAN (R 4.3.1)
##  R6            2.5.1   2021-08-19 [3] CRAN (R 4.2.0)
##  Rcpp          1.0.11  2023-07-06 [1] CRAN (R 4.3.2)
##  remotes       2.4.2.1 2023-07-18 [1] CRAN (R 4.3.2)
##  rlang         1.1.3   2024-01-10 [3] CRAN (R 4.3.2)
##  rmarkdown     2.25    2023-09-18 [3] CRAN (R 4.3.1)
##  rstudioapi    0.15.0  2023-07-07 [3] CRAN (R 4.3.1)
##  sass          0.4.8   2023-12-06 [3] CRAN (R 4.3.2)
##  sessioninfo   1.2.2   2021-12-06 [1] CRAN (R 4.3.2)
##  shiny         1.7.5.1 2023-10-14 [1] CRAN (R 4.3.2)
##  stringi       1.8.3   2023-12-11 [3] CRAN (R 4.3.2)
##  stringr       1.5.1   2023-11-14 [3] CRAN (R 4.3.2)
##  urlchecker    1.0.1   2021-11-30 [1] CRAN (R 4.3.2)
##  usethis       2.2.2   2023-07-06 [1] CRAN (R 4.3.2)
##  vctrs         0.6.5   2023-12-01 [3] CRAN (R 4.3.2)
##  xfun          0.41    2023-11-01 [3] CRAN (R 4.3.2)
##  xtable        1.8-4   2019-04-21 [1] CRAN (R 4.3.2)
##  yaml          2.3.8   2023-12-11 [3] CRAN (R 4.3.2)
## 
##  [1] /home/jono/R/x86_64-pc-linux-gnu-library/4.3
##  [2] /usr/local/lib/R/site-library
##  [3] /usr/lib/R/site-library
##  [4] /usr/lib/R/library
## 
## ──────────────────────────────────────────────────────────────────────────────


rstats  APL  go 

See also