DEV Community

Cover image for Functional Programming in Dart with pure
Mathieu Kerjouan
Mathieu Kerjouan

Posted on

Functional Programming in Dart with pure

Writing only functional oriented code can become addictive and when switching to something different, like OOP, it's kinda hard to forgot about all these marvelous techniques previously learned. Luckily, many packages can be found to fix this issue on Dart, the oldest one is called dartz and was created by Björn Sperber. Bad luck though, the project repository seems to be unmaintained. If you are still curious, one interesting talk (Pure functional programming in Dart by Björn Sperber) can be found on youtube.

Few other projects exist into the wild to implement functional programming in Dart. Here a quick summary list:

  • dartz, the latest version is 0.10.1, but the project is inactive. This was one of the first Dart module implementing functional programming concept, perhaps also the first one to be presented during a conference;

  • pure, the latestversion at this time of writing is 1.0.0. The project is active and quite minimalist;

  • fpdart, the latest version is 1.2.0, and the project seems to be active;

  • dfunc, the latest version is 0.10.0 but the project seems to be stopped.

The choice of a package is limited, only pure and fpdart seem to be active. I'm a bit sad, I wanted to test dartz but if it's a dead project, it's better to move to something else. In this article, we will focus on the pure package.

Bootstrapping

pure project includes API documentation and few examples. The source code is also available on Github.

$ dart create tpure
Creating tpure using template console...

  .gitignore
  analysis_options.yaml
  CHANGELOG.md
  pubspec.yaml
  README.md
  bin/tpure.dart
  lib/tpure.dart
  test/tpure_test.dart

Running pub get...                     0.3s
  Resolving dependencies...
  Downloading packages...
  Changed 48 dependencies!
  1 package has newer versions incompatible with dependency constraints.
  Try `dart pub outdated` for more information.

Created project tpure in tpure! In order to get started, run the following commands:

  cd tpure
  dart run

$ cd tpure

$ dart pub add pure
Resolving dependencies... 
Downloading packages... 
  package_config 2.2.0 (3.0.0 available)
+ pure 1.0.0
Changed 1 dependency!
1 package has newer versions incompatible with dependency constraints.
Try `dart pub outdated` for more inf
Enter fullscreen mode Exit fullscreen mode

pure is using Dart extension types feature to create wrappers around types.

import 'package:pure/pure.dart';
Enter fullscreen mode Exit fullscreen mode

Constant

Takes a single argument, discards it and return the value that it was called on.

Based on the documentation, this feature returns a constant value instead of any other arguments. The example presented show the result of this feature when applied on a map method.

void main(List<String> arguments) {
  constant();
}

void constant() {
  print("Constant (map):");
  const numbers = [1,2,3];
  final result = numbers.map(0.constant);
  print(result);

  print("Constant (reduce):");
  final result2 = numbers.reduce(0.constant);
  print(result2);

  print("Constant (fold):");
  final result3 = numbers.fold(0, 0.constant);
  print(result3);
}
Enter fullscreen mode Exit fullscreen mode

Let run this code.

$ dart run
Building package executable... 
Built tpure:tpure.
Constant (map):
(0, 0, 0)
Constant (reduce):
0
Constant (fold):
0
Enter fullscreen mode Exit fullscreen mode

This is syntax sugar around function definition, the previous code is equivalent to:

void constant() {
  print("Constant (map):");
  const numbers = [1,2,3];
  final result = numbers.map((_) => 0);
  print(result);

  print("Constant (reduce):");
  final result2 = numbers.reduce((_, _) => 0);
  print(result2);

  print("Constant (fold):");
  final result3 = numbers.fold(0, (_,_) => 0);
  print(result3);
}
Enter fullscreen mode Exit fullscreen mode

How to use this feature? At this time, I don't really have an idea. I think it can be useful when we return always some constants in an iterable... Or to make less confusing closure notation. The implementation can be seen in lib/src/constant/extensions.dart

Composition

A composition permits to execute functions in pipeline, it uses ComposeX (defined in lib/src/composition/dot.dart)and PipeX (defined in lib/src/composition/pipe.dart) types.

To use this feature, we first need to create a new function. The example provided show how to convert an int to a double then a double to a String. Let convert an integer to hexadecimal String.

void composition() {
  print("Composition:");
  final f = (int x) {
    return x.toDouble() + 0.1;
  };

  final g = (double x) {
    return "--" + x.toString() + "--" ;
  };

  final value = 12.pipe(f).pipe(g);
  print(value);
}
Enter fullscreen mode Exit fullscreen mode
$ dart run
Building package executable... 
Built tpure:tpure.
Composition:
--12.1--
Enter fullscreen mode Exit fullscreen mode

Composition is an extremely powerful mechanism to deal with multi-functions. pure is adding the method pipe() to all types permitting to use it. The previous example convert an Int to a Double and finally to a String. The computation is clear and each functions can be tested one by one.

Currying

Currying is another interesting functional programming mechanism to convert one function with many arguments to a pipeline of functions with one argument. It can be mixed with composition to give more flexibility on complex functions.

void curry() {
  print("Curry:");
  final f = (int x, int y, int z) {
    return (x, y, z);
  };

  print(f(1,2,3));
  print(f.curry(1));
  print(f.curry(1)(2));
  print(f.curry(1)(2)(3));
}
Enter fullscreen mode Exit fullscreen mode
$ dart run
Building package executable... 
Built tpure:tpure.
Curry:
(1, 2, 3)
Closure: (int) => (int) => (int, int, int)
Closure: (int) => (int, int, int)
(1, 2, 3)
Enter fullscreen mode Exit fullscreen mode

Flipping

This feature can be useful when dealing with external functions breaking your convention. It can be seen on Erlang/Elixir for example, most of the functions on Elixir are passing the state first to modify it with the pipeline operator (|>), Erlang is usually doing the opposite. In Dart, reversing the arguments positions can simply done with the flip() method offered by pure. The example from the documentation is way enough to understand this concept.

void flipping() {
  print("flipping:");
  final person = (String firstname, String lastname) {
    print("$firstname $lastname");
  };
  person("John", "Smith");
  person.flip("Smith", "John");
}
Enter fullscreen mode Exit fullscreen mode
$ dart run
John Smith
John Smith
Enter fullscreen mode Exit fullscreen mode

The final result is the same, but the arguments were flipped on the second call. Interesting feature to make a code more flexible without modifying the convention you enforced. Not sure it is used a lot though.

Nullable

Dealing with null can be challenging in Dart, and most of the function one will create will avoid using null as arguments, but sometimes, it's impossible to avoid. Then, instead of rewriting a part of the function to support null parameters, nullable can be used instead.

void nullable() {
  final concat = (String firstname, String pseudo, String lastname) {
    return [firstname, pseudo, lastname].join(" ");
  };
  print(concat("John", "Johny", "Smith"));
  print(concat.nullable("test", null, null));
}
Enter fullscreen mode Exit fullscreen mode
$ dart run
John Johny Smith
null
Enter fullscreen mode Exit fullscreen mode

Recursion

Creating recursive functions in Dart can overflow the stack. Indeed, Dart does not support tail recursive call by default, and a trampoline function must be created to deal with that. Fortunately, pure is offering an implementation for this specific use case. To be honest, I don't really like the method used by pure to create the trampoline. The syntax is a bit odd, but it's perhaps I'm coming from functional programming. Here a really simple recursive function doing nothing.

int rec1(int i) {
  if (i<=0) return -1;
  if (i%1024==0) print(i);
  return rec1(i+1);
}

Tram<int> rec2(int i) {
  if (i<=0) return Tram.done(-1);
  if (i%(1024*1024)==0) print(i);
  return Tram.call(() => rec2(i+1));
}

void trampoline() async {
  print("trampoline:");
  await Isolate.run(() {
    try {
      rec1(1);
    }
    catch(e) {
      print(e);
      return -1;
    }
  });

  rec2.bounce(1);
}

void main(List<String> arguments) {
  trampoline();
}
Enter fullscreen mode Exit fullscreen mode
$ dart run
trampoline:
1024
2048
3072
4096
5120
6144
7168
8192
9216
10240
11264
12288
13312
14336
15360
16384
17408
18432
19456
20480
21504
Stack Overflow
1048576
2097152
3145728
4194304
5242880
6291456
7340032
8388608
9437184
...
Enter fullscreen mode Exit fullscreen mode

rec1() function will overflow the stack a bit after the 20k calls, and then throw a Stack Overflow exception. To avoid crashing the main thread, rec1() is started inside an Isolate. This is a proof Dart is not compatible with recursive call, or at least, very long recursive calls.

On the other hand, rec2() is using the trampoline feature offered by pure. When executed, the function will continue until the variable int will overflow (by default, Dart is using 64 bits integers, so, it will take a while). The Tram.call() method is used to create the trampoline, then, to use the trampoline, the bounce() method is called from the rec2 reference.

I'm not sure if it's the most elegant way to do that in Dart, but it works. Comparing the performance of this kind of call with a simple iteration can also be nice.

Memoization

Memoization is a technique to store the result from pure functions. In short, when a pure function is called one time, the returned values is cached. When the same pure function is called another time with the same arguments, because of the purity, the return value will be exactly the same. Instead of re-doing the computation, the value from the cache is returned. This is an optimization mechanism found in many functional programming language, like in Haskell.

void memoization() {
  print("memoization:");
  final pure = () {
    return (x, y) {
      sleep(Duration(seconds: 3));
      return x + y;
    }.memoize();
  };
  final p = pure();
  print("datetime: ${DateTime.now()}");
  print(p(1,2));
  print("datetime: ${DateTime.now()}");
  print(p(1,2));
  print("datetime: ${DateTime.now()}");
  print(p(3,3));
  print("datetime: ${DateTime.now()}");
  print(p(3,3));
  print("datetime: ${DateTime.now()}");
}
Enter fullscreen mode Exit fullscreen mode
$ dart run
memoization:
datetime: 2026-08-24 11:07:36.620523
3
datetime: 2026-08-24 11:07:39.656219
3
datetime: 2026-08-24 11:07:39.656613
6
datetime: 2026-08-24 11:07:42.684693
6
datetime: 2026-08-24 11:07:42.684823
Enter fullscreen mode Exit fullscreen mode

As you can see, the first value is taking 3 seconds to be returned (due to the call to sleep() function), but the next time the function is called with the same arguments, the result is given instantaneously. This is because the returned values is cached and instead of executing the function twice, the value is simply returned from the cache.

This work very well for pure functions, but when it comes to impure functions, this is another store. A pure function is reproducible, but an impure function can have different output even if the same inputs are passed. In this case, one can have an issue with memoization cache. In short: use it only with pure function.

Thunk

A Thunk is a function injecting arguments to another function. I never really used this kind of feature in the past though.

void thunk() {
  print("thunk:");
  final t = (num x, num y) => x + y;
  final s = t.thunk(1, 2);
  print(s());
}
Enter fullscreen mode Exit fullscreen mode
$ dart run
thunk:
3
Enter fullscreen mode Exit fullscreen mode

This feature is straightforward and can be used with repetitive functions call is made. Again, I never used this kind of methods, so, I don't really know where should I use it.

Conclusion

To be quite honest, doing functional programming in Dart does not seem natural. For example, creating a simple recursive fibonacci sequence in Dart is painful, even with pure. This package is offering nice interfaces though, mostly used to glue some of your code with external modules. It's always nice to have this kind of features.

Unfortunately, I don't have a lot of resources to share with you on pure:

Maybe starting my own functional oriented library in the future could solve that? Not sure, but could be a good idea to learn about Dart types.


Cover Image by Sonika Agarwal on Unsplash

Top comments (0)