Well, the title says it all!

But let me explain. Last week, I stumbled upon a new APL apology post. It struck some deep chord in me and gave me an impulse to make another attempt to understand this beautifully weird language.

What I (somewhat unexpectedly) find out, is that besides the use of extensive character set and extreme terseness, APL has two main features that are not at all alien to Ruby: calculations through operation chaining, and an extensive library of array operations, suitable for said chaining (in Ruby, they are represented by Enumerable module).

At this point, I felt that probably some of APL approaches and examples could be translated to Ruby pretty straightforwardly, and that would be an idiomatic Ruby. To challenge this feeling, I experimented with translating the (in)famous APL’s one-line Conway’s Game of Life implementation—and succeeded to implement GoL in exactly one Ruby statement.

Of course, the implementation required the notion of “mathematical array”, matching that of APL, so technically it is “one statement plus supporting class”, but I still prefer to think about it as “one-statement” one, as the class implemented is of generic use (somewhat like Numo::NArray), and operations used are familiar to any Rubyist.

To look immediately at the final implementation you may jump straight to the repo. The rest of this article goes through the implementation process, closely following the explanations in APL’s version.


Before we start, some basic information about APL-style Arrays:

  • In APL, array is a mathematical array: it is a rectangular multidimensional matrix of scalars. It is represented by APL::Ary in Ruby code, and shortened to AA in explanations below.
require 'apl'
AA = APL::Ary
  • Scalar is number, or character, or another array. This means one should not confuse multidimensional arrays (matrices, with an equal number of elements alongside some dimension), and nested arrays. Example:
puts AA[1, 2, 3, 4].reshape(2, 2)
# 1 2
# 3 4
# -- two-dimensional matrix of numbers
puts AA[AA[1, 2], AA[3, 4]]
# ┌───┐ ┌───┐
# │1 2│ │3 4│
# └───┘ └───┘
# -- one-dimensional array of two one-dimensional arrays.
# Note the frames around nested arrays helping to understand the nesting
  • Array might be easily turned into scalar containing this array (and back) with #wrap/#unwrap:
a = AA[1, 2]
puts a
# 1 2
puts a.wrap
# ┌───┐
# │1 2│
# └───┘
puts a.wrap.unwrap
# 1 2
  • As usual for mathematical arrays, mathematical operations could be performed with scalar (like “add 2 to every element”) or another array of the same shape (like “add item of array A to item in a similar position of array B”)

That being said, let’s begin with some first generation of our Game of Life:

current_gen = AA[0, 0, 0, 0, 0, 0, 0, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0, 0].reshape(5, 5)
puts current_gen
# 0 0 0 0 0
# 0 0 1 1 0
# 0 1 1 0 0
# 0 0 1 0 0
# 0 0 0 0 0

So far, we’ve just created an array of 1s and 0s and “reshaped” it into 5×5 two-dimensional matrix.

The next step would be rotating it horizontally:

puts current_gen.hrotate(1)
# 0 0 0 0 0
# 0 1 1 0 0
# 1 1 0 0 0
# 0 1 0 0 0
# 0 0 0 0 0

#hrotate is simply shifting values in each row to the left, cyclically, and resembles (1-dimensional only) Array#rotate of Ruby.

Now, let’s produce 3 variants of the rotation: 1 to the left, none, and 1 to the right:

current_gen.wrap.product(AA[-1, 0, 1], &:hrotate)
# ┌─────────┐ ┌─────────┐ ┌─────────┐
# │0 0 0 0 0│ │0 0 0 0 0│ │0 0 0 0 0│
# │0 0 0 1 1│ │0 0 1 1 0│ │0 1 1 0 0│
# │0 0 1 1 0│ │0 1 1 0 0│ │1 1 0 0 0│
# │0 0 0 1 0│ │0 0 1 0 0│ │0 1 0 0 0│
# │0 0 0 0 0│ │0 0 0 0 0│ │0 0 0 0 0│
# └─────────┘ └─────────┘ └─────────┘

It becomes more peculiar at this point.

Let’s start with #product: It is like Ruby’s Array#product (make all possible combinations of two arrays), with one important difference: APL’s “product”, instead of just producing combinations, accepts also operation to immediately apply to them. So, in vanilla Ruby:

[1, 2].product([3, 4]).map { |a, b| "#{a}+#{b}"}
# => ["1+3", "1+4", "2+3", "2+4"]

…but in APL-style #product it would be expressed (pseudo-code) as just

[1, 2].product([3, 4]) { |a, b| "#{a}+#{b}"}

#wrap here is necessary so we will product just one entire array (as a scalar) per -1, 0, and 1, not each-row-per-each-number.

With that in hands, let’s produce some more rotations, shifting all numbers by -1, 0, and 1 vertically:

puts current_gen.wrap.product(AA[-1, 0, 1], &:hrotate).product(AA[-1, 0, 1], &:vrotate)
# ┌─────────┐ ┌─────────┐ ┌─────────┐
# │0 0 0 0 0│ │0 0 0 0 0│ │0 0 0 1 1│
# │0 0 0 0 0│ │0 0 0 1 1│ │0 0 1 1 0│
# │0 0 0 1 1│ │0 0 1 1 0│ │0 0 0 1 0│
# │0 0 1 1 0│ │0 0 0 1 0│ │0 0 0 0 0│
# │0 0 0 1 0│ │0 0 0 0 0│ │0 0 0 0 0│
# └─────────┘ └─────────┘ └─────────┘
# ┌─────────┐ ┌─────────┐ ┌─────────┐
# │0 0 0 0 0│ │0 0 0 0 0│ │0 0 1 1 0│
# │0 0 0 0 0│ │0 0 1 1 0│ │0 1 1 0 0│
# │0 0 1 1 0│ │0 1 1 0 0│ │0 0 1 0 0│
# │0 1 1 0 0│ │0 0 1 0 0│ │0 0 0 0 0│
# │0 0 1 0 0│ │0 0 0 0 0│ │0 0 0 0 0│
# └─────────┘ └─────────┘ └─────────┘
# ┌─────────┐ ┌─────────┐ ┌─────────┐
# │0 0 0 0 0│ │0 0 0 0 0│ │0 1 1 0 0│
# │0 0 0 0 0│ │0 1 1 0 0│ │1 1 0 0 0│
# │0 1 1 0 0│ │1 1 0 0 0│ │0 1 0 0 0│
# │1 1 0 0 0│ │0 1 0 0 0│ │0 0 0 0 0│
# │0 1 0 0 0│ │0 0 0 0 0│ │0 0 0 0 0│
# └─────────┘ └─────────┘ └─────────┘

Note that what we currently see is a 2 dimensional matrix (3×3) of 2-dimensional matrices (5×5): inner metrices are wrapped in frames.

Now, let’s sum them all up:

puts current_gen.wrap
  .product(AA[-1, 0, 1], &:hrotate)
  .product(AA[-1, 0, 1], &:vrotate)
  .flatten(1).reduce(&:+)
# ┌─────────┐
# │0 1 2 2 1│
# │1 3 4 3 1│
# │1 4 5 4 1│
# │1 3 3 2 0│
# │0 1 1 1 0│
# └─────────┘

#reduce here is the same Enumerable#reduce, and #flatten(1) also behaves the usual way, making 2-dimensional array into 1-dimensional. The important feature is that APL-style arrays are summing up element-wise, so now we have a sum of all 9 matrices, representing how many alive neighbors (including itself) every cell had.

Now, it should be noticed, that only cells with 3 or 4 should be alive in the next generation:

  • cell with 3 means “alive + 2 neighbors” (condition to live) or “empty with 3 neighbors” (condition to become alive)
  • cell with 4 means “alive + 3 neighbours” (condition to live) or “empty with 4 neighbours” (not a condition to become alive)

To check “whether it is equal to something”, we have .eq operator implemented. That’s, probably, most “non-Rubyish” part of the solution: instead of producing true/false, it gives 1/0. Unfortunately, it is important to the algorithm to always stay as numbers.

So, comparing with each number separately will give us…

puts current_gen.wrap
  .product(AA[-1, 0, 1], &:hrotate)
  .product(AA[-1, 0, 1], &:vrotate)
  .reduce(&:+).reduce(&:+)
  .eq(3)
# ┌─────────┐
# │0 0 0 0 0│
# │0 1 0 1 0│
# │0 0 0 0 0│
# │0 1 1 0 0│
# │0 0 0 0 0│
# └─────────┘

puts current_gen.wrap
  .product(AA[-1, 0, 1], &:hrotate)
  .product(AA[-1, 0, 1], &:vrotate)
  .flatten(1).reduce(&:+)
  .eq(4)
# ┌─────────┐
# │0 0 0 0 0│
# │0 0 1 0 0│
# │0 1 0 1 0│
# │0 0 0 0 0│
# │0 0 0 0 0│
# └─────────┘

But we also may apply both operations at once, producing array of two elements (ability to perform one operation several times is called pervasion in APL):

puts current_gen.wrap
  .product(AA[-1, 0, 1], &:hrotate)
  .product(AA[-1, 0, 1], &:vrotate)
  .flatten(1).reduce(&:+)
  .eq(AA[3, 4])
# ┌─────────┐ ┌─────────┐
# │0 0 0 0 0│ │0 0 0 0 0│
# │0 1 0 1 0│ │0 0 1 0 0│
# │0 0 0 0 0│ │0 1 0 1 0│
# │0 1 1 0 0│ │0 0 0 0 0│
# │0 0 0 0 0│ │0 0 0 0 0│
# └─────────┘ └─────────┘

Now, we need to add a condition of “whether it was alive” to the second array. It is easy to perform with just &-ing with original array. To not break our chain of operations, we’ll also & the first array with 1 (no-op):

puts current_gen.wrap
  .product(AA[-1, 0, 1], &:hrotate)
  .product(AA[-1, 0, 1], &:vrotate)
  .flatten(1).reduce(&:+)
  .eq(AA[3, 4])
  .zip(AA[1, current_gen], &:&)
# ┌─────────┐ ┌─────────┐
# │0 0 0 0 0│ │0 0 0 0 0│
# │0 1 0 1 0│ │0 0 1 0 0│
# │0 0 0 0 0│ │0 1 0 0 0│
# │0 1 1 0 0│ │0 0 0 0 0│
# │0 0 0 0 0│ │0 0 0 0 0│
# └─────────┘ └─────────┘

#zip here, like #product above, is almost like the Array#zip of Ruby, but also applies provided operation to each pair.

…and then reduce them with |-ing both:

puts current_gen.wrap
  .product(AA[-1, 0, 1], &:hrotate)
  .product(AA[-1, 0, 1], &:vrotate)
  .flatten(1).reduce(&:+)
  .eq(AA[3, 4])
  .zip(AA[1, current_gen], &:&)
  .reduce(&:|)
# ┌─────────┐
# │0 0 0 0 0│
# │0 1 1 1 0│
# │0 1 0 0 0│
# │0 1 1 0 0│
# │0 0 0 0 0│
# └─────────┘

…which is almost our final answer, but it is still a “scalar with array inside”, and we need to unwrap it:

puts current_gen.wrap
  .product(AA[-1, 0, 1], &:hrotate)
  .product(AA[-1, 0, 1], &:vrotate)
  .flatten(1).reduce(&:+)
  .eq(AA[3, 4])
  .zip(AA[1, current_gen], &:&)
  .reduce(&:|)
  .unwrap
# 0 0 0 0 0
# 0 1 1 1 0
# 0 1 0 0 0
# 0 1 1 0 0
# 0 0 0 0 0

So, here is the final solution:

def life(current_gen)
  current_gen.wrap
    .product(AA[-1, 0, 1], &:hrotate)
    .product(AA[-1, 0, 1], &:vrotate)
    .flatten(1).reduce(&:+)
    .eq(AA[3, 4])
    .zip(AA[1, current_gen], &:&)
    .reduce(&:|)
    .unwrap
end

Here is APL’s version:

Life←{↑1 ⍵∨.∧3 4=+/,¯1 0 1∘.⊖¯1 0 1∘.⌽⊂⍵}

For those interested, here is statement-by-statement equivalent¹ (note that APL statement performed right-to-left):

def life(current_gen)                 # Life←{    -- function declaration
  current_gen                         # ⍵         -- function argument
    .wrap                             # ⊂         -- wrap to make it scalar
    .product(                         # ∘.        -- product
      AA[-1, 0, 1],                   # ¯1 0 1    --  -1, 0, 1 (yeah, ¯1 is -1)
      &:hrotate)                      # ⌽         -- hrotate
    .product(                         # ∘.        -- product
      AA[-1, 0, 1],                   # ¯1 0 1
      &:vrotate)                      # ⊖         -- vrotate
    .flatten(1)                       #           -- flatten
    .reduce(&:+)                      # +/        -- reduce to sum
    .eq(                              # =
      AA[3, 4])                       # 3 4
    .zip(                             #           -- see below¹ about those 3 lines
      AA[1, current_gen],             # 1 ⍵
      &:&).reduce(&:|)                # ∨.∧
    .unwrap                           # ↑         -- unwrap
end                                   # }

¹I made one simplification: abstained from implementing quite complicated APL’s “inner product” operator (which takes two functions and makes them into something that I represented in regular Ruby with zip+reduce).

That’s it! Now, for the fun example of usage

Glider on the grid.

Original APL’s article demonstrates the usage by moving Glider through 10×10 grid. Let’s try this. But first, we want a method to display those 0s and 1s a bit prettier. Again, borrowed from APL:

def show(grid)
  # APL-style AA#values_at(aa) produces array of items from the first array, taken and shaped
  # using numbers from second array as indexes.
  puts AA[' ', '█'].values_at(grid)
end

Now:

glider = AA[1, 1, 1, 1, 0, 0, 0, 1, 0].reshape(3, 3)
grid = glider.take(-10, -10)

show grid.wrap
# ┌──────────┐
# │          │
# │          │
# │          │
# │          │
# │          │
# │          │
# │          │
# │       ███│
# │       █  │
# │        █ │
# └──────────┘


generations = [grid]
9.times { generations << life(generations.last) }

show AA[*generations]

# or, simpler, with 2.7's Enumerator#produce:

generations = Enumerator.produce(grid) { |cur| life(cur) }.take(10).map(&:wrap)
show AA[*generations]
# ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
# │          │ │          │ │          │ │          │ │          │ │          │ │          │ │          │ │          │ │          │
# │          │ │          │ │          │ │          │ │          │ │          │ │          │ │          │ │          │ │          │
# │          │ │          │ │          │ │          │ │          │ │          │ │          │ │          │ │          │ │          │
# │          │ │          │ │          │ │          │ │          │ │          │ │          │ │          │ │          │ │          │
# │          │ │          │ │          │ │          │ │          │ │          │ │          │ │          │ │          │ │      █   │
# │          │ │          │ │          │ │          │ │          │ │       █  │ │      ██  │ │      ██  │ │     ███  │ │     ██   │
# │          │ │        █ │ │       ██ │ │       ██ │ │      ███ │ │      ██  │ │      █ █ │ │     ██   │ │     █    │ │     █ █  │
# │       ███│ │       ██ │ │       █ █│ │      ██  │ │      █   │ │      █ █ │ │      █   │ │       █  │ │      █   │ │          │
# │       █  │ │       █ █│ │       █  │ │        █ │ │       █  │ │          │ │          │ │          │ │          │ │          │
# │        █ │ │          │ │          │ │          │ │          │ │          │ │          │ │          │ │          │ │          │
# └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘

That’s all!

GitHub repo

UPD 2020-05-21: Some additions brought by Twitter:

  • Here is a blog post which achieves the same task by cleverly adding just a several methods to Ruby stdlib’s Matrix class. It is from 2009 and in Japanese, but at the end you can see recently added Ruby 2.7 version of the code.
  • Here is a transcript (Google Translate) of—again, from 2009, again, in Japanese—Matz’s lecture, where he suggests (if you believe Google Translate) that “ the characteristics of APL may affect programming languages ​​in the future”.