Find neighbors in a matrix without using loops, logical operators, assignment operators

In a given two-dimensional array, find the values of the neighbors for each element.

For example, the array:

1 2 3
4 5 6
7 8 9

For the element "1" neighbors: [2, 4, 5]
For the element "5" neighbors: [1, 2, 3, 4, 6, 7, 8, 9]
That is, the elements differ from the current one by the index + - 1.

The difficulty is that you need to solve only the methods of functional programming: recursion, and/or, map, reduce, zip, etc.
BUT without using loops, logical operators, assignment operators.

Author: jfs, 2018-04-11

2 answers

Solution without explicit for, if, =:

#!/usr/bin/env python3
from functools import partial
from itertools import product

def all_neighbors(matrix):
    def neighbors(i, j):
        def valid_index(dij):
            return (any(dij)
                    and 0 <= (i+dij[0]) < len(matrix)
                    and 0 <= (j+dij[1]) < len(matrix[i+dij[0]]))
        return tuple(map(lambda dij: matrix[i+dij[0]][j+dij[1]],
                         filter(valid_index,
                                product((-1, 0, 1), repeat=2))))

    def row_neighbors(i):
        return tuple(map(partial(neighbors, i), range(len(matrix[i]))))

    return map(row_neighbors, range(len(matrix)))

Example:

print(*all_neighbors([[1, 2, 3, 4], [5, 6, 7, 8]]), sep='\n')

Result:

((2, 5, 6), (1, 3, 5, 6, 7), (2, 4, 6, 7, 8), (3, 7, 8))
((1, 2, 6), (1, 2, 3, 5, 7), (2, 3, 4, 6, 8), (3, 4, 7))

By adding dummy elements to the borders, you can do without explicitly checking the boundaries for the indexes:

#!/usr/bin/env python3
from functools import partial
from itertools import chain, product
from operator import is_not

def add_edges(matrix,  sentinel=None):
    assert matrix
    return (list(chain([[sentinel] * (len(matrix[0]) + 2)],
                       map(lambda row: [sentinel] + row + [sentinel], matrix),
                       [[sentinel] * (len(matrix[0]) + 2)])),
            partial(is_not, sentinel))

def mesh(ys, xs, keep=lambda yx: True):
    return zip(*filter(keep, product(ys, xs)))

def all_neighbors_flat(matrix, not_sentinel):
    def neighbors(i, j):
        return tuple(filter(not_sentinel,
                            map(lambda di, dj: matrix[i+di][j+dj],
                                *mesh(*[(-1, 0, 1)]*2, keep=any))))
    return map(neighbors, *mesh(range(1, len(matrix)-1),
                                range(1, len(matrix[0])-1)))

Example:

print(*all_neighbors_flat(*add_edges([[1, 2, 3, 4], [5, 6, 7, 8]])), sep='\n')

What does * (asterisk) and ** double asterisk mean in Python?

Here the output is flat (all neighbors in a row):

(2, 5, 6)
(1, 3, 5, 6, 7)
(2, 4, 6, 7, 8)
(3, 7, 8)
(1, 2, 6)
(1, 2, 3, 5, 7)
(2, 3, 4, 6, 8)
(3, 4, 7)

You can group them in rows as in the first example shown.

Aside: about functional programming (FP)

Absence/availability for,if,= by itself, it does not make the program "functional". The OP consists in using pure (referentially transparent (RT)) functions (as functions are defined in mathematics). As a result, the values of objects do not change, but only new ones are created by expressions, in a declarative style, in which almost everything is a value, including the functions themselves (hence using "higher-order functions" that functions accept, return).


Logical operators and, or do not contradict the FP in any way: for fixed input, they always return the same results, without side effects (RT). And even the short-circuit behavior of these operators is in the spirit of lazily calculating only the necessary parts in the OP. The operators are available as values as functions in the operator module.


Using the " assignment operator" = (as it is defined in Python) itself also does not contradict the OP. a = 1 binds the name a to an object created by a 1 constant (since int objects are immutable in Python, it can always be the same object). If = is used only once for each name, then (up to NameError) you can always replace the name with the corresponding expression on the right side of =. The use of the name in this case is just syntax sugar to improve the readability of the code. If if = is forbidden, then named parameters in the function cannot be passed (objects passed as arguments are bound to the corresponding names when the function is called), and def and import are all operations that names enter.

For example, two fragments, one with def, the other with =, and lambda return the same result:

>>> def f():
        return 1
>>> f()
1

And

>>> f = lambda: 1
>>> f()
1

There are slight differences in the meaning f (f.__name__), but the values returned by the functions are identical:

>>> import dis
>>> def f(): return 1
...
>>> dis.dis(f)
  1           0 LOAD_CONST               1 (1)
              2 RETURN_VALUE
>>> f = lambda: 1
>>> dis.dis(f)
  1           0 LOAD_CONST               1 (1)
              2 RETURN_VALUE

The presence of for - cycles is also not necessary in conflict with the OP. For example, if we want to get the squares of the even elements of the collection:

def even_squares(items):
    return map(lambda x: x*x, filter(lambda x: x % 2 == 0, items))

The absence of map / filter and the presence of for/if do not change the result in any way [idiomatic version]:

def even_squares(items):
    return (x*x for x in items if x % 2 == 0)

Both in the case of map/filter and in the case of the generator (genexpr), x corresponds to different elements of the input collection. There is a formal difference in that x in the same local the context is changed in genexpr, but this can be considered an implementation detail, syntax sugar (list generator (listcomp) was created to replace the variant with map/filter as loop macro in Lisp, or as the presence of do notation does not turn Haskell into an imperative language).

Briefly: the formal presence of loops, logical operators, and assignment operators does not in itself contradict the functional programming style.

 3
Author: jfs, 2018-04-13 12:48:19
def in_bounds(i, j, x):                                                     
    return 0 <= i < len(x) and 0 <= j < len(x[0])                           

def neighbors(x):                                                           
    return [[x[i + di][j + dj] for di in (-1, 0, 1) for dj in (-1, 0, 1) if in_bounds(i + di, j + dj, x) and (di != 0 or dj != 0)]
            for i in range(len(x)) for j in range(len(x[0]))]               

x = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]                                       
print(neighbors(x))

Output:

[[2, 4, 5], [1, 3, 4, 5, 6], [2, 5, 6], [1, 2, 5, 7, 8], [1, 2, 3, 4, 6, 7, 8, 9], [2, 3, 5, 8, 9], [4, 5, 8], [4, 5, 6, 7, 9], [5, 6, 8]]
 1
Author: Scarabyte, 2018-04-11 20:05:35