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);
}
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.
-
Message
is a sealed class, ensuring that it can only be implemented within the same library. By default, this library is one file. -
TextMessage
,ImageMessage
, andFileMessage
are three classes that implementMessage
. -
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. -
printTexts
is a function that prints text messages out of a list of messages using a normal switch statement. If the message is aTextMessage
, it prints its content, and if it's anImageMessage
, it prints the string'<img>'
. Otherwise, it prints the string'<smth-else>'
. - In the
main
function, several message instances are created and stored in a list calledmessages
. ThegetMessage
function is used to print the extracted information from each message, while theprintTexts
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
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>
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);
}
And here're all 3 gists.