Signature

From void to unit

Issues with void in C♯

void needs to be handle separately = 2 times more work 😠

  • 2 types of delegates: Action vs Func<T>

  • 2 types od task: Task vs Task<T>

Exemple : ITelemetry

interface ITelemetry
{
  void Run(Action action);
  T Run<T>(Func<T> func);

  Task RunAsync(Func<Task> asyncAction);
  Task<T> RunAsync<T>(Func<Task<T>> asyncFunc);
}

🔗 Mads Torgersen is the C#'s Lead Designer. In his interview by Nick Chapsas, at 1:21:25, he indicates the 3 features he would completely remove from C#: event, delegate and void!

Type Void

💡 What about creating a Void type, a singleton with no data in it:

public class Void
{
    public static readonly Void Instance = new Void();

    private Void() {}
}

Let's play with it...

ITelemetry simplification

First, let's define the following helpers to convert to Void :

public static class VoidExtensions
{
    // Action -> Func<Void>
    public static Func<Void> AsFunc(this Action action)
    {
        action();
        return Void.Instance;
    }

    // Func<Task> -> Func<Task<Void>>
    public async static Func<Task<Void>> AsAsyncFunc(this Func<Task> asyncAction)
    {
        await asyncAction();
        return Void.Instance;
    }
}

Then, we can write a default implementation (C♯ 8) for 2 of 4 methods:

interface ITelemetry
{
    void Run(Action action) =>
        Run(action.AsFunc());

    T Run<T>(Func<T> func);

    Task RunAsync(Func<Task> asyncAction) =>
        RunAsync(asyncAction.AsAsyncFunc());

    Task<T> RunAsync<T>(Func<Task<T>> asyncFunc);
}

Type unit

In F♯ theVoid type exists! It's called unit because it has only one instance, written() and that we use like any other literal.

Impact on the function signature

  • Rather than void functions, we have functions returning the unit type.

  • Rather than functions with 0 parameter, we have functions taking a unit parameter that can only be ().

let doNothing () = () // unit -> unit
let now () = System.DateTime.Now // unit -> DateTime

ignore function

Remember : in F♯, everything is an expression. → No value is ignored, except ()/unit designed for this purpose → At the beginning of an expression or between several let bindings, we can insert unit expressions worth ()/unit, for example printf "mon message"

Issue: we call a function which triggers a side-effect but also returns a value we are not interested in.

Example: save is a function that saves to the database and returns true or false

let save entity = true // Fake implementation

let problem =
    save "hello"
//  ~~~~~~~~~~~~ ⚠️
// Warning FS0020: The result of this expression has type 'bool' and is implicitly ignored.
// Consider using 'ignore' to discard this value explicitly, e.g. 'expr |> ignore',
// or 'let' to bind the result to a name, e.g. 'let result = expr'.

    "ok" // Random final value just for demo purpose

Solution 1 : discard the returned value

let solution1 =
    let _ = save "hello" // 👌
    "ok"

Solution 2 : use the built-in ignore function, that has this signature :'a -> unit → Whatever the value supplied as parameter, it ignores it and returns ().

let solution2 =
    save "hello" |> ignore // 👍
    "ok"

Other examples:

// Side-effects / file system
System.IO.Directory.CreateDirectory folder |> ignore
// Ignore the returned DirectoryInfo

// Fluent builder:
let configureAppConfigurationForEnvironment (env: Environment) (basePath: string) (builder: IConfigurationBuilder) =
    builder
        .SetBasePath(basePath)
        .AddJsonFile("appsettings.json", optional = false, reloadOnChange = true)
        .AddJsonFile($"appsettings.{env.name}.json", optional = false, reloadOnChange = true)
    |> ignore

// Event handler:
textbox.onValueChanged(ignore)

⚠️ Trap: ignoring a value that we should use in our program.

[ 1..5 ]
|> Seq.map save
|> ignore // 💣

// Expected
[ 1..5 ]
|> Seq.iter (save >> ignore)

Arrow notation

  • 0-parameters function: unit -> TResult.

  • 1-parameter function: T -> TResult.

  • 2-parameters function: T1 -> T2 -> TResult * 3-parameter function: `T1 -> T2 -> T3 -> TResult

  • 3-parameters function: T1 -> T2 -> T3 -> TResult

Quiz

Do you know why we have -> between the parameters? What is the underlying concept?

Answer in the next page...

Last updated

Was this helpful?