Skip to main content

Functions

Shockingly, functions are one of the core elements of the functional programming paradigm. Accordingly, Ribs provides some tools to work with functions themselves.

Aliases

Ribs uses a set of type aliases to make reading function signatures (subjectively) easier. If you've spent any time working with Scala, these aliases should look familiar:

Function-N Aliases
typedef Function1<A, B> = B Function(A);
typedef Function2<A, B, C> = C Function(A, B);

// These 2 function signatures are identical
int dartFun(String Function(double) f) => throw UnimplementedError();
int ribsFun(Function1<double, String> f) => throw UnimplementedError();

The 1 in Function1 indicates that the function takes one parameter. Predictably, the 2 in Function2 indicates the function takes two parameters. Aliases exist up to 22. One benefit of these aliases is that the type naturally reads left to right so we can quickly see that a Function4<String, double, int, List<Foo>> takes a String, double and int and will return a value of type List<Foo>.

info

You don't have to use these aliases in your own code but they're worth familiarizing yourself with since they're used throughout the Ribs API.

Composition

Functions are one of the smallest building blocks of our programs. To create useful programs though, we'll need to use many functions together. How do they fit together though? That's a pretty general question but Ribs does provide a few ways to help you along the way as you gradually combine your small functions into something larger.

andThen

It's often the case you'll want to feed the output of one function into another:

Simple andThen Example
int addOne(int x) => x + 1;
int doubleIt(int x) => x * 2;

final addOneThenDouble = addOne.andThen(doubleIt);

final a = addOneThenDouble(0); // (0 + 1) * 2 == 2
final b = addOneThenDouble(2); // (2 + 1) * 2 == 6

While this example is a bit trivial, you'll likely come across instances on your FP journey where chaining two function into a single value that you can then pass around will result in a cleaner and more composable solution.

compose

Using the same function definitions from above we can compose two functions:

Simple compose Example
final doubleItThenAddOne = addOne.compose(doubleIt);

final c = doubleItThenAddOne(0); // (0 * 2) + 1 == 1
final d = doubleItThenAddOne(2); // (2 * 2) + 1 == 5

Examining the behavior of andThen and compose leads to the conclusion that f.compose(g) is the same as g.andThen(f).

Currying

Ribs also provides functions to curry and uncurry functions. Currying is the process of taking a function f that takes N parameters and turning it into a function that takes one parameter and returns a partially applied version of f that takes N-1 parameters. Check out this example:

Currying
// Converts a function from:
// (A, B) => C
// to:
// A => B => C
Function1<A, Function1<B, C>> curryFn<A, B, C>(Function2<A, B, C> f) =>
throw UnimplementedError('???');

It's worth the time to work through implementing this function. But if you're just looking for a quick answer:

Curry Implementation
Function1<A, Function1<B, C>> curryFnImpl<A, B, C>(Function2<A, B, C> f) =>
(a) => (b) => f(a, b);

Now that we know what currying is, we can use Ribs provided curried function for FunctionN (where 0 < N < 23) like so:

Currying with Ribs
int add2(int a, int b) => a + b;

// Ribs also provides type aliases for curried functions that take the form
// of FunctionNC, where the 'C' denotes the function is curried.
final Function2C<int, int, int> add2Curried = add2.curried;

This naturally begs the question: "Can you uncurry a function?". And the answer is most definitely yes! Ribs provides this ability out of the box:

Uncurrying with Ribs
int add3(int a, int b, int c) => a + b + c;

final Function3C<int, int, int, int> add3Curried = add3.curried;
final Function3<int, int, int, int> add3Uncurried = add3Curried.uncurried;

Tupled

One last, but very useful function that Ribs provides is the ability to convert functions from accepts a set of individual arguments to one that accepts a tuple of the same argument types:

Tupled Function
int fun(int a, String b, bool c) => throw UnimplementedError();

Function1<(int, String, bool), int> funTupled = fun.tupled;

final result = funTupled((2, 'Hello!', false));

You may be asking why this would ever be useful but it becomes more apparent as you start working with generic endcoders/decoders, data classes (which Dart doesn't currently support) and tuple destructuring among other things. It's always good to stick this one in your back pocket to pull out when the situation arises.