nim-lang / RFCs

A repository for your Nim proposals.
135 stars 26 forks source link

`template fn(a: iterable[T])` should accept typed args with implicit `items` inserted, like `for` #397

Open timotheecour opened 3 years ago

timotheecour commented 3 years ago

A lot of APIs could benefit from the new iterable type (which accepts called iterators), but right now there's still 1 issue: iterable[T] only accepts called iterators, not values (eg seq[T]).

This proposal would allow a value a with items(a) defined on it to match against iterable[T], in the same way that for implicitly adds items:

template fn(a: iterable[T]) = discard
template iota(n: int): int = for i in 0..<n: yield i
fn(iota(3)) # ok
fn(items(@[0, 1, 2]) # ok
fn(@[0, 1, 2]) # before RFC: CT error; with RFC: transformed into fn(items(@[0, 1, 2]))

the implicit conversion would have lower priority than an explicit overload, eg:

template fn(a: iterable[T]): int = 1
template fn[T](a: T): int = 2
assert fn(iota(3)) == 1
assert fn(@[0,1,2]) == 2
# ditto if we had: `template fn[T](a: seq[T]): int = 2` etc

explicit overloads are still useful in cases where you need extra information from the type, eg a len; but in many cases, the iteration is all you need and an overload doesn't add value

benefits

avoids having to add need-less boilerplate that would require adding overloads for both iterable[T] and Iterable[T] where the latter is defined below:

before RFC: requires boilerplate of adding template bar(a: Iterable[int]): untyped = bar(items(a))

when true:
  type Foo = object
  iterator mitems(a: Foo): int = discard
  iterator items(a: Foo): int = discard
  template bar(a: iterable[int]) = echo "in bar"
  type Iterable[T] = concept x
    items(x) is T
  template bar(a: Iterable[int]): untyped = bar(items(a))
  # template bar(a: Iterable[int] | iterable[int]) = # doesn't work, and probably a bad idea to conflate typed and iterable

  var x: Foo
  bar(x.items)
  bar(x.mitems)
  bar(x)

after RFC:

when true:
  type Foo = object
  iterator mitems(a: Foo): int = discard # not needed; just to illustrate
  iterator items(a: Foo): int = discard
  template bar(a: iterable[int]) = echo "in bar"

  var x: Foo
  bar(x.items)
  bar(x.mitems)
  bar(x)

note 1

this would cause infinite loop in compiler, and even if it were fixed, would be a worse solution as converters have a more subversive side effect on type system

converter toIterable*[T](a: Iterable[T]): iterable = items(a)

links

juancarlospaco commented 3 years ago

Typo on title. Sounds useful for JS targets too. :+1:

Varriount commented 3 years ago

I'd like to add that Python has two concepts for this: "iterable" and "iterator".

In pseudo-code:

type
  Iterable[T, iT] = concept
    iter(self: T): iT
  Iterator[T, iT, vT] = concept of Iterable
    iter(self: T): iT
    next(self: T): vT

(Is it even possible to properly specify this using the current concept implementation?)

With this, all a for loop needs to do is call iter on its argument, and then repeatedly call next on the returned iterator. This works because calling iter on an iterable will just return the passed in iterable (it acts as the identity function).