Evaluation Strategies in Programming Languages, Part 2

Evaluation Strategies in Programming Languages, Part 2

Part 1 of this article is here

https://www.linkedin.com/pulse/evaluation-strategies-programming-languages-part-1-laaksonen

It covers the theory of evaluation and binding strategies, inlining, coroutines and other topics. This second part shows how they are used in specific programming languages.


Specific Programming Languages

To better understand how different evaluation strategies work, it is best to go through various programming languages, and examine what evaluations strategies they use, and how they are implemented. Examples should give better understanding on this subject. But I also suggest that everyone should test these things using a simple test project. Create variables and test functions and see from output what happens.


Ruby

Ruby is a language that uses pass-by-value, but since everything in Ruby is an object, the values are references to objects. This means that when you pass an argument to a function, you are passing a copy of the reference to the object, not the object itself. The function can use the reference to access or modify the object, but it cannot change which object the reference points to.

For example, consider this function:

def func(str)
 str.upcase!
end

This function takes a string argument and modifies it by calling the `upcase!` method, which changes the string to uppercase in place. If you pass a string variable to this function, the function will receive a copy of the reference to the same string object, and it will modify that object. This will affect the original variable as well, because it still points to the same object. For example:

s = "hello"
func(s)
puts s # => HELLO

However, if you assign a new string object to the argument inside the function, it will not affect the original variable, because it still points to the old object. For example:

def func(str)
 str = "goodbye"
end

 

s = "hello"
func(s)
puts s # => hello

This is different from pass-by-reference, where the function would receive the actual reference to the object, not a copy of it, and it could change which object the reference points to. For example, in C++, you can use the `&` operator to indicate pass-by-reference:

void func(string& str) {
 str = "goodbye";
}

 

string s = "hello";
func(s);
cout << s << endl; // => goodbye

So, Ruby is not pass-by-reference, but it can sometimes appear to behave like it because of its use of references to objects. However, there are some subtle differences that you should be aware of. This underlines why programmer should know, regardless of terminology, what exactly is passed to function as argument and how they are handled in specific programming language. Value can be primitive value, or it can be a reference, or even a pointer to memory address.


C++

C++ is a language that supports both pass-by-value and pass-by-reference. This means that you can choose whether to pass a copy of the variable or a reference to the variable to a function. The function can use the reference to access or modify the variable, or it can use the copy to work with a local version of the variable.

To pass a variable by reference in C++, you need to use the `&` operator both in the function declaration and in the function call. For example:

#include <stdio.h>

void swapnum(int &i, int &j) {
 int temp = i;
 i = j;
 j = temp;
}

 

int main(void) {
 int a = 10;
 int b = 20;
 printf("A is %d and B is %d\n", a, b);
 swapnum(a, b);
 printf("A is %d and B is %d\n", a, b);
 return 0;
}

 

Output:

A is 10 and B is 20
A is 20 and B is 10

This function takes two int arguments by reference and swaps their values. The function receives references to the same variables that were passed in, not copies of them. The function can use the references to modify the variables, and the changes will be reflected in the original variables.

To pass a variable by value in C++, you don't need to use any operator. For example:

#include <iostream>
using namespace std;

void swap(int x, int y)
{
    int t = x;
    x = y;
    y = t;
    cout << "After Swapping in function x: " << x
         << ", y: " << y << endl;
}

 

int main()
{
    int x = 1, y = 2;
    cout << "Before Swapping: ";
    cout << "x: " << x << ", y: " << y << endl;
    swap(x, y);
    cout << "After Swapping: ";
    cout << "x: " << x << ", y: " << y << endl;
    return 0;
}

Output:

Before Swapping: x: 1, y: 2
After Swapping in function x: 2, y: 1
After Swapping: x: 1, y: 2

This function takes an int argument by value and increments it by one. The function receives a copy of the variable that was passed in, not a reference to it. The function can use the copy to perform calculations, but it cannot modify the original variable, so the swapping happens only within function execution, but the original variables remain as they were.

Note that C++ also has another operator, `*`, that can be used to pass variables by reference using pointers. A pointer is a variable that stores the address of another variable. To pass a pointer to a function, you need to use the `*` operator both in the function declaration and in the function call. For example:

#include <iostream>
using namespace std;

void swap(int* x, int* y)
{
    int temp = *x;
    *x = *y;
    *y = temp;
}

// Driver code
int main()
{
    int a = 2, b = 5;
    cout << "values of a and b before swapping: " << a
         << " " << b << endl;
    swap(&a, &b); // passing address of a and b
    cout << "values of a and b after swapping: " << a << " "
         << b << endl;
    return 0;
}

This function takes two pointers to int arguments and swaps their values. The function receives pointers to the same variables that were passed in, not copies of them. The function can use the pointers to access or modify the variables, and the changes will be reflected in the original variables.


Call-by-future is an evaluation strategy in which the arguments to a function are evaluated concurrently with the function body using coroutines. This means that the arguments are wrapped in future objects that can be awaited from the function, and the function can access the values of the arguments when they are ready. This strategy allows for concurrency and parallelism, but it may also introduce overhead and non-determinism. This strategy is used in languages such as C++20, where coroutines are implemented as awaitable objects that can be used with std::future or std::async. For example:

auto compute = []() -> std::future<int> {

 int fst = co_await std::async(get_first);

 int snd = co_await std::async(get_second);

 co_return fst + snd;

};

 

auto f = compute();

// some heavy task

f.get();

This code creates a coroutine that computes the sum of two values obtained from two asynchronous tasks. The coroutine uses co_await to suspend itself until the future values are ready, and then resumes to return the result. The coroutine returns a std::future<int> that can be used to get the final value.


Partial evaluation works in C++ by using template metaprogramming to perform compile-time specialization of programs based on static inputs. Template metaprogramming is a technique that allows writing programs that generate other programs using templates as a form of code generation.

For example, suppose we have a template function `pow` that computes the power of a base and an exponent:

template <int base, int exp>

int pow() {

 return (exp == 0) ? 1 : base * pow<base, exp - 1>();

}

This function can be partially evaluated with respect to the static inputs `base` and `exp` by instantiating the template with concrete values. For example, `pow<2, 3>()` will produce the following specialized function:

int pow<2, 3>() {

 return 2 * pow<2, 2>();

}

 

int pow<2, 2>() {

 return 2 * pow<2, 1>();

}

 

int pow<2, 1>() {

 return 2 * pow<2, 0>();

}

 

int pow<2, 0>() {

 return 1;

}

This function can be further optimized by constant folding and inlining, resulting in the following residual program:

int pow<2, 3>() {

 return 8;

}

This residual program is equivalent to the original program when `base = 2` and `exp = 3`, but it is much faster and simpler, since it does not involve any recursion or multiplication.

It is possible to use lazy evaluation in C++, although it is not a native feature of the language. Lazy evaluation means that an expression is only evaluated when its value is actually needed, not when it is declared or assigned. This can have some benefits, such as improving performance, avoiding unnecessary calculations, and creating potentially infinite data structures. However, it can also have some drawbacks, such as introducing unexpected behaviour, making debugging more difficult, or causing memory leaks.

There are different ways to implement lazy evaluation in C++, depending on the context and the goal. Some of them are:


Using iterators: Iterators are objects that can traverse a collection of elements and return one element at a time. Iterators can be used to implement lazy evaluation by using the `yield` keyword, which returns an `IEnumerable<T>` that supports deferred execution. For example, suppose we have a function that returns a collection of prime numbers:

IEnumerable<int> GetPrimes(

{

   int n = 2;

   while (true)

   {

       if (IsPrime(n))

       {

           yield return n;

       }

       n++;

   }

})

This function uses an infinite loop to generate prime numbers, but it does not compute them all at once. Instead, it returns an `IEnumerable<int>` that will only compute and return the next prime number when it is requested. This way, we can create a potentially infinite collection of prime numbers without wasting memory or time.


Using delegates: Delegates are objects that can store and invoke a function or a lambda expression. Delegates can be used to implement lazy evaluation by deferring the execution of a function until it is needed. For example, suppose we have a function that performs a costly calculation:

int ExpensiveCalculation(int x)

{

   // Do some heavy computation

   return result;

}

If we pass this function to another function that may or may not use its result, we are forcing the calculation to happen before it is needed. A better way to pass the function is to use a delegate that captures the parameter of the calculation and only performs it when invoked:

void DoSomething(Func<int> func

{

   // Do something

   if (condition)

   {

       int result = func(); // The delegate is invoked here

       // Use result

   }

}

 

int x = 10;

DoSomething(() => ExpensiveCalculation(x)); // The lambda expression is not evaluated until it is called inside DoSomething)

This way, we can avoid performing the calculation until it is actually needed.

 

Using the `Lazy<T>` class: The `Lazy<T>` class is a wrapper that provides a way to create an object that is initialized only when its value is first accessed. The `Lazy<T>` class takes a function or a lambda expression that returns an instance of type `T`, and stores it internally. When the `Value` property of the `Lazy<T>` object is accessed, it invokes the function and caches the result for future use. For example, suppose we have a class that has a property that performs some expensive initialization:

class Foo

{

private:

   Bar _bar;

 

public:

   Bar GetBar()

   {

       if (_bar == nullptr)

       {

           _bar = new Bar(); // Expensive initialization

       }

       return _bar;

   }

};

If we create an instance of `Foo` and never access its `Bar` property, we are wasting resources by initializing `_bar`. A better way to implement this property is to use a `Lazy<Bar>` field, which will initialize `_bar` only when it is first accessed:

class Foo

{

private:

   Lazy<Bar> _bar;

 

public:

   Foo()

   {

       _bar = Lazy<Bar>([]() { return new Bar(); }); // The lambda expression is not evaluated until _bar.Value is accessed

   }

 

   Bar GetBar()

   {

       return _bar.Value;

   }

};

This way, we can avoid initializing `_bar` until it is actually needed.



Python

Python is similar to Ruby in that it uses pass-by-value, but the values are references to objects. This means that when you pass an argument to a function, you are passing a copy of the reference to the object, not the object itself. The function can use the reference to access or modify the object, but it cannot change which object the reference points to.

For example, consider this function:

def func(lst):

 lst.append(4)

This function takes a list argument and modifies it by calling the `append()` method, which adds an element to the end of the list. If you pass a list variable to this function, the function will receive a copy of the reference to the same list object, and it will modify that object. This will affect the original variable as well, because it still points to the same object. For example:

l = [1, 2, 3]

func(l)

print(l) # => [1, 2, 3, 4]

However, if you assign a new list object to the argument inside the function, it will not affect the original variable, because it still points to the old object. For example:

def func(lst):

 lst = [5, 6, 7]

 

l = [1, 2, 3]

func(l)

print(l) # => [1, 2, 3]

This is different from pass-by-reference, where the function would receive the actual reference to the object, not a copy of it, and it could change which object the reference points to. For example, in C#, you can use the `ref` keyword to indicate pass-by-reference:

void func(ref int x) {

 x = 10;

}

 

int y = 5;

func(ref y);

Console.WriteLine(y); // => 10

So, Python is not pass-by-reference, but it can sometimes appear to behave like it because of its use of references to objects.


JavaScript

JavaScript is a language that does not have pass-by-reference, but only pass-by-value. This means that when you pass a variable to a function, you are passing a copy of the value of the variable, not a reference to the variable itself. The function can use the copy to perform calculations, but it cannot modify the original variable.

However, JavaScript has two types of values: primitive values and object values. Primitive values are numbers, strings, booleans, null, and undefined. Object values are arrays, functions, and other objects. When you pass a primitive value to a function, you are passing a copy of the value itself. When you pass an object value to a function, you are passing a copy of the reference to the object. A reference is like a pointer that points to the location of the object in memory.

This means that when you pass an object value to a function, the function can use the reference to access or modify the properties of the object, but it cannot change which object the reference points to. For example:

function changeName(obj) {

 obj.name = "Alice";

}

 

var person = {name: "Bob"};

changeName(person);

console.log(person.name); // => Alice

This function takes an object value as an argument and modifies its name property. The function receives a copy of the reference to the same object that was passed in, not a copy of the object itself. The function can use the reference to modify the object, and the change will be reflected in the original object.

However, if you assign a new object value to the argument inside the function, it will not affect the original object, because it still points to the old object. For example:

function changeObject(obj) {

 obj = {name: "Alice"};

}

 

var person = {name: "Bob"};

changeObject(person);

console.log(person.name); // => Bob

This function takes an object value as an argument and assigns a new object value to it. The function receives a copy of the reference to the same object that was passed in, not a copy of the object itself. The function can use the reference to assign a new object value to it, but it cannot change which object the original reference points to.

So, JavaScript is a language that passes variables by value only, but it can sometimes appear to behave like pass-by-reference when passing object values, because of its use of references to objects.


Unreal Engine Blueprints

Unreal Engine Blueprints is a visual scripting system that allows users to create gameplay elements and logic without writing code. Blueprints use a node-based interface to represent expressions, statements, functions, events, and variables. In Blueprints, the first node to execute is an event, and then execution flows through the white execution wire from left to right.

You can visualize the execution flow while your game is running in the editor, which can help with debugging. Data also flows through wires coloured to match the variable types. Input pins are evaluated when the node executes, tracing the data wires back from right to left until the final result is calculated and supplied to the node.

What makes Blueprints a bit confusing from evaluation point of view, is the ambiguity of nodes. Although you can easily think that nodes are processing steps in program, there are many different types of nodes like, Event nodes, Execution Statements, Functions, Timers, Flow Control etc. User may program his own Functions and Macros. As I have written several times before, Blueprints are a rich environment for programming when used according to same principles as any other programming language. Unfortunately, they are often used by developers who have no understanding about programming, leading to poorly performing, inconsistent and unmaintainable components.

Apparent easiness of Blueprints makes it a bit too easy to start developing things, giving a false impression of understanding the programming, and thus many will skip the necessary preliminary concepts and subjects which are prerequisite for real professional programming. However, Blueprints use different evaluation strategies depending on the context and the syntax of the nodes. Some examples are:

Eager evaluation: Blueprints use eager evaluation for most nodes, meaning that they are executed as soon as they are encountered. For example, arithmetic operations, comparisons, assignments, function calls, and event dispatchers are all eagerly evaluated. Eager evaluation ensures that the order of execution is predictable and consistent with the visual flow of the nodes, but it may also execute unnecessary or expensive nodes that are not used or depend on a condition.

Lazy evaluation: Blueprints use lazy evaluation for some nodes, meaning that they are executed only when their value is needed. The Delay node uses a timer to execute its output after a specified amount of time. The Sequence node uses a counter to execute its outputs one by one. Lazy evaluation allows for minimal execution and conditional execution, but it may also introduce complexity and overhead.

Blueprints use macro expansion, too. You can define Blueprint macros, meaning that any occurrence of specific macro in Blueprint code is substituted with the code in corresponding macro.

Call-by-value: Blueprints use call-by-value for function inputs and outputs, meaning that the input expression is evaluated and a copy of its value is bound to the corresponding input in the function. This is the default behavior for types that implement the Copy trait, such as integers, booleans, characters, enums, structs, and references. Call-by-value ensures that the function does not modify the original input, but it may also incur a performance cost for large or complex types.

Call-by-reference: Blueprints use call-by-reference for function inputs and outputs, meaning that the input expression is not evaluated, but a reference to its location is bound to the corresponding input in the function. This is the default behavior for types that do not implement the Copy trait, such as arrays, maps, sets, actors, components, and objects. Call-by-reference allows the function to access or modify the original input, but it also requires explicit borrowing and dereferencing syntax. Call-by-reference can be either shared ( & ) or mutable ( &mut ), depending on whether the function can read or write to the input.

 

Unreal Blueprint function call-by-reference borrowing and dereferencing syntax is a way of passing parameters to a function by reference, meaning that the function can modify the original values of the arguments. To use this syntax, you need to do the following steps:

  • In the function definition, mark the parameters that you want to pass-by-reference with a Ref keyword in the Details panel.
  • In the function call node, connect the arguments that you want to pass-by-reference to the Ref pins on the node. These pins have a yellow border to indicate that they are references.
  • In the function body, use the Get or Set nodes to access or modify the values of the reference parameters. These nodes have a Dereference pin that connects to the parameter pin. The Dereference pin has a blue border to indicate that it is a reference.

 

Unreal Engine Blueprints do not have a general mechanism for caching evaluated input parameters, but some nodes may have specific behaviours that allow them to evaluate their inputs only once. For example:

Sequence node: The Sequence node executes its outputs one by one, but it evaluates all of its inputs at the same time when it is executed. This means that the inputs are evaluated only once, regardless of when the outputs are executed. This can be useful for avoiding repeated or inconsistent evaluations of the same expression.

Delay node: The Delay node executes its output after a specified amount of time, but it evaluates its input when it is executed. This means that the input is evaluated only once, regardless of when the output is executed. This can be useful for avoiding unnecessary or expensive evaluations of an expression that is not needed immediately.

Switch nodes: The Switch nodes execute one of their outputs based on the value of their input, but they evaluate their input only once when they are executed. This means that the input is evaluated only once, regardless of which output is executed. This can be useful for avoiding repeated or inconsistent evaluations of the same expression.

It does influence evaluation whether the Unreal Engine Blueprint function is pure or not. A pure function is a function that does not have any side effects, such as modifying the state of the class or the world, and always returns the same output for the same input. A pure function can be evaluated at any time, and its output can be cached and reused by the compiler. A pure function does not have an execution pin, but only data pins. A pure function is designated by checking the Pure checkbox in the function properties, or by specifying the BlueprintPure keyword in the function declaration for functions defined in code.

An impure function is a function that may have side effects, such as modifying the state of the class or the world, or returning different outputs for the same input. An impure function must be explicitly executed by connecting an execution pin to a function call node in an event graph. An impure function can have both execution pins and data pins. An impure function is designated by leaving the Pure checkbox unchecked in the function properties, or by specifying the BlueprintCallable keyword in the function declaration for functions defined in code.

If a pure function output is connected to multiple inputs, it is evaluated only once when the function is executed. The output value is then passed to all of the connected inputs. This can be useful for avoiding repeated or inconsistent evaluations of the same function. However, if you have connected e.g. a Random function node’s output into multiple inputs along the execution flow, then each node will receive the same random number, instead of different random number for each input, which could have been what you intended. It should be noted that this is not guaranteed, and it may depend on the compiler optimization and the order of evaluation. Therefore, it is recommended to avoid relying on this behaviour, and to use variables or local variables to store the output value of a pure function if it needs to be used multiple times or consistently.


Rust

Rust is a programming language that supports both eager and lazy evaluation strategies, depending on the context and the syntax. Some examples are:

Eager evaluation: Rust uses eager evaluation for most expressions, meaning that they are evaluated as soon as they are encountered. For example, function calls, arithmetic operations, comparisons, type casts, and assignments are all eagerly evaluated. Eager evaluation ensures that the order of evaluation is predictable and consistent with the source code, but it may also evaluate unnecessary or expensive expressions that are not used or depend on a condition.

Lazy evaluation: Rust uses lazy evaluation for some expressions, meaning that they are evaluated only when their value is needed. For example, the && and || operators use short-circuit evaluation, which evaluates the second operand only if the first operand does not suffice to determine the value of the expression. The .. and ..= operators use range expressions, which create an iterator that lazily generates values when iterated over. The async and await keywords use coroutines, which suspend and resume execution until a future value is ready. Lazy evaluation allows for minimal evaluation and conditional execution, but it may also introduce complexity and overhead.

Rust uses different parameter passing strategies depending on the type and the syntax of the parameters. Some examples are:

Pass-by-value: Rust uses pass-by-value for most types, meaning that the argument expression is evaluated and a copy of its value is bound to the corresponding parameter in the function. This is the default behavior for types that implement the Copy trait, such as integers, booleans, characters, tuples, arrays, and pointers. Pass-by-value ensures that the function does not modify the original argument, but it may also incur a performance cost for large or complex types.

Pass-by-reference: Rust uses pass-by-reference for some types, meaning that the argument expression is not evaluated, but a reference to its location is bound to the corresponding parameter in the function. This is the default behavior for types that do not implement the Copy trait, such as strings, vectors, slices, and structs. Pass-by-reference allows the function to access or modify the original argument, but it also requires explicit borrowing and dereferencing syntax. Pass-by-reference can be either shared ( & ) or mutable ( &mut ), depending on whether the function can read or write to the argument.

Call-by-value: Rust uses call-by-value for function pointers, meaning that the argument expression is evaluated and a pointer to its value is bound to the corresponding parameter in the function. Function pointers are types that have the syntax fn (T) -> U , where T and U are other types. They represent simple functions that do not capture any variables from their environment. Call-by-value allows the function to invoke the argument as another function, but it cannot modify it.

Call-by-reference: Rust uses call-by-reference for closures, meaning that the argument expression is not evaluated, but a reference to its value is bound to the corresponding parameter in the function. Closures are types that have the syntax Fn (T) -> U , FnMut (T) -> U , or FnOnce (T) -> U , where T and U are other types. They represent functions that can capture variables from their environment by reference, by mutable reference, or by value respectively. Call-by-reference allows the function to invoke or modify the argument as another function, but it also requires explicit borrowing and dereferencing syntax.


C#

C# is a language that supports both pass-by-value and pass-by-reference. This means that you can choose whether to pass a copy of the variable or a reference to the variable to a function. The function can use the reference to access or modify the variable, or it can use the copy to work with a local version of the variable.

However, passing depends on the parameters type, too. In C#, there are value types (primitives) and reference types, like objects.

-         Pass by value means passing a copy of the variable to the method.

-         Pass by reference means passing access to the variable to the method.

-         A variable of a reference type contains a reference to its data.

-         A variable of a value type contains its data directly.

Because a struct is a value type, when you pass a struct by value to a method, the method receives and operates on a copy of the struct argument. The method has no access to the original struct in the calling method and therefore can't change it in any way. The method can change only the copy.

A class instance is a reference type, not a value type. When a reference type is passed by value to a method, the method receives a copy of the reference to the class instance. That is, the called method receives a copy of the address of the instance, and the calling method retains the original address of the instance.

To pass a variable by value in C#, you don't need to use any keyword. For example:

void AddOne(int x) {

 x += 1;

}

 

int y = 5;

AddOne(y);

Console.WriteLine(y); // => 5

This function takes an int argument by value and adds one to it. The function receives a copy of the variable that was passed in, not a reference to it. The function can use the copy to perform calculations, but it cannot modify the original variable. Remember, that for reference type of parameters the passed value is the reference.

To specifically  make any variable as pass-by-reference in C#, you need to use the `ref` keyword both in the function declaration and in the function call. For example:

void DoubleInt(ref int x) {

 x += x;

}

 

int y = 5;

DoubleInt(ref y);

Console.WriteLine(y); // => 10

This function takes an int argument by reference and doubles its value. The function receives a reference to the same variable that was passed in, not a copy of it. The function can use the reference to modify the variable, and the change will be reflected in the original variable. Notice, that you may use a parameter of value type as pass-by-reference. A pass-by-reference parameter of function is different concept than pass-by-value parameter of reference type.

Note that C# also has another keyword, `out`, that can be used to pass variables by reference. The difference between `ref` and `out` is that `ref` requires that the variable is initialized before passing it, while `out` does not. However, `out` requires that the function assigns a value to the parameter before returning, while `ref` does not. For example:

void GetSquareAndCube(int x, out int square, out int cube) {

 square = x * x;

 cube = x * x * x;

}

 

int y = 5;

int s, c;

GetSquareAndCube(y, out s, out c);

Console.WriteLine(s); // => 25

Console.WriteLine(c); // => 125

This function takes an int argument by value and two int arguments by reference using `out`. The function calculates the square and cube of the input and assigns them to the output parameters. The output parameters do not need to be initialized before passing them, but they need to be assigned inside the function. The function can use the references to modify the output variables, and the changes will be reflected in the original variables.

Lazy evaluation is a technique that delays the computation of an expression until its value is actually needed. In C#, lazy evaluation can be achieved by using iterators, delegates, or the Lazy<T> class. Lazy evaluation can be useful for improving performance, avoiding unnecessary calculations, and creating potentially infinite data structures.

For example, suppose we have a method that returns a collection of numbers:

public List<int> GetNumbers(

{

   List<int> numbers = new List<int>();

   for (int i = 0; i < 100; i++)

   {

       numbers.Add(i);

   }

   return numbers;

})

If we call this method and only use the first element of the collection, we are wasting time and memory by computing and storing the rest of the elements. A better way to implement this method is to use an iterator with the yield keyword, which returns an IEnumerable<T> that supports lazy evaluation:

public IEnumerable<int> GetNumbers(

{

   for (int i = 0; i < 100; i++)

   {

       yield return i;

   }

})

Now, if we call this method and only use the first element of the collection, the rest of the elements will not be computed or stored until they are requested. This can improve the performance and memory usage of the program.

Another example of lazy evaluation is using delegates to defer the execution of a function until it is needed. For example, suppose we have a method that takes a Func<T> as a parameter and performs some operation on it:

public void DoSomething(Func<int> func

{

   Console.WriteLine("Doing something...");

   int result = func();

   Console.WriteLine("Result: " + result);

})

If we pass a function that performs a costly calculation to this method, we are forcing the calculation to happen before it is needed. A better way to pass the function is to use a lambda expression that captures the parameters of the calculation and only performs it when invoked:

int x = 10

int y = 20;

DoSomething(() => x * y); // The lambda expression is not evaluated until it is called inside DoSomething;

This way, we can avoid performing the calculation until it is actually needed.

 

A third example of lazy evaluation is using the Lazy<T> class, which provides a way to create an object that is initialized only when its value is first accessed. For example, suppose we have a class that has a property that performs some expensive initialization:

public class Foo

{

   private Bar _bar;

 

   public Bar Bar

   {

       get

       {

           if (_bar == null)

           {

               _bar = new Bar(); // Expensive initialization

           }

           return _bar;

       }

   }

}

If we create an instance of Foo and never access its Bar property, we are wasting resources by initializing _bar. A better way to implement this property is to use a Lazy<Bar> field, which will initialize _bar only when it is first accessed:

public class Foo

{

   private Lazy<Bar> _bar;

 

   public Foo()

   {

       _bar = new Lazy<Bar>(() => new Bar()); // The lambda expression is not evaluated until _bar.Value is accessed

   }

 

   public Bar Bar

   {

       get

       {

           return _bar.Value;

       }

   }

}

This way, we can avoid initializing _bar until it is actually needed.

Some advantages of lazy evaluation are:

  • It can improve the performance and memory usage of the program by avoiding unnecessary computations or allocations.
  • It can enable the creation of potentially infinite data structures, such as streams or sequences, that can be consumed on demand.
  • It can simplify the logic of some algorithms or functions by deferring complex calculations or side effects until they are needed.

Some disadvantages or problems of lazy evaluation are:

  • It can introduce unexpected behaviour or bugs if the programmer does not understand or control when and how the expressions are evaluated.
  • It can make debugging or testing more difficult or confusing, as the evaluation order or timing may not be obvious or predictable.
  • It can cause memory leaks or resource exhaustion if the unevaluated expressions hold references to large objects or resources that are not released.

 


Java

Java is a language that does not have pass-by-reference, but only pass-by-value. This means that when you pass a variable to a method, you are passing a copy of the value of the variable, not a reference to the variable itself. The method can use the copy to perform calculations, but it cannot modify the original variable.

However, Java has two types of values: primitive values and object values. Primitive values are int, char, boolean, and other basic types. Object values are instances of classes, such as String, ArrayList, or your own custom classes. When you pass a primitive value to a method, you are passing a copy of the value itself. When you pass an object value to a method, you are passing a copy of the reference to the object. A reference is like a pointer that points to the location of the object in memory.

This means that when you pass an object value to a method, the method can use the reference to access or modify the state of the object, but it cannot change which object the reference points to. For example:

public class Dog {

 private String name;

 

 public Dog(String name) {

   this.name = name;

 }

 

 public String getName() {

   return name;

 }

 

 public void setName(String name) {

   this.name = name;

 }

}

 

public class Test {

 

 public static void changeName(Dog dog) {

   dog.setName("Fido");

 }

 

 public static void main(String[] args) {

   Dog dog = new Dog("Rex");

   System.out.println(dog.getName()); // => Rex

   changeName(dog);

   System.out.println(dog.getName()); // => Fido

 }

}

This class defines a Dog object with a name property. The changeName method takes an object value as an argument and modifies its name property. The method receives a copy of the reference to the same object that was passed in, not a copy of the object itself. The method can use the reference to modify the object, and the change will be reflected in the original object.

However, if you assign a new object value to the argument inside the method, it will not affect the original object, because it still points to the old object. For example:

public class Test {

 

 public static void changeDog(Dog dog) {

   dog = new Dog("Fido");

 }

 

 public static void main(String[] args) {

   Dog dog = new Dog("Rex");

   System.out.println(dog.getName()); // => Rex

   changeDog(dog);

   System.out.println(dog.getName()); // => Rex

 }

}

This class defines a Dog object with a name property. The changeDog method takes an object value as an argument and assigns a new object value to it. The method receives a copy of the reference to the same object that was passed in, not a copy of the object itself. The method can use the reference to assign a new object value to it, but it cannot change which object the original reference points to.

So, Java is a language that passes variables by value only, but it can sometimes appear to behave like pass-by-reference when passing object values, because of its use of references to objects. However, there are some subtle differences that you should be aware of.


R Language

R is a language that does not have pass-by-reference, but only pass-by-value. This means that when you pass a variable to a function, you are passing a copy of the value of the variable, not a reference to the variable itself. The function can use the copy to perform calculations, but it cannot modify the original variable.

However, R has a feature called copy-on-modify, which means that it only creates a copy of the variable when it is modified. This can save memory and improve performance when passing large objects to functions. For example:

x <- 1:10 # create a vector

tracemem(x) # track the memory address of x

#> [1] "<0x7f9a8a2c6e18>"

y <- x # assign x to y

# no copy made yet

y[1] <- 0 # modify y

#> tracemem[0x7f9a8a2c6e18 -> 0x7f9a8a2c6e58]: 

# a copy of x is made and assigned to y

This example shows that R does not create a copy of x when it is assigned to y, but only when y is modified. The tracemem function helps us track the memory address of x and see when it changes.

However, copy-on-modify does not mean that R has pass-by-reference. Even if R does not create a copy of the variable until it is modified, it still passes the variable by value to the function. The function cannot change the original variable, only its own copy. For example:

x <- 1:10 # create a vector

tracemem(x) # track the memory address of x

#> [1] "<0x7f9a8a2c6e18>"

f <- function(x) {

 x[1] <- 0 # modify x

}

f(x) # call f with x

#> tracemem[0x7f9a8a2c6e18 -> 0x7f9a8a2c6e58]: f 

# a copy of x is made and passed to f

x # check x

#> [1] 1 2 3 4 5 6 7 8 9 10

# x is not changed by f

This example shows that R creates a copy of x and passes it to f by value. The function f modifies its own copy of x, but not the original x. The tracemem function shows us when the copy is made and passed to f.

So, R is a language that passes variables by value only, but it uses copy-on-modify to optimize memory usage and performance when passing large objects to functions. However, there are some ways to achieve pass-by-reference behavior in R using environments or packages such as R.oo or Rcpp. These methods allow you to create objects that can be modified by functions without creating copies. However, they are not part of the standard R language and require some extra care and caution when using them.


Haskell

Haskell uses lazy evaluation by default, which means that the parameters are evaluated only when they are needed, and not before. Lazy evaluation can avoid unnecessary computations, enable infinite data structures, and support modular programming. However, lazy evaluation can also introduce space leaks, unpredictable performance, and non-determinism.

Binding strategies define the kind of value that is passed to the function for each parameter, such as by value, by reference, by name, etc. Haskell uses call by name by default, which means that the parameters are passed as unevaluated expressions (thunks) that are substituted for the formal parameters in the function body. Call by name supports lazy evaluation and higher-order functions, but it can also cause multiple evaluations of the same expression or unexpected side effects.

Haskell also provides ways to change the default evaluation and binding strategies using various mechanisms, such as strictness annotations, bang patterns, seq function, evaluation strategies library, etc. These mechanisms allow the programmer to control the degree of evaluation (normal form, weak head normal form, etc.), the order of evaluation (left-to-right, right-to-left, etc.), and the parallelism of evaluation (sequential, parallel, concurrent, etc.)

Other evaluation strategies that can be used in Haskell are:

Parallel evaluation: This is a way of exploiting multiple processors or cores to evaluate the parameters of a function in parallel, using the `Eval` monad and the `rpar` and `rseq` functions. Parallel evaluation can improve the performance of the program, but it can also introduce non-determinism or overhead. Parallel evaluation can be modularized using evaluation strategies, which are functions that specify how to traverse and evaluate a data structure in parallel.

Strict evaluation: This is a way of forcing the parameters of a function to be evaluated before the function is called, using the `seq` function or the `!` annotation. Strict evaluation can avoid space leaks, improve performance, or ensure a predictable order of side effects. However, strict evaluation can also cause unnecessary computations, lose laziness, or introduce divergence.

Eager evaluation: This is a way of evaluating the parameters of a function from left to right as soon as the function is called, using the `BangPatterns` extension or the `($!)` operator. Eager evaluation is similar to strict evaluation, but it does not require the parameters to be fully evaluated to normal form, only to weak head normal form. Eager evaluation can have similar benefits and drawbacks as strict evaluation.


C Language

The C language uses a left-to-right evaluation order, which means that it evaluates the parameters of a function call from left to right. For example:

int f(int x) {

 printf("%d\n", x);

 return x;

}

 

int main() {

 f(1) + f(2); // prints 1 then 2

}

However, the evaluation order is only relevant for expressions with side effects, such as printing or modifying variables. For pure expressions, the order does not matter. Otherwise, the order of evaluation of the operands of any C operator, including the order of evaluation of function arguments in a function-call expression, and the order of evaluation of the subexpressions within any expression is unspecified. The compiler will evaluate them in any order, and may choose another order when the same expression is evaluated again.

The C language uses the call-by-value binding strategy, which means that it copies the contents of the actual parameters into the formal parameters. This means that any changes made to the formal parameters do not affect the actual parameters. For example:

void swap(int x, int y) {

 int aux = x;

 x = y;

 y = aux;

}

 

int main() {

 int a = 2;

 int b = 3;

 printf("%d, %d\n", a, b); // prints 2, 3

 swap(a, b);

 printf("%d, %d\n", a, b); // prints 2, 3

}

The swap function does not actually swap the values of a and b, because it only operates on copies of them. To achieve the desired effect, we need to use pointers to pass the addresses of the actual parameters, and dereference them in the function body. The C language does not support call-by-reference natively, but it can simulate it by using pointers. A pointer is a variable that stores the address of another variable. When you pass a pointer to a function, you are passing the value of the address, not the value of the variable itself. This allows the function to access and modify the variable that the pointer points to, by dereferencing it with the * operator. For example:

void swap(int *x, int *y) { // x and y are pointers to int

 int temp = *x; // temp stores the value of x's pointee

 *x = *y; // x's pointee is assigned the value of y's pointee

 *y = temp; // y's pointee is assigned the value of temp

}

 

int main() {

 int a = 2;

 int b = 3;

 printf("%d, %d\n", a, b); // prints 2, 3

 swap(&a, &b); // pass the addresses of a and b to swap

 printf("%d, %d\n", a, b); // prints 3, 2

}

In this example, the swap function takes two pointers to int as parameters. The main function passes the addresses of a and b to swap, using the & operator. The swap function then dereferences x and y to access and exchange the values of a and b. After the function call, the values of a and b are swapped.

 

Node.js

Node.js is a runtime environment that executes JavaScript code outside the browser. Node.js uses the V8 JavaScript engine, which implements the ECMAScript specification and supports various evaluation strategies for JavaScript code. Node.js is a multi-paradigm programming language that supports the following paradigms: event-driven, imperative, object-oriented, and functional programming. Functional programming and object-oriented programming are the most common and practical paradigms in JavaScript.

Functional programming is a paradigm that treats computation as the evaluation of mathematical functions and avoids changing state or mutable data. It can help write cleaner and more maintainable code by using concepts such as currying, composition, higher-order functions, immutable data structures and reactive programming. Node.js can support functional programming with its built-in features and external libraries. Some of the popular libraries for functional programming on Node.js are:

Ramda: A library that provides curried functions that can be composed easily and work well with immutable data1.

RxJS: A library that implements the reactive programming paradigm using observables, which are streams of data that can be manipulated with functional operators2.

lodash: A utility library that offers a wide range of functions for working with arrays, objects, strings, collections and more. It also has a functional version (lodash/fp) that supports currying and composition3.

There are many other libraries for functional programming on Node.js, such as Functools, mori, Bacon.js and more.

As a multi-paradigm language, Node.js supports several evaluation like:

Strict evaluation: This is the default evaluation strategy for JavaScript, where expressions are evaluated as soon as they are bound to a variable or passed as an argument to a function. Strict evaluation can be faster and simpler than other strategies, but it can also cause unnecessary computations or side effects.

Lazy evaluation: This is an evaluation strategy where expressions are evaluated only when their values are actually needed, not when they are bound or passed. Lazy evaluation can improve the performance and memory usage of a program by avoiding unnecessary computations or allocations. It can also enable some forms of infinite data structures or delayed execution. Node.js does not support lazy evaluation natively, but it can be simulated by using functions, promises, generators, or other techniques.

Short-circuit evaluation: This is a technique that applies to some Boolean operators, such as && and ||, that can skip evaluating some sub-expressions if the result of the expression can be determined without them. For example, in the expression x && y, if x is false, then the whole expression is false, regardless of the value of y. Therefore, the evaluation of y can be skipped. Short-circuit evaluation is a form of lazy evaluation that can improve the performance and safety of a program by avoiding unnecessary computations or errors.

Strategy pattern: This is a design pattern that allows a program to select an algorithm or a behaviour at runtime, depending on some criteria or context. The strategy pattern can make a program more flexible and adaptable by decoupling the algorithm or behaviour from the client that uses it. Node.js supports the strategy pattern by using functions, objects, classes, or other techniques to encapsulate different algorithms or behaviours and pass them as parameters or arguments to other functions or objects.

Binding and evaluation strategies are two important aspects of Node.js functional programming that affect how functions are applied to arguments and how expressions are evaluated. Binding refers to the process of associating a function with some of its arguments, creating a new function that expects the remaining arguments. This technique is also known as partial application or currying. It can help create reusable and composable functions that can be easily adapted to different contexts.

For example, suppose we have a function that adds two numbers:

const add = (a, b) => a + b;

We can bind this function with one argument, say 5, and create a new function that adds 5 to any number:

const add5 = add.bind(null, 5); // null is the context, which is irrelevant her

add5(10); // 15

add5(20); // 25e

We can also use libraries like lodash or ramda that provide curried versions of their functions, or create our own currying functions.

The advantage of binding is that it allows us to create more specific and expressive functions from more general ones, without repeating code or logic. It also enables us to use higher-order functions, which are functions that take other functions as arguments or return them as results. Higher-order functions can help us abstract common patterns and behaviours, such as mapping, filtering, reducing, composing, etc.

The disadvantage of binding is that it can make the code less readable and intuitive, especially for developers who are not familiar with functional programming. It can also introduce some performance overhead due to the creation of extra functions and closures.

Eager evaluation means that expressions are evaluated as soon as they are bound to a variable or passed as an argument to a function. This is the default strategy in most programming languages, including JavaScript.

For example, suppose we have a function that logs a message and returns a number:

const logAndReturn = (msg, n) => {

 console.log(msg);

 return n;

};

If we pass this function as an argument to another function, such as Math.max, the message will be logged before the Math.max function is executed:

Math.max(logAndReturn('A', 10), logAndReturn('B', 20)); // A B 20

The advantage of eager evaluation is that it is simple and predictable. It ensures that expressions are evaluated in the order they appear in the code, and that side effects (such as logging) are executed immediately.

The disadvantage of eager evaluation is that it can be wasteful and inefficient. It can evaluate expressions that are not needed for the final result, or evaluate them more than once. It can also cause infinite loops or stack overflows if the expressions are recursive or depend on each other.

Lazy evaluation means that expressions are evaluated only when they are needed for the final result. This can be achieved by using thunks, which are functions that wrap an expression and delay its evaluation until they are called.

For example, suppose we have a function that creates a thunk from an expression:

const thunk = (expr) => () => expr;

We can use this function to wrap our logAndReturn function and pass it as an argument to another function, such as Math.max. The message will not be logged until the thunk is called by the Math.max function:

Math.max(thunk(logAndReturn('A', 10)), thunk(logAndReturn('B', 20))); // B 20

Notice that only ‘B’ is logged, because the thunk with ‘A’ is never called.

The advantage of lazy evaluation is that it can be more efficient and elegant. It can avoid unnecessary computations, reuse intermediate results, and handle infinite or recursive data structures.

The disadvantage of lazy evaluation is that it can be more complex and unpredictable. It can change the order of evaluation and side effects, making the code harder to debug and reason about. It can also introduce memory leaks if thunks are not garbage collected properly.

Another way to achieve lazy evaluation in Node.js is by using generators, which are special functions that can produce a sequence of values over time.

Generators are declared with the function* syntax and use the yield keyword to pause and resume their execution. When a generator is called, it returns an iterator, which is an object that has a next method that returns the next value of the sequence. The iterator also has a done property that indicates whether the generator has finished producing values or not.

For example, suppose we have a generator function that produces the Fibonacci sequence, which is a series of numbers where each number is the sum of the previous two:

function* fibonacci() {

 let a = 0;

 let b = 1;

 while (true) {

   yield a;

   [a, b] = [b, a + b];

 }

}

This generator function will never end, because it has an infinite loop. However, we can use lazy evaluation to get only the values we need from it. For example, we can use a for...of loop to iterate over the first 10 values of the sequence:

const fib = fibonacci(); // returns an iterato

for (let i = 0; i < 10; i++) {

 console.log(fib.next().value); // logs 0, 1, 1, 2, 3, 5, 8, 13, 21, 34

}r

Notice that the generator function is only evaluated when we call next() on the iterator. This way, we avoid unnecessary computations and memory allocation.

The advantage of using generators for lazy evaluation is that they are built-in features of JavaScript and Node.js, and they are easy to use and understand. They also allow us to create and consume infinite or recursive data structures without causing stack overflows or memory leaks.

The disadvantage of using generators for lazy evaluation is that they are not compatible with some older versions of JavaScript and Node.js, and they may require transpilation or polyfills to work in some environments. They also have some limitations when working with asynchronous code, such as promises or callbacks.

In conclusion, binding and evaluation strategies are important concepts in functional programming that have trade-offs between simplicity and efficiency, readability and expressiveness, predictability and elegance. Node.js supports both strategies with its built-in features and external libraries, allowing developers to choose the best approach for their applications.



Concluding Remarks

In my previous articles I have often said that there is no tool that is “generally best” or best for everything. There is no best game engine, no best programming language, no best code editor. It depends on what you are developing. It is same with algorithms and data structures, and so it is with evaluations strategies, too. You need to know their pros and cons in different situations.

Each evaluation strategy and parameter passing method has it advantages and disadvantages, many of them listed and explained above. But you also need to know and understand expression binding strategy and order, evaluation order, concepts like inlining, coroutines, concurrency and many others. Each programming language has its own peculiarities, and you should not blindly think that pass-by-value is really that. This article has just scratched the surface, there are even more details waiting, and every programming language is in transition. New features, and strategies, are introduced, and old ones are deprecated.

Knowing and understanding all those details gives several advantages to developer. First, you are better equipped to avoid side effect type of bugs. Second, it is easier to optimize and avoid performance bottlenecks in execution. Third, they make a fascinating conversation subject in any social event, and will impress people beyond belief, making you very popular and successful. Fourth, reciting evaluation strategies is an easy way to put other people to sleep.

Thomas Hellstrom

Senior Full-Stack Engineer (Ruby/Go/Javascript) | 10+ years of experience

2mo

Oh my god, this is just pure gold!  Clear, short, and at the same time thoughtful. I was passively looking for something like this for a long time. Thank you kindly for your input on this!

Like
Reply

To view or add a comment, sign in

Insights from the community

Others also viewed

Explore topics