std/enumerable
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.iyiAll 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
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
endEnumerable for Array(T) forall Timpl
impl Enumerable for Array(T) forall T
endEnumerable for Set(T) forall Timpl
impl Enumerable for Set(T) forall T
end
Enumerable— SPEC.md II.6.