Yogesh Parwani

Building awesome mobile experiences, one pixel at a time.

Yogesh Parwani

Building awesome mobile experiences, one pixel at a time.

Yogesh Parwani

Building awesome mobile experiences, one pixel at a time.

Apr 5, 2022

6 min read

Composition in Flutter & Dart [Functional Programming — Part 3]

Previously, we covered Closures and Currying in FP. Today, we will look at composition and how it helps create modular applications.

What is Composition?

Let’s see the English definition of composition. According to dictionary.com: Composition is the act of combining parts or elements to form a whole. Okay, that’s easy to digest. In a nutshell, it is like legos. We have lego bricks that are combined to make a structure.

In FP, we define small general-purpose functions that can be combined to make complex functions. The output of one function becomes the input of another function, and so on. The input gets passed from function to function and finally returns the result. Hence, we can think of composition as a pipeline through which the data flows.

The mathematical notation of composition is f.g, which translates to f(g(x)) in programming. It flows inside out.

  1. First, x gets evaluated.

  2. x is passed on to g as input, and g(x) is evaluated.

  3. Lastly, g(x) is evaluated, and the result is passed to f, and finally, f(g(x)) is evaluated.

In Dart, the compose function translates to

Function compose(Function f, Function g) => (x) => f(g(x));

Compose is a Higher-order function. It takes two functions and returns a function that takes the input. The order of execution for compositions is right to left, so the function g is executed first and then function f.

Example
import 'package:dartz/dartz.dart';

void main(List<String> args) {
  final _shout = compose(exclaim, toUpper);
  print(_shout('Ouch! that hurts')); // "OUCH! THAT HURTS!"

  // Dartz
  final _shout2 = compose<String, String, String>(exclaim, toUpper);
  print(_shout2('Ouch! that hurts')); // "OUCH! THAT HURTS!"
}

String toUpper(String value) => value.toUpperCase();
String exclaim(String value) => '$value!';

Function compose(Function f, Function g) => (x) => f(g(x));

We create a function shout, a composition of two smaller utility functions, toUpper and exclaim. On line 4, we compose these two functions to create one shout function. Line 8 uses the composeFfunction provided by the dartz package.

How does Flutter use composition?

The Flutter framework is one of the best examples that demonstrate the power of composition. For UI, we combine widgets. For example, do you want to give some padding? Combine the widget with a Padding widget. Need some decoration? Combine the widget with a DecoratedBox widget, etc.

Flutter heavily uses composition. The Widget tree which we deal with to design our UIs follows composition. Widgets are like Lego bricks. They are small general-purpose widgets that we combine to make complex Widgets/UI. For instance, Container comprises several widgets like Padding, DecoratedBox, Align, LimitedBox etc.

Compose vs Pipe

Similar to compose, we also have a pipe utility. The only difference is that compose performs a right to left execution, and a pipe performs a left to right execution order.

void main(List<String> args) {
  final _compose = compose(doubler, increment);
  final _pipe = pipe(doubler, increment);

  print(_compose(10)); // 22
  print(_pipe(10)); // 21
}

int increment(int value) => value + 1;

int doubler(int value) => value * 2;

/// Order of compositon is from right to left.
Function compose(Function f, Function g) => (x) => f(g(x));

/// Order of compositon is from left to right.
Function pipe(Function f, Function g) => (x) => g(f(x));

On line 14, we have compose, and as it has a right to left order, the function g is executed first, and the result is passed on to function f.

On line 17, we have pipe. Here, the function f is executed first, and the result is passed on to the function g, hence being left to right.

In the compose scenario, the input 10 first increments to 11 through increment function and later is doubled so returns 22 as a result.

In the pipe scenario, the input 10 is first doubled using the doubler function and then becomes 20 and then increments to 21 and is returned.

Example

Let’s combine the concepts we have learned and build several functions that convert strings from one case to another.

const _camelCase = 'loremIpsumDolorSitAmet';
const _pascalCase = 'LoremIpsumDolorSitAmet';
const _snakeCase = 'lorem_ipsum_dolor_sit_amet';
const _kebabCase = 'lorem-ipsum-dolor-sit-amet';

We need the functions that can convert the above cases into one another. As in legos, we need the lego bricks first, which in this case are going to be some utility functions.

/// Capitalizes all the elements of a list except the first one.
List<String> capitalizeTail(List<String> values) => [
      toLowerCase(head(values)),
      ...capitalizeWords(tail(values)),
];

/// Converts a list of words to a list of words with the first letter capitalized.
List<String> capitalizeWords(List<String> words) =>
    words.map(capitalizeWord).toList();

/// Converts a list of words to lower case.
List<String> lowerCaseWords(List<String> values) =>
    values.map(toLowerCase).toList();

/// Returns the first element of a list.
String head(List<String> values) => values.first;

/// Returns all the elements of a list except the first one.
List<String> tail(List<String> values) => values.sublist(1).toList();

/// Converts a string to lower case.
String toLowerCase(String value) => value.toLowerCase();

/// Converts a string to upper case.
String toUpperCase(String value) => value.toUpperCase();

/// Capitalizes the first letter of a string.
String capitalizeWord(String value) =>
    toUpperCase(value[0]) + value.substring(1);

String joinWithoutSpace(List<String> values) => values.join();
String joinWithHyphen(List<String> values) => values.join('-');
String joinWithUnderScore(List<String> values) => values.join('_');

List<String> splitWithUnderScore(String value) => value.split('_');
List<String> splitWithHyphen(String value) => value.split('-');

The compose function we have defined earlier only had the support to take two functions as arguments. Now we will define a function that can take n number of arguments.

Function composeN(List<Function> fns) {
  final _reversed = fns.reversed.toList();

  return (x) {
    for (Function fn in _reversed) {
      x = fn(x);
    }
    return x;
  };
}

Now that we have the build blocks let’s combine them to create more meaningful functions. These are some of the functions used. Head over to the Github repo to find more.

Snake case to Pascal case

We’ll start by converting the snake case to camel, pascal, and kebab cases.

void main(List<String> args) {
  // snake case to pascal case
  final _snakeToPascal = composeN([
    joinWithoutSpace,
    capitalizeWords,
    splitWithUnderScore,
  ]);

  print(_snakeToPascal(_snakeCase)); // LoremIpsumDolorSitAmet
  print(_snakeToPascal(_snakeCase) == _pascalCase); // true

  // snake case to camel case
  final _snakeToCamel = composeN([
    joinWithoutSpace,
    capitalizeTail,
    splitWithUnderScore,
  ]);

  print(_snakeToCamel(_snakeCase)); // loremIpsumDolorSitAmet
  print(_snakeToCamel(_snakeCase) == _camelCase); // true

  // snake case to kebab case
  final _snakeToKebab = composeN([
    joinWithHyphen,
    splitWithUnderScore,
  ]);

  print(_snakeToKebab(_snakeCase)); // lorem-ipsum-dolor-sit-amet
  print(_snakeToKebab(_snakeCase) == _kebabCase); // true
}

We have defined a _snakeToPascal function. It takes one argument and returns the result. Let’s look at its implementation. _snakeToPascal is a composition of 3 smaller functions: splitWithUnderscore, capitalizeWordsand joinWithoutSpace. Let’s pass “lorem_ipsum_dolor_sit_amet” as an input to the function. As we know, compose has a right to left order, so

  1. The input is first passed to the splitWithUnderscore function, which splits the input into [“lorem”, “ipsum”, “dolor”, “sit”, “amet”].

  2. The returned value of the splitWithUnderscore, an array, is passed on to the 2nd function, i.e., capitalizeWordswhich converts the first letter of every element into uppercase and returns the list. [“Lorem”, “Ipsum”, “Dolor”, “Sit”, “Amet”]

  3. The result from capitalizeWordsis then passed on to the joinWithoutSpace function, which joins the elements together and returns the result. “LoremIpsumDolorSitAmet”

Remember what we said at the start? We are defining a pipeline for our data through composing. This is it; our data flows through these pipelines and returns the result. Once we have spent some time building the general-purpose functions, combining them now to make meaningful functions is a piece of cake.

Snake case to Camel case

On line 15, _snakeToCamel is pretty straightforward. The first and the last functions are the same, splitWithUnderscore and joinWithoutSpace. We change the middle function from capitalizeWords to capitalizeTail, and our function is ready.

The reason to use capitalizeTail is that we don’t need the first word to be capitalized in the case of camelCase. capitalizeTail is the same as capitalizeWords, but it ignores the first word, which matches our use case.

Snake case to Kebab case

Snake case to kebab case is more simple. Here we need to compose two functions (splitWithUnderscore & joinWithHyphen), and our job is done.

Camel case to other cases

void main(List<String> args) {
  // camel case to snake case
  final _camelToSnake = composeN([
    joinWithUnderScore,
    lowerCaseWords,
    splitWords,
  ]);

  print(_camelToSnake(_camelCase)); // lorem_ipsum_dolor_sit_amet
  print(_camelToSnake(_camelCase) == _snakeCase); // true

  // camel case to pascal case
  final _camelToPascal = composeN([
    joinWithoutSpace,
    capitalizeWords,
    splitWords,
  ]);

  print(_camelToPascal(_camelCase)); // LoremIpsumDolorSitAmet
  print(_camelToPascal(_camelCase) == _pascalCase); // true

  // camel case to kebab case
  final _camelToKebab = composeN([
    joinWithHyphen,
    lowerCaseWords,
    splitWords,
  ]);

  print(_camelToKebab(_camelCase)); // lorem-ipsum-dolor-sit-amet
  print(_camelToKebab(_camelCase) == _kebabCase); // true
}

Kebab case to other cases

void main(List<String> args) {
  // kebab case to snake case
  final _kebabToSnake = composeN([
    joinWithUnderScore,
    lowerCaseWords,
    splitWithHyphen,
  ]);

  print(_kebabToSnake(_kebabCase)); // lorem_ipsum_dolor_sit_amet
  print(_kebabToSnake(_kebabCase) == _snakeCase); // true

  // kebab case to camel case
  final _kebabToCamel = composeN([
    joinWithoutSpace,
    capitalizeTail,
    splitWithHyphen,
  ]);

  print(_kebabToCamel(_kebabCase)); // loremIpsumDolorSitAmet
  print(_kebabToCamel(_kebabCase) == _camelCase); // true

  // kebab case to pascal case
  final _kebabToPascal = composeN([
    joinWithoutSpace,
    capitalizeWords,
    splitWithHyphen,
  ]);

  print(_kebabToPascal(_kebabCase)); // LoremIpsumDolorSitAmet
  print(_kebabToPascal(_kebabCase) == _pascalCase); // true
}

Pascal case to other cases

void main(List<String> args) {
  // pascal case to snake case
  final _pascalToSnake = composeN([
    joinWithUnderScore,
    lowerCaseWords,
    splitWords,
  ]);

  print(_pascalToSnake(_pascalCase)); // lorem_ipsum_dolor_sit_amet
  print(_pascalToSnake(_pascalCase) == _snakeCase); // true

  // pascal case to camel case
  final _pascalToCamel = composeN([
    joinWithoutSpace,
    capitalizeTail,
    splitWords,
  ]);

  print(_pascalToCamel(_pascalCase)); // loremIpsumDolorSitAmet
  print(_pascalToCamel(_pascalCase) == _camelCase); // true

  // pascal case to kebab case
  final _pascalToKebab = composeN([
    joinWithHyphen,
    lowerCaseWords,
    splitWords,
  ]);

  print(_pascalToKebab(_pascalCase)); // lorem-ipsum-dolor-sit-amet
  print(_pascalToKebab(_pascalCase) == _kebabCase); // true
}

Final Thoughts

I like to think of composition as a divide and conquer technique. The major advantage of composition is that we end up with small pieces that are highly reusable and customizable. The following article will go a step further and cover the equality and immutability of complex data structures in Dart.

Awesome! Pat yourself on the back for reaching till the end. I hope I added some value to the time you invested. Find out more examples on the GitHub repository, and reach out on Twitter or LinkedIn for suggestions/Questions or any topic you’d like me to cover. You can support it by clapping👏, and thanks for reading :) Follow for more😄

Until next time, folks!















LET'S
CONNECT

LET'S
CONNECT

LET'S
CONNECT