I’m very happy to share version 1.0 of a library that @pierrelgol and I collaborated on. Our efforts resulted in a library called Fluent:
As the name suggests, it is an implementation of the fluent interface, allowing you to chain algorithms in succession and accomplish tasks succinctly. Fluent has many algorithms built in and can be used on any numeric-type slice and has special functionality for string manipulation.
Fluent interfaces are lightweight (the size of a slice), comptime deduced, and never allocate. The entire library is one file that can be imported into your project much like std.ArrayList
.
Fluent Interface:
Here’s an example of using the fluent interface to concatenate two strings, trim spaces from both sides, and then capitalize the first letter of each word:
const str_a: []const u8 = " hello,";
const str_b: []const u8 = " world! ";
var buf: [64]u8 = undefined;
const result = Fluent.init(str_a) // initialize our interface on str_a
.concat(str_b, buf[0..]) // concatenate str_b into buffer
.trim(.periphery, .scalar, ' ') // trim spaces on both sides
.title(); // python title function
// based on std.math.Order, could also be result.order(...) == .eq
try std.expect(result.equal("Hello, World!"));
Here we take a sum of squares and then take the square root:
const magnitude = std.math.sqrt(
Fluent.init(floats[0..]).mapReduce(f32, sqr, add, 0.0)
);
I also like fluent for one-off functions - sorting is easy:
_ = Fluent.init(slice).sort(.ascending);
Ccompute sigmoid function:
const x = Fluent.init(buf[0..])
.copy(&[_]f32{ -2, -1, 0, 1, 2 })
.map(.{
Fluent.negate,
std.math.exp,
Fluent.bind(.{ 1.0 }, Fluent.add),
Fluent.bind(.{ 1.0 }, Fluent.div),
});
Iterators:
Fluent also comes equipped with a composable iterator backend. Iterators can be used in conjunction with fluent interfaces or independently as a stand-alone utility. Here’s a few examples…
Copy the reverse of a list using a reverse iterator:
const count = Fluent.iterator(.reverse, items_a[0..]).write(items_b[0..]);
Sum up the square of all elements that are even:
const rdx = Fluent
.iterator(.forward, items[0..])
.filter(isEven)
.map(square)
.reduce(i32, Fluent.add, 0);
Set stride to increment by two 2 and produce windows of size 4:
var itr = Fluent
.iterator(.forward, items[0..])
.strided(2)
while (itr.window(4)) |window| { // ...
Fuse multiple unary functions and filters:
var itr = Fluent
.iterator(.forward, items[0..])
.map(.{
Fluent.negate,
std.math.exp,
}).filter(.{
skipNans,
skipInfs,
});
while (itr.next()) |value| { //...
The backends of Fluent are split into Mutable
and Immutable
variants. The mutable variants pick up all of the functions that the immutable ones have (thus mutable strings can do everything that const strings can such as search, compare, etc).
If a function is marked as immutable, the function cannot modify its current slice. It can, however, return a modified slice that contains the result of the operation. Trim, for instance, doesn’t remove anything, but instead returns a new fluent interface that holds a slice with adjusted boundaries. Concat and join are also considered immutable as they do not modify the original slice, but instead write their results to a provided buffer and return a fluent interface attached to that buffer.
Here’s a list of the algorithms we currently have:
Immutable:
all - check if all elements of the acquired slice are true by given predicate
concat - appends the aquired slice to a given slice into a given buffer
contains - check if contains a given scalar, sequence, or any
containsFrom - check if contains a given scalar, sequence, or any after a given index
count - counts all, leading, trailing, until, inside, inverse of scalar, sequence, any
endsWith - checks if the acquired slice ends with a scalar, sequence, or any
equal - returns true if lexicogrpahical order is equal to a given slice
find - returns first index of scalar, slice, or any
findFrom - returns first index after a given position of scalar, slice, or any
getAt - returns an element for given positive or negative index
join - appends the aquired slice to a given range of slices into a given buffer
mapReduce - applies unary function and reduces on intial value and binary function
max - returns an optional maximum value from the acquired slice
min - returns an optional minimum value from the acquired slice
none - check if no elements of the acquired slice are true by given predicate
product - returns the product of all elements or zero if slice is empty
print - prints the acquired slice based on a given format string
order - returns the lexicographical order compared to a given slice
reduce - returns a reduction based on an intial value and binary function
slice - chainable slicing operation for acquired slice
startsWith - checks if the acquired slice starts with a scalar, sequence, or any
sample - randomly samples a range from the acquired slice given a size
sum - returns the sum of all elements or zero if slice is empty
trim - trims left, right, periphery of scalar, sequence, any
write - writes the acquired slice to a given buffer
Mutable:
copy - copy a given slice into the acquired slice
fill - fills the acquired slice with a scalar value
map - transforms every elment in the acquired slice with a given unary function
partition - partiions the acquired slice based on predicate in stable or unstable manner
replace - replaces slice, sequence, or any at left, right, periphery or all
reverse - reverses the acquired slice
rotate - rotates the array by both negative and positive amounts
setAt - sets a given position with a provided value using index wrapping
shuffle - randomly shuffles the acquired slice
sort - sorts the range in ascending or descending order
And for strings:
Immutable:
digit - returns optional integer parsed from string
differenceWith - returns set diference between acquired slice and given slice
float - returns optional floating-point number parsed from string
getToken - extract a token given a set of delimiters
intersectWith - returns set intersection between acquired slice and given slice
isDigit - check if string only contains digits
isAlpha - check if string only contains alphabetic characters
isSpaces - check if string only contains whitespace
isLower - check if string only contains alphabetic lower case
isUpper - check if string only contains alphabetic upper case
isHex - check if string only contains hexidecimal characters
isASCII - check if string only contains ASCII characters
isPrintable - check if string only contains printable characters
isAlnum - check if string only contains alpha numeric characters
unionWith - returns set union between acquired slice and given slice
Mutable:
lower - transform all alphabetic characters to lower case
upper - transform all alphabetic characters to upper case
capitalize - transform first character to upper case and rest to lower case
title - capitalize each sequence separated by spaces
We also support the following iterators (from the standard library):
split - splits a sequence on a given delimiter
tokenize - tokenizes a sequence on a given delimiter
The current plan for Fluent is to gather feedback, optimize and upgrade algorithms, integrate more SIMD over time and add functionality.
All in all, I had a great time putting this together with @pierrelgol and I think we made a neat little swiss-army knife that you might enjoy as well. Thanks everyone