Minimum Mandelplot
While researching for my posts about đťś‹ in the Mandelbrot set, I came across a very concise Mandelbrot-plotting program in the Julia language.
function mandelbrot(a)
z = 0
for i=1:50
z = z^2 + a
end
return z
end
for y=1.0:-0.05:-1.0
for x=-2.0:0.0315:0.5
abs(mandelbrot(complex(x, y))) < 2 ? print("*") : print(" ")
end
println()
end
That’s 235 characters, including the four-space-per-level indents. To use a modern metric of concision, it’s tweetable!
I’ve been wondering ever since what the minimum idiomatic Swift Mandelbrot-plotting program might be. Here’s what I’ve come up with so far:
import Numerics
typealias Z = Complex<Double>
for i in stride(from: 1.0, through: -1.0, by: -0.05) {
for r in stride(from: -2.0, through: 0.5, by: 0.0315) {
sequence(first: Z(0, 0)) { z in
z*z + Z(r, i)
}
.prefix(51)
.allSatisfy { $0.length <= 2 } ?
print("*", terminator: "")
: print(" ", terminator: "")
}
print()
}
339 characters—and as you may have noticed, that’s after cheating by using tabs for indentation. Not tweetable.
Here’s the output, because who doesn’t want to see a Mandelbrot set plot?
**
******
********
******
******** ** *
*** *****************
************************ ***
****************************
******************************
******************************
************************************
* **********************************
** ***** * **********************************
*********** ************************************
************** ************************************
***************************************************
*****************************************************
***********************************************************************
*****************************************************
***************************************************
************** ************************************
*********** ************************************
** ***** * **********************************
* **********************************
************************************
******************************
******************************
****************************
************************ ***
*** *****************
******** ** *
******
********
******
**
This program has the same basic design as the plotter I built here, but conserves characters by eschewing custom sequence and plot-points types in favor of Standard Library methods. stride(from:through:by:)
iterates over the real (r
) and imaginary (i
) parts of the complex plane where the set lives, and sequence(first:next:)
is a wonderfully concise way to iterate the complex function whose behavior defines the set.
For each complex point Z[r, i]
, we plot a "*"
if the function is still bounded after 50 iterations; otherwise, we plot a space. The Julia implementation iterates the function call 50 times, so we examine the first 51 elements in the sequence, since sequence(first:next:)
gives the value passed in the parameter first
as the first element of the sequence, before continuing with the values obtained by iterating the function call.
The boundedness check is accomplished using allSatisfy(_:)
to determine whether all 50 initial function values in the sequence have a magnitude—a length
—no greater than 2.
Swift is clearly more verbose than Julia, but I’m quite happy with that in general. Swift reads more like prose, which in this case increases the possibility that someone who’s not familiar with the Mandelbrot set might learn how it works. Julia reads more like a domain-specific language for mathematics. Which is great! I think that’s one of its goals.1 If you’re already comfortable with the jargon and notational conventions of mathematics, Julia lets you translate your mathematical ideas into code very efficiently.
I think Julia’s biggest advantage over Swift in this test of terseness is its compact stride syntax:
for y=1.0:-0.05:-1.0
for x=-2.0:0.0315:0.5
versus Swift’s
for i in stride(from: 1.0, through: -1.0, by: -0.04) {
for r in stride(from: -2.0, through: 0.5, by: 0.02) {
But upon first encountering Julia’s stride syntax, it wasn’t immediately clear to me what it meant. I mean, not immediately immediately—it required a double take, but I figured it out without having to read the docs. It’s a greater degree of encoding than Swift’s stride(from:through:by)
, but once you decode it, you can read and type it very efficiently.
Swift has its own compact syntax for its open- and closed-ended range types, which can be used to implement pithier, but still very readable stride functions.
func stride<T>(_ range: ClosedRange<T>, by strideParam: T.Stride) -> StrideThrough<T> where T: Strideable {
stride(from: range.lowerBound, through: range.upperBound, by: strideParam)
}
func stride<T>(_ range: Range<T>, by strideParam: T.Stride) -> StrideTo<T> where T: Strideable {
stride(from: range.lowerBound, to: range.upperBound, by: strideParam)
}
So in the alternate timeline where those are already part of the Standard Library, our minimum Mandelbrot plotter reduces to this:
import Numerics
typealias Z = Complex<Double>
for i in stride(-1.0...1.0, by: 0.05) {
for r in stride(-2.0...0.5, by: 0.0315) {
sequence(first: Z(0, 0)) { z in
z*z + Z(r, i)
}
.prefix(51)
.allSatisfy { $0.length <= 2 } ?
print("*", terminator: "")
: print(" ", terminator: "")
}
print()
}
310 characters, a savings of 29! We’re getting there! By which I mean the arbitrary goal of tweetability. (Why do I care? I don’t even have a Twitter account.2)
I must confess that in addition to requiring the many worlds interpretation, there’s another character-conserving cheat here. Since we’re printing in the console without any fancy libraries, we must print rows from top to bottom, which is why the Julia program and the original Swift program stride over the imaginary values “backwards”: from greater to lesser values. With stride(from:through:by:)
, this is done by passing the upper bound as from
and the lower bound as through
, and passing a negative value for by
. But we can’t do that with the slimmer stride
:
for i in stride(1.0...(-1.0), by: -0.05) {
print(i)
}
// -> Fatal error: Can't form Range with upperBound < lowerBound
Even after adding two parenthesis characters to appease the compiler, we get an error at runtime, when it becomes obvious we’ve illegally inverted the relationship of the upper and lower bound members of a ClosedRange
.
To plot the Mandelbrot set in the conventional complex plane, where imaginary values increase up the vertical axis, we’d have to write the outer loop as
for i in stride(-1.0...1.0, by: 0.05).reversed() {
// [...]
}
But that restores 11 of our cleverly-eliminated characters! Thankfully, the Mandelbrot set is symmetric about the horizontal axis, so only a careful reading of the code will reveal that we’ve plotted it upside-down to save characters.
The Swift program’s character count also suffers from Swift’s lack of a concise expression for printing without a trailing newline. Julia has print()
, which doesn’t output a newline, and println()
, which does.
Swift has print(_:separator:terminator:)
. The first parameter is zero or more things to print. separator
is the string that’s interposed between each printed item, and it defaults to " "
. terminator
is the string printed after everything else, and it defaults to "\n"
. Printing without a newline in Swift requires something like
print("hello", terminator: "")
I’m less pleased with this specific case of verbosity. It’s very flexible, but printing without a newline is common enough when writing a console app that it’s nice to have a quick way to say it. The Julia Mandelbrot plotter needs only
print("*") : print(" ")
where Swift requires
print("*", terminator: "") : print(" ", terminator: "")
23 vs. 55 characters. More than 55 for Swift, if we wrap the long line containing that fragment.
I haven’t thought of a way to write short-named wrapper functions for print(_:separator:terminator:)
that isn’t a kludge. A substantial part of the difficulty is finding a trim but not ridiculous way to write “printWithNoTerminator”. For now, I’ll have to invoke yet another alternative timeline—one that forked off from our own much further back in the history of Swift, where it was decided to have separate print()
and println()
functions, like Julia. Then we could have the program
import Numerics
typealias Z = Complex<Double>
for i in stride(-1.0...1.0, by: 0.05) {
for r in stride(-2.0...0.5, by: 0.0315) {
sequence(first: Z(0, 0)) { z in
z*z + Z(r, i)
}
.prefix(51)
.allSatisfy { $0.length <= 2 } ? print("*") : print(" ")
}
println()
}
274 characters! We’ve achieved tweetability! But even if it didn’t require transport to an alternate universe, this is a hollow victory: separate print()
and println()
functions don’t seem like idiomatic Swift to me. As verbose as it sometimes is, print(_:separator:terminator:)
is native Swift. But I’d love to hear about a more compact, yet still Swifty way to print strings without newlines!
I said at the outset I wanted an idiomatic Swift program. I don’t presume to be the arbiter of the Swift idiom. But a big part of my current thinking about it is that it means using the Standard Library wherever it meets my needs.
I was so smitten by the idea of using allSatisfy(_:)
on a Sequence
to test boundedness when I wrote the first 𝜋 in the Mandelbrot set post that I haven’t considered whether there are better alternatives. I’m starting to have some thoughts about differences between my strategy and that of the Julia program, but that’s another post.
-
I know very little about Julia beyond this Mandelbrot-plotting program, so I can’t say whether the program I’ve reproduced here is particularly idiomatic. It is, however, the first program presented when you click the “See Julia Code Examples” link on the home page of the official Julia language site. I’d love to know if an experienced Julia programmer might do it any differently. ↩
-
I had a Twitter account, but I deleted it a few years ago, because I was too obsessed with reading everything in my timeline every day, and that included so much toxicity. It was pure consumption on my part—I never tweeted anything. But lately I’ve been considering that I’ve gotten a lot of value from the high quality signal that can be strained out of the noise (I read programming-related feeds via the web). And since I’ve started blogging, I could begin repaying that with my own modest contributions. So maybe? ↩