Dart: Features

Dart: Features

Special features of Dart language.

Dart is a hybrid language. Is object-oriented but also a functional programming language. Here are some interesting features that make Dart special:


Closures

A closure in Dart is a function that has access to variables from its enclosing scope, even after the enclosing function has returned.

Some key points about closures in Dart:

  • A closure is a function object that has access to variables in its lexical scope, even after the lexical scope has returned.

  • Closures are created whenever a function accesses a variable from an outer scope.

  • The outer variables captured by the closure remain available to the inner function, even after the outer function has returned.

  • This allows the inner function to access and modify the captured variables.

  • Closures in Dart are implemented using function objects.

  • Dart functions are objects and can be assigned to variables and returned from other functions.

Example:

int counter = 0;

Function makeCounter() {
  return () {
    counter++; 
    print(counter);
  } 
}

var increment = makeCounter();
increment();  // Prints 1  
increment();  // Prints 2

Here, the makeCounter() function returns a closure - an inner function that has access to the counter variable from the outer function scope.

When we call increment(), it increments and prints the captured counter variable, even though makeCounter() has already returned.


Generators

A generator in Dart is a function that can pause its execution and return a state, which can later be resumed to continue its execution.

Some key points about generators in Dart:

  • Generators allow breaking up the execution of a function into multiple parts.

  • They are implemented using iterator functions that contain yield statements.

  • When a yield statement is encountered, the generator pauses and returns a value.

  • The generator's state is saved so it can be resumed later.

  • Generators are resumed by calling the moveNext() method on the iterator.

  • This allows iterating over the values returned by successive yield statements.

Example:

Iterable<int> fibonacciNumbers() sync* {
  int a = 0, b = 1;  
  while (true) {
    yield a;
    var temp = a;
    a = b; 
    b = temp + b;  
  }
}

var numbers = fibonacciNumbers();
print(numbers.moveNext());  // Prints a 
print(numbers.moveNext()); // Prints b

Here, fibonacciNumbers() is a generator function that yields the next Fibonacci number each time.

We can iterate over the returned iterator to get successive Fibonacci numbers.

Key benefits of generators:

  • Allow breaking up long-running functions

  • Save and restore state between invocations

  • Can model infinite streams of data


Properties

You can define properties for functions in Dart using the function syntax.

To define a property for a function, you use:

FunctionName({
  required this.propertyName1,
  this.propertyName2,
  ...
})

Here:

  • required indicates the property is required and must be passed.

  • this.propertyName refers to a property of the function.

  • The properties can have default values using = defaultValue.

For example:

addNumbers({
  required int firstNumber,
  required int secondNumber,
}) {
  return firstNumber + secondNumber;
}

Here we have defined two required properties - firstNumber and secondNumber - for the addNumbers() function.

We can then call the function like this:

var result = addNumbers(
  firstNumber: 10,  
  secondNumber: 20  
);

Passing the property values as named arguments.

We can also define optional properties:

multiplyNumbers({
  required int firstNumber,
  required int secondNumber,
  int base = 1,
}) { ... }

Here base has a default value of 1, so it is optional.

So in summary, yes - you can define properties for functions in Dart using the { } syntax. The properties can be required or optional, and can have default values.


Objects

Dart functions can act as objects in several ways:

  1. Functions are first-class citizens in Dart. This means functions can be:
  • Assigned to variables

  • Passed as arguments to other functions

  • Returned from functions

For example:

void func1() {
  print('func1');
}

void func2(Function f) {
  f(); 
}

Function getFunc() {
  return () => print('hello');
}

void main() {
  var f = func1;  
  f();                // Calls func1

  func2(func1);       // Passes func1 as arg

  var g = getFunc();
  g();                // Prints hello
}
  1. Functions can have properties as discussed earlier using the { } syntax. This makes them behave more like objects.

  2. Functions can implement interfaces. This allows treating functions as objects.

For example:

abstract class Comparable {
  int compareTo(Object other);
}

int compare(int a, int b) => a - b;

void main() {
  Comparable c = compare;  
  c.compareTo(2);   // Calls compare function  
}
  1. Functions can be passed to other functions as function types.
void doSomething(void Function() f) {
   f();  
}

doSomething(func1);

So in summary, functions in Dart can be treated as first-class citizens and objects by:

  • Being assigned to variables

  • Having properties

  • Implementing interfaces

  • Being passed as function types

This allows treating functions as objects and makes for a more expressive language.


Lambda

Dart supports lambda functions, also known as anonymous functions or closures.

Lambda functions in Dart are defined using the syntax:

(parameters) => expression

Or

(parameters) {
   statements  
}

For example:

var add = (int a, int b) => a + b;

var square = (int x) {
  return x * x;  
};

Here add and square are lambda functions that add two numbers and square a number respectively.

You can pass lambda functions as arguments to other functions:

doSomething((a, b) => a + b);

list.forEach((item) => print(item));

And lambda functions can capture variables from the outer scope using closures:

int multiplier = 2;

var doubleNumber = (int x) {
  return x * multiplier;  
};

doubleNumber(10); // Returns 20

Here doubleNumber closes over the multiplier variable.

So in summary, yes - Dart supports:

  • Defining lambda functions using => or { } syntax

  • Passing lambda functions as arguments

  • Capturing outer scope variables using closures

This makes Dart a very functional language and allows for concise syntax when passing functions as arguments.


Callback

A callback function in Dart is a function that is passed as an argument to another function, and is executed after the outer function has finished.

A lambda function can be used as a callback, since lambdas can be passed as function arguments.

For example:

void doSomething(void Function() callback) {
  // Do something... 
  callback();  
}

void main() {
  doSomething(() {
    print('Callback function');
  });
}

Here we have passed an anonymous lambda function as the callback argument to the doSomething() function.

The lambda function prints 'Callback function', and this is executed after doSomething() has completed its task.

We can also pass named functions as callbacks:

void callbackFunction() {
  print('Callback!'); 
}

doSomething(callbackFunction);

Here we pass the named callbackFunction as the callback.

Callback functions are useful in situations like:

  • Event handlers

  • Asynchronous functions

  • Iterators

For example:

button.onTap = () {
  print('Button tapped');
};

Here we have passed a lambda as a callback to the onTap event handler. This lambda will be executed whenever the button is tapped.

So in summary, a callback function:

  • Is passed as an argument to another function

  • Is executed after the outer function has completed

  • Lambda functions can be used as callbacks

Callbacks allow splitting up operations into separate functions that are composed at runtime, making the code flexible and reusable.


Use-Cases

Here are some common use cases for callback functions:

  1. Event handlers: Callbacks are used to handle events like button clicks, form submissions, etc. A function is passed as a callback and executed when the event occurs.

For example:

button.onClick = () {
  // Handle click 
};
  1. Asynchronous functions: Callbacks are useful when dealing with asynchronous functions that take some time to complete. A callback is passed to be executed after the asynchronous operation completes.

For example:

getData() {
  // Asynchronous call 
  someAsyncCall((data) {
    // Callback with data 
  });
}

The callback is executed after someAsyncCall completes and data is available.

  1. Iterators: Callbacks are used with iterators like forEach, map, etc. A function is passed as a callback and executed for each element.

For example:

list.forEach((element) {
  // Called for each element 
});
  1. Higher-order functions: Functions that take other functions as arguments can use callbacks. This allows composing multiple functions.

For example:

doSomething(callback) {
  // ... 
  callback(); 
}

doSomething(() => print('Done!'));
  1. Delaying function execution: Callbacks allow delaying the execution of a function until some other operation has completed.

So in summary, callbacks are useful for:

  • Handling events

  • Asynchronous operations

  • Iterating over collections

  • Higher-order functions

  • Delaying function execution

Hope this helps explain the common use cases for callback functions! Let me know if you have any other questions.


Disclaim: These topics are explained by Ric. I have just asked some random questions about features I know from other languages. You should follow the documentation to learn Dart properly.