Skip to main content

Command Palette

Search for a command to run...

Handling N-dimensional Lists in Elixir with Ndim

Updated
4 min read

Process the n-dimensional list

In data processing and numerical computing, operations on n-dimensional lists are common. These nested data structures represent matrices, tensors, and other multi-dimensional arrays. Here's an example of such structures:

# 2 dimensional list (matrix)
[
  [1, 2],
  [3, 4]
]

As an elixir alchemist, use Enum.map/2 to transform lists is our second nature. However, when dealing with nested lists, transforming elements at specific depths requires multiple nested mapping operations. Consider these approaches:

[[1, 2], [3, 4]]
|> Enum.map(fn row ->
     Enum.map(row, fn i ->
       i * 10
     end)
   end)

#=> [[10, 20], [30, 40]]

When working with three or higher dimensional lists, the complexity of operations increases significantly:

[
  [
    [1, 2]
  ],
  [
    [3, 4]
  ]
]
|> Enum.map(fn row ->
     Enum.map(fn column ->
       Enum.map(fn i ->
        i * 10
       end)
     end)
end)

Function Lifting through Functors in Haskell

In Haskell, a lazy evaluation functional programming language, lists implement the Functor typeclass. This allows us to 'lift' functions that work on single values into a function works on lists, using fmap.

For example, if we have a function (+ 1) that adds 1 to a single number, fmap (+ 1) lifts this function to work on an entire list of numbers, transforming each element. This is conceptually similar to Enum.map/2 in Elixir.

And the key feature of function lifting is its composability through functor composition. Consider how the same function can be lifted to operate on increasingly nested structures:

-- Function operating on a single value
(+ 1) 1
--> 2

-- Function lifted to operate on a list (1-dimensional)
fmap (+ 1) [1, 2]
--> [2, 3]

-- Function lifted to operate on a nested list (2-dimensional)
(fmap . fmap) (+ 1) [[1, 2], [3, 4]]
--> [[2, 3], [4, 5]]

-- Function lifted to operate on a doubly nested list (3-dimensional)
(fmap . fmap . fmap) (+ 1) [[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
--> [[[2, 3], [4, 5]], [[6, 7], [8, 9]]]

The composition of fmap with itself (fmap . fmap) allows us to reach deeper into nested structures while preserving their dimensionality. Each additional composition of fmap adds another level of penetration into the nested structure.

Using Kernel.get_in/3

In Elixir, we can achieve similar behavior using Kernel.update_in/3 with Access.all/1. This approach allows us to traverse and transform nested lists at specific depths:

# list
[1, 2] |> update_in([Access.all()], &(&1 + 1)) #=> [2, 3]

# 2 dimensional list
[[1, 2], [3, 4]] |> update_in([Access.all(), Access.all()], &(&1 + 1))
#=> [[2, 3], [4, 5]]

# 3 dimensional list
[[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
|> update_in([Access.all(), Access.all(), Access.all()], &(&1 + 1))
#=> [[[2, 3], [4, 5]], [[6, 7], [8, 9]]]

While this approach works, it requires repetitive use of Access.all() for each level of nesting, making it syntactically verbose.

Dimensional Mapping with Ndim

The Ndim library, named for its ability to handle n-dimensional lists, provides a more concise syntax for transforming nested lists. It offers both dimension-specific functions and a general-purpose mapping function:

# Transform elements in a 2-dimensional list
[[1, 2], [3, 4]]
|> Ndim.d2map(&(&1 + 1))
#=> [[2, 3], [4, 5]]

# Transform elements in a 3-dimensional list
[[[1, 2], [3, 4]], [[5, 6], [7, 8]]]
|> Ndim.d3map(&(&1 + 1))
#=> [[[2, 3], [4, 5]], [[6, 7], [8, 9]]]

In addition to the dimension-specific functions (d2map/2 through d5map/2), Ndim provides a general-purpose dmap/3 that can operate on lists of any dimension:

# Transform elements at any specified dimension
nested_list |> Ndim.dmap(dimension, transformation_function)

# Example: Transform a 6-dimensional list
deeply_nested_list |> Ndim.dmap(6, &(&1 + 1))

Converting N-dimensional Lists to Coordinate Maps

The Ndim library also provides functionality to transform n-dimensional lists into coordinate maps, where each value is keyed by its dimensional coordinates. Currently supports 2-dimensional and 3-dimensional structures:

# Converting a 2-dimensional list to a coordinate map
[[1, 2], [3, 4]]
|> Ndim.to_coordinate_map()
#=> %{{0, 0} => 1, {0, 1} => 2, {1, 0} => 3, {1, 1} => 4}

# Converting a 3-dimensional list to a coordinate map
[[[1, 2]], [[3, 4]]]
|> Ndim.to_coordinate_map()
#=> %{{0, 0, 0} => 1, {0, 0, 1} => 2, {1, 0, 0} => 3, {1, 0, 1} => 4}

This coordinate mapping is particularly useful for:

  • Sparse matrix operations

  • Direct coordinate-based access

  • Grid-based calculations


Hope you like it, and happy hacking!

More from this blog

聊聊 Elixir 中的 type

最近有幾位朋友分別來問 Elixir 的 type 的問題,想說中文世界好像沒有比較完整的東西,就把知道的東西整理出來。 (目前) Elixir 的 type 能做什麼? tl;dr: 最主要是文件,然後在某種程度下防止錯誤。 我覺得這應該是在研究 Elixir 的 type 時最需要知道的事情了。不像 Haskell 及 F# 這種以型別著稱的 ML 系語言,Elixir / Erlang 本質上是個動態語言,所有與型別有關的標註都會被編譯器忽略。而 Erlang 內建的型別檢查工具 dia...

Oct 18, 20222 min read

Steam 上的程式教學類遊戲

農曆年期間比較有空,玩了一些之前買的遊戲。這次特別試了幾個標榜讓不會寫程式的人學寫程式的遊戲。分享一下試玩的心得。 1. 7 Billion Humans 考慮到劇情的話我最喜歡的是 7 Billion Humans。它用拖拉語法的方式下指令,一開始還蠻好上手的,但是因為只有 goto 那樣的結構,而操作的時候又是一次對所有的 worker 下指令,所以常常要想一下執行後每個人運作的順序。但是介面有正體中文,以「想要體驗一下寫程式大概是怎麼一回事」來說還蠻適合的。 2. while Tru...

Feb 24, 20201 min read

Let's (re)start from here.

最近的時間大半都花在這上面了。 算算應該是第五次弄部落格系統。算一下扣除上古時期用現成的之外,每個系統平均各寫六篇文章,也都撐不過兩年。前幾個分別用了 Refinery CMS -> jekyll -> middleman -> jekyll。想來架系統的總時數應該超過寫文章的時間 XD 而這次用上了 Gatsby + tailwindcss,除了恢復一下 GraphQL 的手感之外,這次還挑戰了不套別人做的版型,自己把類似上一個部落格的 style 刻出來。想說來分享一下這些技術的感想: G...

Jan 11, 20201 min read

Mostly Functional

31 posts

/.(ex|jsx?|rb|hs|rs|py)/, A father, a bookworm, a pluviophile. Co-organizer of http://Elixir.tw. Learning Satir, coaching & mediation.