Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The joy of Functional Programming in Dart - #ft...

The joy of Functional Programming in Dart - #ftcon23

https://fluttercon.dev/csongor-vogel/

Functional programming is a programming paradigm that has gained increasing popularity in recent years thanks to its ability to improve code quality, reduce bugs, and increase productivity. Luckily, Dart has a strong foundation in functional programming concepts such as higher-order functions, closures, and immutability.

In this talk, we will explore how to unlock the power of functional programming in Dart. We will start with an introduction to functional programming concepts and why they matter. And learn more about functional programming design patterns. We will then dive into how Dart supports functional programming and how to apply functional programming techniques to solve real-world problems. We will cover the core principles of functional programming, such as immutability, purity, and higher-order functions.

Finally, we will discuss the benefits of functional programming in Dart, such as improved code readability, easier debugging, error handling, and faster development time. We will also explore how to use Dart's functional programming libraries, such as the fpdart and dartz packages, to implement functional programming techniques in your code. We will also address common challenges developers may face when transitioning to functional programming and provide strategies to overcome them.

Whether you are new to functional programming or an experienced developer looking to improve your Dart skills, this talk will equip you with the knowledge and techniques to unlock the power of functional programming in Dart and take your coding skills to the next level.

gerfalcon

July 07, 2023
Tweet

More Decks by gerfalcon

Other Decks in Programming

Transcript

  1. The joy of Functional Programming in Dart Senior Software Engineer

    at talabat, Delivery Hero Co-organizer of Flutter Abu Dhabi & Dubai gerfalcon GerfalconVogel Csongor Vogel csongorvogel
  2. About me • Senior Software Engineer at talabat • Lecturer

    at Budapest University of Technology and Economics • Co-organizer of Flutter Abu Dhabi & Dubai
  3. Functional programming • It’s a programming paradigm based on mathematical

    functions • Main goals ◦ Purity ◦ Immutability ◦ High-order functions ◦ Declarative programming style f(x)
  4. Area of a circle 𝐴 = 𝜋𝑟! void main() {

    List<double> inputs = [1.0, 2.0, 3.0]; for (int i = 0; i < inputs.length; ++i) { double radius = inputs[i]; double area = areaOfCircle(radius); print(area); } PI = 3.14159; double areaOfCircle(double r) { return PI * r * r; } // Prints 3.14159, 12.56636, 28.27431
  5. Area of a circle 𝐴 = 𝜋𝑟! void main() {

    List<double> inputs = [1.0, 2.0, 3.0]; for (int i = 0; i < inputs.length; ++i) { double radius = inputs[i]; double area = areaOfCircle(radius); print(area); } var PI = 3.14159; double areaOfCircle(double r) { return PI * r * r; }
  6. Area of a circle 𝐴 = 𝜋𝑟! Not pure function

    ❌ // Prints 3.14159, 12.56636, 28.27431 // 3.0, 12.0, 27.0 var PI = 3; double areaOfCircle(double r) { return PI * r * r; } void main() { List<double> inputs = [1.0, 2.0, 3.0]; for (int i = 0; i < inputs.length; ++i) { double radius = inputs[i]; double area = areaOfCircle(radius); print(area); }
  7. Immutability Data cannot be changed once created, reducing the chance

    of unexpected mutations and making code more predictable.
  8. Area of a circle 𝐴 = 𝜋𝑟! var PI =

    3; double areaOfCircle(double r) { return PI * r * r; } void main() { List<double> inputs = [1.0, 2.0, 3.0]; for (int i = 0; i < inputs.length; ++i) { double radius = inputs[i]; double area = areaOfCircle(radius); print(area); }
  9. Area of a circle 𝐴 = 𝜋𝑟! pure function ✅

    const PI = 3.14159; double areaOfCircle(double r) { return PI * r * r; } void main() { List<double> inputs = [1.0, 2.0, 3.0]; for (int i = 0; i < inputs.length; ++i) { final radius = inputs[i]; final area = areaOfCircle(radius); print(area); } // Prints 3.14159, 12.56636, 28.27431
  10. Area of a circle 𝐴 = 𝜋𝑟! void main() {

    List<double> inputs = [-1.0, 1.0, 3.0]; for (int i = 0; i < inputs.length; ++i) { final radius = inputs[i]; final area = areaOfCircle(radius); print(area); } const PI = 3.14159; double areaOfCircle(double r) { if (r < 0) { throw ArgumentError(”error msg"); } return PI * r * r; } Avoid side-effects ❌
  11. Area of a circle 𝐴 = 𝜋𝑟! void main() {

    List<double> inputs = [-1.0, 1.0, 3.0]; for (int i = 0; i < inputs.length; ++i) { final radius = inputs[i]; final area = areaOfCircle(radius); print(area); } const PI = 3.14159; double areaOfCircle(double r) { if (r < 0) { throw ArgumentError(”error msg"); } } return PI * r * r;
  12. Area of a circle 𝐴 = 𝜋𝑟! void main() {

    List<double> inputs = [-1.0, 1.0, 3.0]; for (int i = 0; i < inputs.length; ++i) { final radius = inputs[i]; final area = areaOfCircle(radius); print(area); } const PI = 3.14159; double? areaOfCircle(double r) { if (r < 0) { return null; } } return PI * r * r;
  13. Area of a circle 𝐴 = 𝜋𝑟! const PI =

    3.14159; double areaOfCircle(double r) { } return PI * r * r; void main() { List<double> inputs = [-1.0, 1.0, 3.0]; for (int i = 0; i < inputs.length; ++i) { final radius = inputs[i]; final area = areaOfCircle(radius); print(area); }
  14. Area of a circle 𝐴 = 𝜋𝑟! const PI =

    3.14159; double areaOfCircle(double r) { } return PI * r * r; void main() { List<double> inputs = [-1.0, 1.0, 3.0]; for (int i = 0; i < inputs.length; ++i) { final radius = inputs[i]; if (radius > 0) { final area = areaOfCircle(radius); print(area); } }
  15. Imperative style void main() { List<double> inputs = [-1.0, 1.0,

    3.0]; for (int i = 0; i < inputs.length; ++i) { final radius = inputs[i]; if (radius > 0) { final area = areaOfCircle(radius); print(area); } }
  16. Imperative style Declarative style void main() { List<double> inputs =

    [-1.0, 1.0, 3.0]; inputs .where((input) => input > 0) .map((radius) => areaOfCircle(radius)) .forEach((area) => print(area)); } void main() { List<double> inputs = [-1.0, 1.0, 3.0]; for (int i = 0; i < inputs.length; ++i) { final radius = inputs[i]; if (radius > 0) { final area = areaOfCircle(radius); print(area); } }
  17. Declarative style void main() { List<double> inputs = [-1.0, 1.0,

    3.0]; inputs .skip(1) .where((input) => input > 0) .map((radius) => areaOfCircle(radius)) .map((area) => area.toStringAsFixed(2)) .toList() .take(2) .forEach((area) => print(area)); } // Prints 3.14, 28.26
  18. Higher-order function Function can be passed as arguments to another

    function or be returned by another function.
  19. Higher-order function double areaOfCircle(double radius) { return PI * r

    * r; } void main() { List<double> inputs = [-1.0, 1.0, 3.0]; inputs .where((input) => input > 0) .map((radius) => areaOfCircle(radius)) .forEach((area) => print(area)); }
  20. Higher-order function void areaOfCircle(double r, Function printFunction) { double area

    = PI * r * r; printFunction('Area of circle is: $area'); } void main() { List<double> inputs = [-1.0, 1.0, 3.0]; inputs .where((input) => input > 0) .map((radius) => areaOfCircle(radius, print)) .toList(); }
  21. Functional programming • It’s a programming paradigm based on mathematical

    functions • Contrast to Object-Oriented Programming (OOP), ◦ mutable state ◦ imperative programming style • Main goals ◦ Purity ◦ Immutability ◦ High-order functions ◦ Declarative programming style f(x)
  22. Functional programming in Dart • final, const keywords ◦ constants

    /// Declaring variables as final and const void main() { final String name = "John"; print(name); // Prints John //name = "David"; // compile-time error const int age = 29; print(age); // Output: 25 //age = 30; // compile-time error }
  23. Functional programming in Dart • final, const keywords ◦ constants

    ◦ lists void main() { final List<int> numbers = [1, 2, 3]; //numbers = [4, 5, 6]; // compile-time error numbers.add(4); // This is allowed print(numbers); // Output: [1, 2, 3, 4] const List<int> constNumbers = [1, 2, 3]; //constNumbers = [4, 5, 6]; // compile-time error //constNumbers.add(4); // compile-time error print(constNumbers); // Output: [1, 2, 3] }
  24. Functional programming in Dart • final, const keywords ◦ constants

    ◦ lists ◦ classes class IPerson { final String name; final int age; IPerson(this.name, this.age); } void main() { var john = IPerson('John', 25); print(john.name); // Prints: John print(john.age); // Prints: 25 //john.name = 'David'; // compile-time error //john.age = 30; // compile-time error }
  25. Functional programming in Dart • final, const keywords ◦ constants

    ◦ lists ◦ classes class IPerson { final String name; final int age; IPerson(this.name, this.age); IPerson copyWith({String? name, int? age}) { return IPerson( name ?? this.name, age ?? this.age, ); } } void main() { var john = IPerson('John', 25); print(john.name); // Prints: John print(john.age); // Prints: 25 var olderJohn = john.copyWith(age: 26); print(olderJohn.name); // Prints: John print(olderJohn.age); // Prints: 26 }
  26. Functional programming in Dart • final, const keywords • Top-level

    functions void main() { myFunction(); } void myFunction() { /* ... */ }
  27. Functional programming in Dart • final, const keywords • Top-level

    functions • Anonymus functions ◦ Lambda void main() { var greet = (name) => 'Hello, $name!’; print(greet('Dash')); // Hello, Dash! }
  28. Functional programming in Dart • final, const keywords • Top-level

    functions • Anonymus functions ◦ Lambda ◦ Closures void main() { var myCounter = counter(start: 0); print(myCounter()); // Prints 0 print(myCounter()); // Prints 1 } Function counter({required int start}) { return () => start++; }
  29. Functional programming in Dart • final, const keywords • Top-level

    functions • Anonymus functions • Extensions extension ExtensionList<T> on List<T> { // get the last item of the list T get lastItem => this[length - 1]; // get a reversed copy of the list List<T> get reversedCopy => this.reversed.toList(); // repeat the list n times List<T> repeat(int times) => [for(int i = 0; i < times; i++) ...this]; } void main() { var list = [1, 2, 3, 4, 5]; print(list.lastItem); // 5 print(list.reversedCopy); // [5, 4, 3, 2, 1] print(list.repeat(2)); // [1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2] }
  30. Functional programming in Dart • final, const keywords • Top-level

    functions • Anonymus functions • Extensions • Records • Patterns Dart v3 🎉
  31. Functional programming in Dart • final, const keywords • Top-level

    functions • Anonymus functions • Extensions • Records • Patterns Dart v3 🎉 const year = (name: 'Best year ever', date: 2023); final nextYear = (name: year.name, date: year.date + 1);
  32. Functional programming in Dart • final, const keywords • Top-level

    functions • Anonymus functions • Extensions • Records • Patterns Dart v3 🎉 const year = (name: 'Best year ever', 2023); final nextYear = (name: year.name, year.$1 + 1);
  33. (String, String) getName() { var firstName = 'John'; var lastName

    = 'Smith'; return (firstName, lastName); } void main() { final name = getName(); print('${name.$1} ${name.$2}'); // John Smith } Functional programming in Dart • final, const keywords • Top-level functions • Anonymus functions • Extensions • Records ◦ Multiple return values • Patterns Dart v3 🎉
  34. (String, String) getName() { var firstName = 'John'; var lastName

    = 'Smith'; return (firstName, lastName); } void main() { final name = getName(); print('${name.$1} ${name.$2}’); // John Smith // Destructuring final (firstName, lastName) = getName(); print('$lastName $firstName'); // Smith John 🇭🇺 } Functional programming in Dart • final, const keywords • Top-level functions • Anonymus functions • Extensions • Records ◦ Multiple return values ◦ Destructuring • Patterns Dart v3 🎉
  35. Functional programming in Dart • final, const keywords • Top-level

    functions • Anonymus functions • Extensions • Records ◦ Multiple return values ◦ Destructuring • Patterns ◦ Exhaustiveness Dart v3 🎉 String describeBools(bool b1, bool b2) => switch ((b1, b2)) { (true, true) => 'both true', (false, false) => 'both false', (true, false) => 'one of each', (false, true) => 'one of each', };
  36. Functional programming in Dart • final, const keywords • Top-level

    functions • Anonymus functions • Extensions • Records ◦ Multiple return values ◦ Destructuring • Patterns ◦ Exhaustiveness Dart v3 🎉 String describeBools(bool b1, bool b2) => switch ((b1, b2)) { (true, true) => 'both true', (false, false) => 'both false', (true, false) => 'one of each', // (false, true) => 'one of each’, compile-time error };
  37. Functional programming in Dart • final, const keywords • Top-level

    functions • Anonymus functions • Extensions • Records ◦ Multiple return values ◦ Destructuring • Patterns ◦ Exhaustiveness ◦ Algebraic data type Dart v3 🎉 sealed class Shape {} class Square implements Shape { final double length; Square(this.length); } class Circle implements Shape { final double radius; Circle(this.radius); } double calculateArea(Shape shape) => switch (shape) { Square(length: var l) => l * l, Circle(radius: var r) => PI * r * r };
  38. Try-catch • Server • No Internet • Authentication • Parsing

    • Unknown Future<AreaModel> getArea() async { try { final uri = Uri.parse(Constants.getAreaApi); final response = await http.get(uri); switch (response.statusCode) { case 200: final data = json.decode(response.body); return AreaModel.fromMap(data); default: throw Exception(response.reasonPhrase); } } on ParsingException catch (_) { rethrow; } on TypeError catch (_) { rethrow; } on Exception catch (_) { rethrow; } catch (_) { rethrow; } }
  39. Try-catch • Server • No Internet • Authentication • Parsing

    • Unknown Future<AreaModel> getArea() async { /// ...} void main() async { try { final result = await getArea(); } on Exception catch (_) { print(Exception while getting area’); } catch (e) { print('Unexpected error while getting area'); } }
  40. Result sealed class Result<S, E extends Exception> { const Result();

    } final class Success<S, E extends Exception> extends Result<S, E> { const Success(this.value); final S value; } final class Failure<S, E extends Exception> extends Result<S, E> { const Failure(this.exception); final E exception; }
  41. Result Future<AreaModel> getArea() async { try { final uri =

    Uri.parse(Constants.getAreaApi); final response = await http.get(uri); switch (response.statusCode) { case 200: final data = json.decode(response.body); return AreaModel.fromMap(data); default: throw Exception(response.reasonPhrase); } } on ParsingException catch (_) { rethrow; } on TypeError catch (_) { rethrow; } on Exception catch (_) { rethrow; } catch (_) { rethrow; } }
  42. Result Future<Result<AreaModel, Exception>> getArea() async { try { final uri

    = Uri.parse(Constants.getAreaApi); final response = await http.get(uri); switch (response.statusCode) { case 200: final data = json.decode(response.body); return Success(AreaModel.fromMap(data)); default: throw Exception(response.reasonPhrase); } } on Exception catch (e) { return Failure(e); } on TypeError catch (error) { return Failure(Exception(error.toString())); } catch (error) { return Failure(Exception(error.toString())); } }
  43. Try-catch void main() async { try { final result =

    await getArea(); print(result); } on Exception catch (_) { print(Exception while getting area’); } catch (e) { print('Unexpected error while getting area'); } } Future<AreaModel> getArea() async {}
  44. Result void main() async { final Result<AreaModel, Exception> result =

    await getArea(); final String value = switch (result) { Success(value: final area) => area.toString(), Failure(exception: final exception) => exception.toString(), }; } Future<Result<AreaModel, Exception>> getArea() async {}
  45. Results 😱 Future<Result<int, Exception>> getBestDeal() async { final areaResult =

    await getArea(); if (areaResult case Success(value: final area)) { final restaurantResult = await getRestaurantByAreaId(area); return switch (restaurantResult) { Success(value: final restaurant) => await getBestDealByRestaurantId(restaurant.id), Failure(exception: final _) => Failure(Exception('')), }; } else { return Failure(Exception('Error while getting best deal')); } } Future<Result<AreaModel, Exception>> getArea() async {} Future<Result<RestaurantModel, Exception>> getRestaurantByAreaId(int) async {} Future<Result<double, Exception>> getBestDealByRestaurantId(int) async {}
  46. Results 😱 Future<Result<int, Exception>> getBestDeal() async { final areaResult =

    await getArea(); if (areaResult case Success(value: final area)) { final restaurantResult = await getRestaurantByAreaId(area); return switch (restaurantResult) { Success(value: final restaurant) => await getBestDealByRestaurantId(restaurant.id), Failure(exception: final _) => Failure(Exception('')), }; } else { return Failure(Exception('Error while getting best deal')); } } Future<Result<AreaModel, Exception>> getArea() async {} Future<Result<RestaurantModel, Exception>> getRestaurantByAreaId(int) async {} Future<Result<double, Exception>> getBestDealByRestaurantId(int) async {}
  47. Useful packages • functional programming ◦ dartz ◦ fpdart •

    immutability ◦ fast_immutable_collections ◦ kt_dart ◦ built_collection ◦ equatable ◦ freezed • before Dart 3 ◦ tuple ◦ sealed_unions ◦ multiple_result
  48. fpdart • Well-documented • Extensions • Types ◦ Unit ◦

    Option ◦ Either ◦ Task ▪ TaskOption ▪ TaskEither ◦ State ▪ StateAsync ◦ IO ▪ IOOption ▪ IOEither ▪ IORef ◦ Reader Sandro Maglione
  49. Either /// Create an instance of [Right] final right =

    Either<String, int>.of(10); /// Create an instance of [Left] final left = Either<String, int>.left('none'); /// Return [Left] if the function throws an error. /// Otherwise return [Right]. final tryCatch = Either.tryCatch( () => int.parse('invalid'), (e, s) => 'Error: $e', ); /// Extract the value from [Either] final value = right.getOrElse((l) => -1); /// Chain computations final flatMap = right.flatMap((a) => Either.of(a + 10)) /// Pattern matching final match = right.match( (l) => print('Left($l)'), (r) => print('Right($r)'), ); • Error handling • Try-catch constructor • Extraction • Chaining • Pattern matching
  50. Result Future<Result<AreaModel, Exception>> getArea() async { try { final uri

    = Uri.parse(Constants.getAreaApi); final response = await http.get(uri); switch (response.statusCode) { case 200: final data = json.decode(response.body); return Success(AreaModel.fromMap(data)); default: throw Exception(response.reasonPhrase); } } on Exception catch (e) { return Failure(e); } on TypeError catch (error) { return Failure(Exception(error.toString())); } catch (error) { return Failure(Exception(error.toString())); } }
  51. Either Future<Either<String, AreaModel>> getArea() async { try { final uri

    = Uri.parse(Constants.getAreaApi); final response = await http.get(uri); switch (response.statusCode) { case 200: final data = json.decode(response.body); return Either.right(AreaModel.fromMap(data)); default: throw Exception(response.reasonPhrase); } } on ParsingException catch (e) { return Either.left(e.toString()); } on Exception catch (e) { return Either.left(e.toString()); } on TypeError catch (error) { return Either.left(error.toString()); } catch (error) { return Either.left(error.toString()); } }
  52. Result void main() async { final result = await getArea();

    final String value = switch (result) { Success(value: final area) => area.toString(), Failure(exception: final exception) => exception.toString(), }; } Future<Result<AreaModel, Exception>> getArea() async {}
  53. Either Future<Either<String, AreaModel> getArea() async {} void main() async {

    final result = await getArea(); /// Pattern matching result.match( (String error) => print(error), (AreaModel area) => print('$area'), ); }
  54. Task /// Create instance of [Task] from a value final

    Task<int> task = Task.of(10); /// Create instance of [Task] from an async function final taskRun1 = Task(() async => 10); final taskRun2 = Task(() => Future.value(10)); /// Map [int] to [String] final Task<String> map = task.map((a) => '$a'); /// Extract the value inside [Task] by running its async function final int value = await task.run(); /// Chain another [Task] based on the value of the current [Task] final flatMap = task.flatMap((a) => Task.of(a + 10)); • Wrapper around Future • lazy evaluation • Composing async functions
  55. TaskEither TaskEither<String, AreaModel> getAreaTaskEither() => TaskEither.tryCatch(() async { final uri

    = Uri.parse(Constants.getAreaApi); final response = await http.get(uri); switch (response.statusCode) { case 200: final data = json.decode(response.body); return AreaModel.fromMap(data); default: throw Exception(response.reasonPhrase); } }, (e, s) => '$e');
  56. Result Future<Result<AreaModel, Exception>> getArea() async {} void main() async {

    final result = await getArea(); /// Pattern matching result.match( (l) => print(l), (r) => print(r), ); }
  57. TaskEither TaskEither<AreaModel, Exception> getArea() async {} void main() async {

    final task = getAreaTaskEither(); final Either<String, AreaModel> result = await task.run(); /// Pattern matching result.match( (l) => print(l), (r) => print(r), ); }
  58. Results Future<Result<int, Exception>> getBestDeal() async { final areaResult = await

    getArea(); if (areaResult case Success(value: final area)) { final restaurantResult = await getRestaurantByAreaId(area); return switch (restaurantResult) { Success(value: final restaurant) => await getBestDealByRestaurantId(restaurant.id), Failure(exception: final _) => Failure(Exception('')), }; } else { return Failure(Exception('Error while getting best deal')); } } Future<Result<AreaModel, Exception>> getArea() async {} Future<Result<RestaurantModel, Exception>> getRestaurantByAreaId(int) async {} Future<Result<double, Exception>> getBestDealByRestaurantId(int) async {}
  59. TaskEither void main() async { final task = getAreaTaskEither() .flatMap((area)

    => getRestaurantByAreaIdTaskEither(area.id)) .flatMap((restaurant) => getBestDealByRestaurantId(restaurant.id)); final Either<String, double> result = await task.run(); result.fold( (l) => print(l), (r) => print(r), ); } TaskEither<String, AreaModel> getArea() {} TaskEither<String, RestaurantModel> getRestaurantByAreaId(int areaId) {} TaskEither<String, double> getBestDealByRestaurantId(int id) {}
  60. Conclusion • Understand the key concepts of functional programming •

    Be aware about the Dart features • Start small ◦ Focus one aspects of the functional programming ◦ Adopt it together with your colleagues
  61. Resources • https://fsharpforfunandprofit.com/ by Scott Wlaschin • https://github.com/spebbe/dartz by Björn

    Sperber • https://github.com/SandroMaglione/fpdart by Sandro Maglione • https://www.sandromaglione.com/ by Sandro Maglione • https://codewithandrea.com/articles/functional-error-handling-either-fpdart/ by Andrea Bizotto • https://github.com/marcglasberg/fast_immutable_collections by Marcelo Glasberg and Philippe Fanaro • https://www.droidcon.com/2022/11/15/getting-started-with-functional-programming/ by Pascal Welsch • https://resocoder.com/2019/12/14/functional-error-handling-in-flutter-dart-2-either-task- fp/ by Matt Rešetár (Reso Coder)
  62. Senior Software Engineer at talabat, Delivery Hero Co-organizer of Flutter

    Abu Dhabi & Dubai gerfalcon GerfalconVogel Csongor Vogel csongorvogel Thank you