5

I would like to define overloads of map and min/max (as originally defined in sequtils) that works for tables.keys. Specifically, I want to be able to write something like the following:

import sequtils, sugar, tables

# A mapping from coordinates (x, y) to values.
var locations = initTable[(int, int), int]()
# Put in some random values.
locations[(1, 2)] = 1
locations[(2, 1)] = 2
locations[(-2, 5)] = 3

# Get the minimum X coordinate.
let minX = locations.keys.map(xy => xy[0]).min
echo minX

Now this fails with:

/usercode/in.nim(12, 24) Error: type mismatch: got <iterable[lent (int, int)], proc (xy: GenericParam): untyped>
but expected one of:
proc map[T, S](s: openArray[T]; op: proc (x: T): S {.closure.}): seq[S]
  first type mismatch at position: 1
  required type for s: openArray[T]
  but expression 'keys(locations)' is of type: iterable[lent (int, int)]

expression: map(keys(locations), proc (xy: auto): auto = xy[0])

Below are my three attempts at writing a map that works (code on Nim playground: https://play.nim-lang.org/#ix=3Heq). Attempts 1 & 2 failed and attempt 3 succeeded. Similarly, I implemented min using both attempt 1 & attempt 2, and attempt 1 failed while attempt 2 succeeded.

However, I'm confused as to why the previous attempts fail, and what the best practice is:

  • Why does attempt 1 fail when the actual return type of the iterators is iterable[T]?
  • Why does attempt 2 fail for tables.keys? Is tables.keys implemented differently?
  • Is attempt 2 the canonical way of taking iterators / iterables as function arguments? Are there alternatives to this?

Attempt 1: Function that takes an iterable[T].

Since the Nim manual seems to imply that the result type of calling an iterator is iterable[T], I tried defining map for iterable[T] like this:

iterator map[A, B](iter: iterable[A], fn: A -> B): B =
  for x in iter:
    yield fn(x)

But it failed with a pretty long and confusing message:

/usercode/in.nim(16, 24) template/generic instantiation of `map` from here
/usercode/in.nim(11, 12) Error: type mismatch: got <iterable[(int, int)]>
but expected one of:
iterator items(a: cstring): char
  first type mismatch at position: 1
  required type for a: cstring
  but expression 'iter' is of type: iterable[(int, int)]
... (more output like this)

From my understanding it seems to say that items is not defined for iterable[T], which seems weird to me because I think items is exactly what's need for an object to be iterable?


Attempt 2: Function that returns an iterator.

I basically copied the implementation in def-/nim-itertools and defined a map function that takes an iterator and returns a new closure iterator:

type Iterable[T] = (iterator: T)

func map[A, B](iter: Iterable[A], fn: A -> B): iterator: B =
  (iterator: B =
    for x in iter():
      yield fn(x))

but this failed with:

/usercode/in.nim(25, 24) Error: type mismatch: got <iterable[lent (int, int)], proc (xy: GenericParam): untyped>
but expected one of:
func map[A, B](iter: Iterable[A]; fn: A -> B): B
  first type mismatch at position: 1
  required type for iter: Iterable[map.A]
  but expression 'keys(locations)' is of type: iterable[lent (int, int)]
proc map[T, S](s: openArray[T]; op: proc (x: T): S {.closure.}): seq[S]
  first type mismatch at position: 1
  required type for s: openArray[T]
  but expression 'keys(locations)' is of type: iterable[lent (int, int)]

expression: map(keys(locations), proc (xy: auto): auto = xy[0])

which hints that maybe tables.keys doesn't return an iterator?


Attempt 3: Rewrite keys using attempt 2.

This replaces tables.keys using a custom myKeys that's implemented in a similar fashion to the version of map in attempt 2. Combined with map in attempt 2, this works:

func myKeys[K, V](table: Table[K, V]): iterator: K =
  (iterator: K =
    for x in table.keys:
      yield x)
Zecong Hu
  • 2,584
  • 18
  • 33
  • 6
    I don't know what is the cause of this issue. However, my brief opinions on iterators is that they are compile time for loop readability helpers and shouldn't be used as objects like in python. Closure iterators are a different story, but most (all?) system iters are implemented as inline. My suggestion is to use **toSeq**: `locations.keys.toSeq.map(xy => xy[0]).min`. – hola Dec 07 '21 at 21:40

1 Answers1

0

Explanation of errors in first attempts

which hints that maybe tables.keys doesn't return an iterator

You are right. It does not return an iterator, it is an iterator that returns elements of the type of your Table keys. Unlike in python3, there seems to be no difference between type(locations.keys) and type(locations.keys()). They both return (int, int).

Here is keys prototype:

iterator keys[A, B](t: Table[A, B]): lent A

The lent keyword avoids copies from the Table elements.

Hence you get a type mismatch for your first and second attempt: locations.keys.map(xy => xy[0]) has an incorrect first parameter, since you get a (int, int) element where you expect a iterable[A].

Proposals

  1. As for a solution, you can either first convert your keys to a sequence (which is heavy), like hola suggested.
  2. You can directly rewrite a procedure for your specific application, mixing both the copy in the sequence and your operation, gaining a bit in performance.
import tables

# A mapping from coordinates (x, y) to values.
var locations = initTable[(int, int), int]()

# Put in some random values.
locations[(1, 2)] = 1
locations[(2, 1)] = 2
locations[(-2, 5)] = 3

func firstCoordinate[X, Y, V](table: Table[(X, Y), V]): seq[X] =
  result = @[]
  for x in table.keys:
    result.add(x[0])

let minX = locations.firstCoordinate.min
echo minX

This is not strictly adhering your API, but should be more efficient.

Dimitri Lesnoff
  • 317
  • 1
  • 14