kex docs Standard Library 0.4.0-alpha kex.run ↗

Algebraprelude

type Ordering

Algebraic structures, ordering, and combining values associatively.

Kex traits do not inherit from one another, so concrete types explicitly implement every structure whose laws they satisfy.

The two most important things that you will meet in everyday code. Ordering is what a comparison answers, and it composes. This is how a multi-key sort is written without nested ifs:

a.age.compare(b.age).thenBy { a.score.compare(b.score) }

Monoid is "these two values combine, and there is a neutral one". Numbers, strings and lists all satisfy it, which is what lets repeat be written once:

"ab".repeat(3)   # => "ababab"
[1].repeat(2)    # => [1, 1]
5.repeat(3)      # => 15

The result of a comparison: Less, Equal or Greater.

Declared here rather than only inside the interpreter so that Ordering, Less, Equal and Greater reach the semantic layer the same way every other stdlib type does (through the collected interfaces) instead of existing solely as native environment bindings the type checker and name resolver cannot see.

Variants

  • Less
  • Equal
  • Greater

trait Comparable

Types that have a total order.

Implemented by Number, which covers both Integer and Float.

compare

Compares this value with other and answers Less, Equal or Greater.

== stays independent of this: a type may be equatable without being ordered.

A total order: compare answers Less, Equal or Greater. == stays independent: a type may be Equatable without being ordered.

compare : This -> Ordering
Parameters
other This
the value to compare against

Returns: Ordering — how this value orders against other

Examples
1.compare(2)     # => Less
2.compare(2)     # => Equal
3.compare(2)     # => Greater

Sorting with an explicit comparison

people.sort { |a, b| a.age.compare(b.age) == Less }

make Number implements Comparable

Number carries the implementation, so Integer and Float both inherit it rather than repeating the same three comparisons. Mixed receivers work because < and > promote across the two (1.compare(1.0) is Equal).

compare

Compares two numbers, across the Integer/Float boundary.

Mixed receivers work because < and > promote across the two, so 1.compare(1.0) is Equal.

compare(other)
Parameters
other Number
the number to compare against

Returns: Ordering — how this number orders against other

Examples
1.compare(2)      # => Less
1.compare(1.0)    # => Equal
2.5.compare(2)    # => Greater

trait Monoid

Types whose values combine associatively and have a neutral element.

Implemented by Integer (addition), String and List (concatenation), Map and both Set flavours (union), and Ordering ("first decision wins").

identity

The neutral element: combining it with any value gives that value back.

combine must be associative and identity neutral on both sides.

identity : This

Returns: This — the identity

Examples
Integer.identity   # => 0
String.identity    # => ""

combine

Combines this value with other.

Must be associative: a.combine(b).combine(c) and a.combine(b.combine(c)) have to agree.

combine : This -> This
Parameters
other This
the value to combine with

Returns: This — the combined value

Examples
5.combine(3)          # => 8
"ab".combine("cd")    # => "abcd"
[1].combine([2])      # => [1, 2]

Folding a list of values into one

parts.reduce(String.identity) { |acc, s| acc.combine(s) }

repeat

Combines this value with itself n times.

Repeating zero times gives the identity: "" for a string, 0 for an integer, [] for a list. A negative count is invalid and ends the program.

repeat(0)
Parameters
n Integer
how many copies to combine; must not be negative

Returns: This — the repeated value

Examples
"ab".repeat(3)   # => "ababab"
[1].repeat(2)    # => [1, 1]
5.repeat(3)      # => 15
"x".repeat(0)    # => ""

Drawing a separator line

IO.printLine("-".repeat(40))

trait Group

A Monoid in which every value has an inverse that combines with it to give the identity.

Implemented by Integer, where the inverse is negation.

identity

The neutral element.

identity : This

Returns: This — the identity

combine

Combines this value with other.

combine : This -> This
Parameters
other This
the value to combine with

Returns: This — the combined value

inverse

The value that combines with this one to give the identity.

inverse : This

Returns: This — the inverse

Examples
5.inverse             # => -5
5.combine(5.inverse)  # => 0

make Integer implements Monoid, Group

Implements Monoid, Group over Integer for addition.

combine

Adds other to this integer. Addition is the monoid operation for Integer.

combine(other)
Parameters
other This
the integer to add

Returns: This — the sum

Examples
5.combine(3)   # => 8

make String implements Monoid

Implements Monoid over String for concatenation.

combine

Concatenates other onto this string. Concatenation is the monoid operation for String.

combine(other)
Parameters
other This
the string to append

Returns: This — the concatenation

Examples
"ab".combine("cd")   # => "abcd"

make [A] implements Monoid

Implements Monoid over List<A> for concatenation.

combine

Concatenates other onto this list. Concatenation is the monoid operation for List.

combine(other)
Parameters
other This
the list to append

Returns: This — the concatenation

Examples
[1].combine([2, 3])   # => [1, 2, 3]

Flattening a list of lists

groups.reduce([]) { |acc, g| acc.combine(g) }

make Ordering implements Monoid

Ordering is a Monoid under "first decision wins", with Equal as identity. That is what makes multi-key comparison compose instead of nesting ifs:

a.name.compare(b.name).combine(a.age.compare(b.age))

combine evaluates its argument eagerly, so the later comparison runs even when the earlier one already decided. Use thenBy when that matters.

combine

Returns the first decisive ordering: this one if it is not Equal, otherwise other.

This is what makes multi-key comparison compose. Note that other is evaluated eagerly, so the later comparison runs even when the earlier one has already decided: use thenBy when that matters.

combine(@Equal, other)
Parameters
other Ordering
the tie-breaking ordering

Returns: Ordering — the first decisive ordering

Examples
Equal.combine(Less)     # => Less
Less.combine(Greater)   # => Less

Sorting by surname, then by first name

a.last.compare(b.last).combine(a.first.compare(b.first))

reverse

Returns the opposite ordering: Less becomes Greater, Greater becomes Less, and Equal stays Equal.

The one-word way to turn an ascending comparison into a descending one.

reverse : Ordering

Returns: Ordering — the reversed ordering

Examples
Less.reverse      # => Greater
Greater.reverse   # => Less
Equal.reverse     # => Equal

Sorting newest first

a.created.compare(b.created).reverse

thenBy

Returns this ordering if it is decisive, otherwise the result of calling tieBreaker.

The short-circuiting form of combine: the block runs only when this comparison is Equal, so a tie-breaker costs nothing once the order is already decided. Prefer it whenever the tie-breaker is more than a field read.

thenBy : Block<Ordering> -> Ordering
Parameters
tieBreaker Block<Ordering>
evaluated only on a tie

Returns: Ordering — the first decisive ordering

Examples
Equal.thenBy { 2.compare(1) }   # => Greater
Less.thenBy { 2.compare(1) }    # => Less

Sorting by age, then by an expensive score

a.age.compare(b.age).thenBy { score(a).compare(score(b)) }