F# Conditional Expressions if...then..else returning unit or ()
Asked Answered
O

4

8

F#'s Condtional Expressions require a condition to check, a branch for true, and a branch for false. For example:

let x = 
    if ("hello" = null) 
    then true
    else false //error if else branch missing

However, something gets weird when unit, aka (), is involved.

let y = 
    if ("hello" = null) 
    then raise <| new ArgumentNullException()
    else () //happy with or without else branch

And more simply:

let z = 
    if ("hello" = null) 
    then ()
    else () //happy with or without else branch

Why isn't anelse branch required when unit is returned?

Orta answered 13/7, 2018 at 13:36 Comment(0)
C
12

Consider the following code:

let f a = if a > 5 then true

If you call f 10, it returns true.

Now, ask yourself this: what should f 2 return? I know you're going to say false, but how does the compiler know that? I mean, it's just as likely that you meant it to return true in both cases, isn't it? Or even, perhaps, crash in the a <= 5 case, who knows?

So in order for the program to be "complete" (i.e. contain instructions for what to do in every situation), you always have to specify an else branch.


unit, however, is special.

Returning unit means that there is no meaningful return value. Essentially unit stands for side-effect: it means that the thing that returned it was meant to produce some effect in the external world. Since F# is not a pure language, such unit-returning things are quite ubiquitous. For example, debug logging:

let f x =
    if x < 42 then printfn "Something fishy, x = %d" x
    x + 5

With such statements, there is no ambiguity: it's always known that the else branch is meant to return () as well. After all, there are no other values of unit, are there? At the same time, always adding else () at the end would be very tiresome and obfuscating. So, in the interest of usability, the compiler doesn't require an else branch in this specific case.

Columbic answered 13/7, 2018 at 14:31 Comment(1)
When unit is involved "always adding else () at the end would be very tiresome and obfuscating". I agree, I just expected that would be the decision made - I'm glad it isn't!Orta
C
5

In F# if is an expression and not a statement. Every expression needs to return a value. And both if and else need to return same type of value, because F# is strongly typed language. So if there is no else branch then by default it has type unit, but if your if returns a value with a type other thanunit, then you need to have an else with the same type.

Cindelyn answered 13/7, 2018 at 16:44 Comment(0)
M
2

The types of the values produced in each branch must match. If there is no explicit else branch, its type is unit. Therefore, if the type of the then branch is any type other than unit, there must be an else branch with the same return type.

If you remove else from the first snippet, it's equivalent to

let x = 
    if ("hello" = null) 
    then true
    else ()

which doesn't typecheck.

Why? I'd guess for compatibility with OCaml.

The else expr3 part can be omitted, in which case it defaults to else (). (7.7.2)

You can think of unit-returning if as an equivalent to the C-like if statement as opposed to the expression.

Meadow answered 13/7, 2018 at 13:43 Comment(6)
It has nothing to do with compatibility with OCaml.Columbic
In the sense that "if OCaml didn't allow omitting else (like SML doesn't), would F#'s designers allow it?" I would expect the answer to be "no, they wouldn't".Meadow
While that may be true, it's not the root of the rationale for this behavior. If you ignore the relationship with OCaml, there's still a reason for the use of if without else, as Fyodor's answer explains.Porphyroid
Plus, there are plenty of things in F# that were added to reduce repetitiveness and don't exist in OCaml, e.g. lightweight syntax.Columbic
It's a question of why F# in particular does this vs why languages with if-expressions tend to do this (even if not always). My best evidence for tradition over principle is that the same reason explained by Fyodor should apply to unit-returning pattern matches: why isn't always having to add | _ -> () "very tiresome and obfuscating"?Meadow
Because it's not nearly as ubiquitous.Columbic
C
2

Question already answered so I am just going to follow-up with my own superficial observations

In F# we don't have statements and expressions. In F# we compose computations using expressions alone. This is a good thing.

In C# where if is a statement this makes sense

if(x)
{
  return 1;
}

If x is true the execution is stopped and we return to the caller the result: 1.

In F# where if an expression it means that if is supposed to produce a value regardless what branch has been taken. Therefore this makes little sense

if x then 1 // What value should else branch evaluate to?

We have to specify both branches

if x then 1 else 0

As the other answers indicate there is a special case when else is omitted. The if expression will produce a unit value, the closest thing we get to a statement in F#. Because the type of the if expression is unit the "true" branch must be of type unit.

if x then ()         // This works
if x then () else () // This works
if x then 1          // This won't work, type mismatch
Chronogram answered 13/7, 2018 at 16:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.