Embrace Functional Programming with /Dart 3.1/

Maxim Saplin - Oct 29 '23 - - Dev Community

Object-Oriented Programming (OOP) has long dominated the programming world, with languages like Java and C# taking center stage. However, in recent years, Functional Programming has gained traction with languages like Haskell, Erlang, and Elixir offering powerful alternatives.

Dart, known as the go-to language for building scalable, reactive applications with the help of the Flutter framework, has traditionally been an object-oriented language. With the release of Dart 3.1, it introduces features that enable you to embrace the merits of functional programming as well.

This article will present an overview of OOP vs. Functional Programming, introduce new features in Dart 3.1, and demonstrate their usage with a hands-on example. Lastly, we will draw a comparison between Dart and other popular functional languages.

OOP vs. Functional Programming

In OOP, you represent data and behavior as objects, usually organized in hierarchies of classes. These objects interact with one another by sending messages or invoking methods. Modularity, inheritance, and polymorphism are key concepts in the OOP paradigm.

Functional Programming, on the other hand, focuses on immutability, treating data as separate from its behavior, and organizing code around functions and expressions. Key principles include pure functions, recursion, higher-order functions, and lazy evaluation.

The next quote from abovementioned article gives a nice comparison:

You can think of functional style architecture as the inverse of OO architecture. Instead of having all the code for one type in one place (OO instance methods in subclass declarations), you have all the code for one operation in one place (functional switching over types to define behavior).

New Features in Dart 3.1

Version 3.1 introduces features that enable functional programming alongside its core OOP principles:

  • Pattern Matching and Object Patterns: Allows for more concise code structures similar to pattern matching in functional languages.
  • Sealed Classes and Exhaustiveness Checking: Provides a way to implement algebraic data types, similar to sum types in functional languages, ensuring your code covers all possible cases.
  • Switch Expressions: Enables functional-style expressions that produce values.
  • Records: Allows returning multiple heterogeneous values from a function. No more custom classes are needed to return, say, (x,y) coordinate

Example: A Simple Message System in Dart

Let's build a simple message system that showcases these new features in Dart. We will define several types of messages, functions to display them.

// sealed classes are abstract (can't be instatiated) and can only be
// implemented in the same library (library in Dart is one file be default)
sealed class Message {
}

class TextMessage implements Message {
  final String content;
  TextMessage(this.content);
}

class ImageMessage implements Message {
  final String content;
  final String imageUrl;
  ImageMessage(this.content, this.imageUrl);
}

class FileMessage implements Message {
  final String content;
  final String fileUrl;
  FileMessage(this.content, this.fileUrl);
}

// Extract fields from a message and return as Record
(String type, String content, String url) getMessage(Message message) {
  // using switch expression, https://dart.dev/language/branches#switch-expressions
  return switch (message) {
    TextMessage(:var content) => ('Text', content, ''),
    ImageMessage(:var content, :var imageUrl) => ('Image', content, imageUrl),
    FileMessage(:var content, :var fileUrl) => ('File', content, fileUrl)
  };
}

void printTexts(List<Message> messages) {
  // using normal switch statement
  for (var m in messages) {
    switch (m) {
      case TextMessage(content: final text): print(text);
      case ImageMessage(): print('<img>');
      default: print('<smth-else>');
    }
  }
}

void main() {
  var m0 = TextMessage('Hello, John!'), m1 = TextMessage('Hi, Jack!');
  var m2 = ImageMessage('Check this out', 'https://example.com/image1.jpg');
  var m3 = FileMessage('Important document', 'https://example.com/document1.pdf');

  var messages = [m0, m1, m2, m3];
  for (var m in messages){ 
    print(getMessage(m)); 
  }
  printTexts(messages);
}
Enter fullscreen mode Exit fullscreen mode

This example illustrates the use of sealed classes, switch expressions, and object patterns in Dart, which were introduced in Dart 3.1, enabling a more functional programming style while retaining the advantages of Dart's object-oriented core.

  1. Message is a sealed class, ensuring that it can only be implemented within the same library. By default, this library is one file.
  2. TextMessage, ImageMessage, and FileMessage are three classes that implement Message.
  3. getMessage is a function that utilizes a switch statement with object patterns to extract fields from a given message. It returns a tuple (using records) consisting of the message type, content, and URL (if applicable). Notice that the switch expression syntax is used here, which promotes a more functional programming style.
  4. printTexts is a function that prints text messages out of a list of messages using a normal switch statement. If the message is a TextMessage, it prints its content, and if it's an ImageMessage, it prints the string '<img>'. Otherwise, it prints the string '<smth-else>'.
  5. In the main function, several message instances are created and stored in a list called messages. The getMessage function is used to print the extracted information from each message, while the printTexts function prints only the text messages.

Same Example in Haskell

-- Define message types
data Message = TextMessage String
             | ImageMessage String String
             | FileMessage String String
             deriving (Show)

-- Extract fields from a message and return as a tuple
getMessage :: Message -> (String, String, String)
getMessage (TextMessage content) = ("Text", content, "")
getMessage (ImageMessage content imageUrl) = ("Image", content, imageUrl)
getMessage (FileMessage content fileUrl) = ("File", content, fileUrl)

-- Print text of messages
printTexts :: [Message] -> IO ()
printTexts [] = return ()
printTexts (TextMessage content : messages) = do
  putStrLn content
  printTexts messages
printTexts (ImageMessage _ _ : messages) = do
  putStrLn "<img>"
  printTexts messages
printTexts (_ : messages) = do
  putStrLn "<smth-else>"
  printTexts messages

main :: IO ()
main = do
  -- Create sample messages
  let m0 = TextMessage "Hello, John!"
      m1 = TextMessage "Hi, Jack!"
      m2 = ImageMessage "Check this out" "https://example.com/image1.jpg"
      m3 = FileMessage "Important document" "https://example.com/document1.pdf"

      messages = [m0, m1, m2, m3]

  -- Print messages using getMessage
  mapM_ (print . getMessage) messages

  -- Print texts of messages using printTexts
  printTexts messages
Enter fullscreen mode Exit fullscreen mode

The Haskell alternative to the Dart code uses algebraic data types to represent different types of messages (TextMessage, ImageMessage, and FileMessage). The getMessage function uses pattern matching on the message data type and returns a tuple with information related to that message. The printTexts function is a recursive function that prints only the text content of messages, similar to the Dart code.

Finally, in the main function, we create instances of messages and print the extracted information from the getMessage function, followed by printing the text messages using the printTexts function.

Output

Both examples produce the same result:

(Text, Hello, John!, )
(Text, Hi, Jack!, )
(Image, Check this out, https://example.com/image1.jpg)
(File, Important document, https://example.com/document1.pdf)
Hello, John!
Hi, Jack!
<img>
<smth-else>
Enter fullscreen mode Exit fullscreen mode

Wrapping Up: Dart and Haskell's Functional Approaches

As demonstrated through the examples, Dart and Haskell employ different approaches in embracing functional programming concepts. Haskell is a pure functional language designed from its core around immutability, expressions, and functions. It naturally encourages pure functions, recursion, and higher-order functions.

Conversely, Dart is an object-oriented language that has progressively incorporated functional features while retaining its core OOP principles. Some of these functional features include pattern matching, sealed classes, switch expressions, and records.

Both languages present their own unique ways of solving problems, and developers can choose based on their needs or preferences. Dart provides a flexible environment to combine OOP and functional programming styles, while Haskell offers an opportunity to immerse oneself in a purely functional paradigm.

By exploring these functional programming concepts and comparing them in both languages, developers can further strengthen their understanding of programming paradigms and learn to adopt the best practices and techniques from each approach.

BONUS

And here we have OOP alternative in Dart. Notice how it is ~50% longer than the functional version:

abstract class Message {
  String get content;
  String getMessage();
}

class TextMessage implements Message {
  @override
  final String content;
  TextMessage(this.content);

  String get url => '';
  String get type => 'Text';

  @override
  String getMessage() {
    return '($type, $content, $url)';
  }
}

class ImageMessage implements Message {
  @override
  final String content;
  final String imageUrl;
  ImageMessage(this.content, this.imageUrl);

  String get url => imageUrl;
  String get type => 'Image';

  @override
  String getMessage() {
    return '($type, $content, $url)';
  }
}

class FileMessage implements Message {
  @override
  final String content;
  final String fileUrl;
  FileMessage(this.content, this.fileUrl);

  String get url => fileUrl;
  String get type => 'File';

  @override
  String getMessage() {
    return '($type, $content, $url)';
  }
}

void printTexts(List<Message> messages) {
  for (var m in messages) {
    if (m is TextMessage) {
      print(m.content);
    } else if (m is ImageMessage) {
      print('<img>');
    } else {
      print('<smth-else>');
    }
  }
}

void main() {
  var m0 = TextMessage('Hello, John!'), m1 = TextMessage('Hi, Jack!');
  var m2 = ImageMessage('Check this out', 'https://example.com/image1.jpg');
  var m3 = FileMessage('Important document', 'https://example.com/document1.pdf');

  var messages = [m0, m1, m2, m3];
  for (var m in messages) {
    print(m.getMessage());
  }
  printTexts(messages);
}
Enter fullscreen mode Exit fullscreen mode

And here're all 3 gists.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .