iyi

Standard library, 0.18.0

std/enumerable

Enumerable — SPEC.md II.6.

import std/enumerable to use it. Source: src/std/enumerable.iyi at v0.18.0.

The load-bearing abstraction of a collections library: 71 method names written against a single abstract def each, shared by every type that can yield its elements. If traits cannot express this shape, the library cannot be written in the idiom the pitch promises and the ergonomics half of it fails. So this file is the evidence for the claim, not an illustration of it.

./bin/crystal run samples/iyi/collections.iyi

All 71 method names are here, in 72 defs. Every one of them is called by the exercises, because a default method that is never called is never typed: compiling this file proves nothing on its own. It needs all three of what II.6 said it would need, and nothing else:

* `type Elem` — an associated type. The element type is an output of the
  impl, not something the caller picks. Were it a parameter, one type could
  implement `Enumerable(String)` and `Enumerable(Int32)` and `arr.map`
  would be ambiguous about which impl it meant.
* `forall U` on methods — `map`, `flat_map`, `group_by`, `min_by`,
  `reduce`, `accumulate` and eight more. `U` belongs to the method, not to
  the trait.
* `where Elem : Comparable` — about a quarter of `Enumerable` is valid
  only for some element types. `max`, `min`, `sum`, `product`, `tally`, `sorted`
  and `minmax` say so instead of duck-typing. `sum` and
  `product` also need `Num` to require `self.zero` and `self.one`: an
  identity belongs to the type, since an empty collection has no element
  to ask.

What this file corrected in the spec's own sketch:

* II.6 writes `abstract def each(&block : Elem -> Nil)`. That form
  *captures* the block into a `Proc`, and a captured block cannot `return`
  from the enclosing method — which kills early exit in `find`, `any?`,
  `all?`, `none?`, `first`, `take`, `take_while`, `empty?`, `index` and
  `find_value`. The block has to be typed without being captured, as
  `& : Elem -> Nil`, and yielded.
* Two of these are elsewhere spelled `find!` and `index!`. With `!` out of
  identifiers (III.1.7) the file takes the convention `Enumerable` already
  uses everywhere else — `max?`/`max`, `first?`/`first` — and applies it
  here too: `?` is the nilable one, the plain name raises. Dropping `!`
  made the library *more* consistent, not less.

Enumerabletrait

std/enumerableiyi doc src/std/enumerable.iyi
pub trait Enumerable
  type Elem
  def accumulate(initial : U, & : (U, Elem -> U)) : Array(U) forall U
  def all?(& : (Elem -> Bool)) : Bool
  def any?(& : (Elem -> Bool)) : Bool
  # The same flag as `each_cons_pair`: with the nil test, a nil element
  # read as "no previous element", so the block was never asked about the
  # element after it and `[nil, nil, 1]` came back as one chunk.
  def chunk_while(& : (Elem, Elem -> Bool)) : Array(Array(Elem))
  def compact_map(& : (Elem -> U | ::Nil)) : Array(U) forall U
  def count(& : (Elem -> Bool)) : Int32
  def cycle(n : Int32, & : (Elem -> Nil)) : Nil
  abstract def each(& : (Elem -> Nil)) : Nil
  # Yields each overlapping window of *n* elements; nothing when the source
  # is shorter than one window.
  def each_cons(n : Int32, & : (Array(Elem) -> Nil)) : Nil
  def each_cons(n : Int32) : Array(Array(Elem))
  # Whether there is a previous element is a flag, not a nil: an element
  # that is itself nil is still an element, and `[nil, 2]` has the pair
  # `{nil, 2}`. The nil test skipped every pair whose first half was nil.
  def each_cons_pair(& : (Elem, Elem -> Nil)) : Nil
  # Yields each window of *n* elements, the last one short if the source
  # does not divide. The block form is the one `each_` promises everywhere
  # else in this trait — `each`, `each_with_index`, `each_with_object`,
  # `each_cons_pair`, `each_step` all take one — and these two were the
  # exception: they materialised `Array(Array(Elem))` and refused a block,
  # which allocates every window up front, the cost a block exists to
  # avoid. `Indexable` has had both shapes all along.
  def each_slice(n : Int32, & : (Array(Elem) -> Nil)) : Nil
  # And the same windows as an array, for a caller that wants them all.
  def each_slice(n : Int32) : Array(Array(Elem))
  def each_step(n : Int32, & : (Elem -> Nil)) : Nil
  def each_with_index(& : (Elem, Int32 -> Nil)) : Nil
  # The element first and the memo second, as the other library yields them.
  # It was the reverse, so `[[1], [2]].each_with_object([] of Int32) { |x,
  # acc| acc.concat(x) }` compiled under both and answered `[]` here: the
  # element was the one mutated (SPEC.md III.1.7a's worst shape).
  def each_with_object(obj : U, & : (Elem, U -> Nil)) : U forall U
  def empty? : Bool
  def find(& : (Elem -> Bool)) : Elem | ::Nil
  # The first truthy answer, as the other library's: this took the first
  # non-nil one, `false` included, so `[1, 2, 3, 4].find_value { |i| i > 2 }`
  # answered false where the other library documents true for that very
  # call, and `{ |i| i > 8 }` answered false where it answers nil.
  def find_value(& : (Elem -> U | ::Nil)) : U | ::Nil forall U
  def first(n : Int32) : Array(Elem)
  def first : Elem
  def first? : Elem | ::Nil
  def flat_map(& : (Elem -> Array(U))) : Array(U) forall U
  def group_by(& : (Elem -> U)) : Hash(U, Array(Elem)) forall U : Hashable
  # Refused under its own name, as `minmax` below is: delegated, a size of
  # zero was refused with `each_slice`'s sentence, "slice size must be
  # positive", from a call the reader never wrote.
  def in_groups_of(n : Int32, filler : Elem) : Array(Array(Elem))
  def in_slices_of(n : Int32) : Array(Array(Elem))
  # Equality, as `Array#includes?` asks it: `<=>` answering 0 is ordering,
  # and for a float it said a NaN was present in a list of NaNs, which no
  # `==` says and `[nan].includes?(nan)` does not. And so no bound: every
  # type has `==`, and the `Comparable` one left from the `<=>` days
  # refused a list of arrays, of nilable numbers or of any class.
  def includes?(value : Elem) : Bool
  def index(& : (Elem -> Bool)) : Int32 | ::Nil
  def index_by(& : (Elem -> U)) : Hash(U, Elem) forall U : Hashable
  # The separator defaults to nothing, the way `Array#join` (src/iyi/array.iyi)
  # and `Indexable#join` already default it: one name means one thing
  # (SPEC.md III.1.7a), and `slice.join` has to compile
  # where `array.join` does. A mandatory separator made the arity of one name
  # depend on which tower the receiver came from.
  #
  # Into one builder: adding each element to the text so far copied that
  # text once per element, and a `List` of 100,000 took nine seconds.
  def join(separator : String = "") : String
  def map(& : (Elem -> U)) : Array(U) forall U
  def map_with_index(& : (Elem, Int32 -> U)) : Array(U) forall U
  def max : Elem where Elem : Comparable
  def max? : Elem | ::Nil where Elem : Comparable
  # Whether there was an element is the key's to say, not the element's: a
  # nil element can be the largest, and `found.nil?` refused
  # `List.new([nil, 1]).max_by { |x| x ? x : 5 }` as empty where
  # `Array#max_by` and the other library answer nil. `min_by` and
  # `minmax_by` read the key the same way.
  def max_by(& : (Elem -> U)) : Elem forall U : Comparable
  def max_by?(& : (Elem -> U)) : Elem | ::Nil forall U : Comparable
  def max_of(& : (Elem -> U)) : U forall U : Comparable
  def max_of?(& : (Elem -> U)) : U | ::Nil forall U : Comparable
  def min : Elem where Elem : Comparable
  def min? : Elem | ::Nil where Elem : Comparable
  def min_by(& : (Elem -> U)) : Elem forall U : Comparable
  def min_by?(& : (Elem -> U)) : Elem | ::Nil forall U : Comparable
  def min_of(& : (Elem -> U)) : U forall U : Comparable
  def min_of?(& : (Elem -> U)) : U | ::Nil forall U : Comparable
  # Refuses under its own name rather than letting `min`'s refusal surface
  # from a call the reader never wrote: `minmax` of an empty collection is
  # `minmax`'s mistake, not `min`'s.
  def minmax : Tuple(Elem, Elem) where Elem : Comparable
  # One walk, as `minmax_of?` below takes: `min?` and then `max?` walked the
  # collection twice, so a source that can be read once (a pipe, a
  # generator) answered {1, nil} for [3, 1, 4, 1, 5] and `minmax` refused
  # it as empty.
  def minmax? : Tuple(Elem | ::Nil, Elem | ::Nil) where Elem : Comparable
  def minmax_by(& : (Elem -> U)) : Tuple(Elem, Elem) forall U : Comparable
  def minmax_by?(& : (Elem -> U)) : Tuple(Elem | ::Nil, Elem | ::Nil) forall U : Comparable
  def minmax_of(& : (Elem -> U)) : Tuple(U, U) forall U : Comparable
  def minmax_of?(& : (Elem -> U)) : Tuple(U | ::Nil, U | ::Nil) forall U : Comparable
  def none?(& : (Elem -> Bool)) : Bool
  def one?(& : (Elem -> Bool)) : Bool
  def partition(& : (Elem -> Bool)) : Array(Array(Elem))
  def present? : Bool
  def product : Elem where Elem : Num
  def reduce(initial : U, & : (U, Elem -> U)) : U forall U
  # A flag for "an element was seen", not a nil accumulator: a nil element
  # reset the fold, so `[nil, 2]` answered 2 without calling the block.
  def reduce?(& : (Elem, Elem -> Elem)) : Elem | ::Nil
  def reject(& : (Elem -> Bool)) : Array(Elem)
  def select(& : (Elem -> Bool)) : Array(Elem)
  def size : Int32
  def skip(n : Int32) : Array(Elem)
  def skip_while(& : (Elem -> Bool)) : Array(Elem)
  def sorted : Array(Elem) where Elem : Comparable
  def sum : Elem where Elem : Num
  def sum_by(& : (Elem -> U)) : U forall U : Num
  def take(n : Int32) : Array(Elem)
  def take_while(& : (Elem -> Bool)) : Array(Elem)
  def tally : Hash(Elem, Int32) where Elem : Hashable
  def tally_by(& : (Elem -> U)) : Hash(U, Int32) forall U : Hashable
  def to_a : Array(Elem)
  def to_h(& : (Elem -> Tuple(K, V))) : Hash(K, V) forall K : Hashable, V
  def zip(other : O) : Array(Tuple(Elem, O::Elem)) forall O : Enumerable
end

Enumerable for Array(T) forall Timpl

std/enumerableiyi doc src/std/enumerable.iyi
impl Enumerable for Array(T) forall T
end

Enumerable for Set(T) forall Timpl

std/enumerableiyi doc src/std/enumerable.iyi
impl Enumerable for Set(T) forall T
end