Addressing some compile-time problems

Although REPL can help explore and tweak correct F# code, it keeps compiler errors intact, as evaluating a code snippet includes compilation by the F# compiler embedded into fsi. And I must admit that some compile-time errors may puzzle an inexperienced F# developer. Here, I will analyze several kinds of such errors and provide advice on how to get rid of them. Before I do this, you should keep in mind that because an initial defect usually gets ingested by type inference as correct code, the reported compilation error is in line with that convoluted type inference determination. That is, type inference often masks the authentic cause of an error. We will go over some occasions of this layout soon.

The if-then return value

One of the easiest-to-grasp occurrences of the similar convoluted determination takes place for the result type of F# if...then... expressions. Usually, it seems counterintuitive that this result cannot be anything but unit. Let's look at why this happens.

In the following snippet, I chose the specific (<) comparison operator within the implementation just to keep things simple (Ch13_1.fsx):

let f a b = 
    if a < b then 
        a 
    else 
        b 

Here, the inferred signature of function f representing the result of evaluating the F# expression if-then-else is f: 'a -> 'a -> 'a (requires comparison), which makes perfect sense (it should not take much effort to recognize a generic implementation of the min function in the preceding code).

Now let's look at what happens if I omit the else part (Ch13_1.fsx):

let f' a b = 
    if a < b then 
        a 

Now the inferred signature of f' is f': unit->unit->unit; in other words, both arguments and the result must be of type unit. What gives? The reasoning behind the seemingly counterintuitive type inference outcome is, in fact, to continue making perfect sense. Let's think what value the function f' must return when the condition a < b is false? The compiler, in the absence of explicit directions, decides that it must be unit. But wait a minute; shouldn't both branches of the if-then-else expression be of the same type? This condition can be only fulfilled if argument a is of type unit, which means argument b must be of type unit as well.

Fine; but what would happen if I try to push type inference into certain ways, for example, forcefully attempting a to be of generic type 'a (Ch13_1.fsx):

let f'' (a:'a) b = 
    if a < b then 
        a 

Or, what if we try pushing a in the direction of being less generic by forcing it to be of a concrete type, for example, int (Ch13_1.fsx):

let f''' (a:int) b = 
    if a < b then 
        a 

Turns out both attempts are futile, as consideration about the unit return type of the omitted else branch is still valid. In the first case, the compiler will just make a nasty warning pointing that

This construct causes code to be less generic than indicated by the type annotations. The type variable 'a has been constrained to be type 'unit'.

In the second case, from a compiler standpoint, there's a plain and simple error

This expression was expected to have type unit but here has type int.

So, how should we handle the if...then... expressions? The moral is that this short form of conditional statement may be used only in cases where a side-effect is needed. Good examples would be logging some diagnostics or changing a mutable value. For cases where a genuine non-unit result has to be returned, the full-blown if-then-else expression must be evaluated with both branches returning values of the same type.

Value restriction

This compile problem usually makes intermediate level F# developers who have grasped and proudly put to use F# features such as partial application and automatic generalization stumble. Imagine that you came out with a powerful data processing algorithm and are implementing it, enjoying the power and beauty of idiomatic F# code in the process. At some moment, you realize that you need a function that takes a list of lists and finds out whether all element lists are empty or not. Not a problem for a seasoned functional programmer like you, right? So you coin something like this (Ch13_2.fsx):

let allEmpty  = List.forall ((=) []) 

Surprise! It does not compile with the compiler warning:

Value restriction. The value 'allEmpty' has been inferred to have generic type val allEmpty : ('_a list list -> bool) when '_a:equality Either make the arguments to 'allEmpty' explicit or, if you do not intend for it to be generic, add a type annotation.

And you (I should admit I did this on multiple occasions in the past) first stare at this mess in disbelief as the F# compiler has accurately inferred your intent but somehow dislikes it. Then you Google "f# value restriction" and get referred to MSDN Automatic Generalization (https://docs.microsoft.com/en-us/dotnet/articles/fsharp/language-reference/generics/automatic-generalization), where you are told that:

The compiler performs automatic generalization only on complete function definitions that have explicit arguments, and on simple immutable values.

This is followed by practical recipes of working around a sudden problem. You try this advice and get the problem fixed, but you are left with the aftertaste of some black magic.

For me, the eye-opener was reading through this excellent blog post: Finer Points of F# Value Restriction (https://blogs.msdn.microsoft.com/mulambda/2010/05/01/finer-points-of-f-value-restriction/). I will demonstrate the hidden dangers of generalization applied to mutable values that may create a motive for you to read this blog post and understand the rationale behind the F# compiler behavior.

Let's look at a seemingly innocuous code fragment here (Ch13_2.fsx):

let gr<'a> : 'a list ref = ref [] 
gr := ["a"] 
let x: string list = !gr 
printfn "%A" x 

Guess what would be the printed x value upon this fragment execution? That would be ["a"], right?

Wrong; [] is what happens! The cause of this is that gr, despite appearing as a value of type 'a list ref, is, in fact, a type function. Used on the left-hand side of the := operator, it just brings a fresh unbound reference instance. Used on the right-hand side of operator !, it brings just another fresh reference instance that refers to an empty list, []. In order to achieve the intuitively expected behavior, we need to bind gr applied to the type argument string to the concrete typed variable cr, and then the latter, being a normal reference, will behave as expected (Ch13_2.fsx):

let cr = gr<string> 
cr := ["a"] 
let y = !cr 
printfn "%A" y 

Now the printed value is ["a"] indeed. Imposing a value restriction error in all cases where the situation deviates from the safest use case compiler protects developers from the surprise code behavior of the kind demonstrated earlier. Getting back to my initial sample, the possible remedial action can be any of the following (Ch13_2.fsx):

let allEmpty xs = List.forall ((=) []) xs // remedy 1 
let allEmpty : int list list -> bool  = List.forall ((=) []) 
// remedy 2 
let allEmpty = fun x -> List.forall ((=) []) x // remedy 3 

Imperfect pattern matching

Many seemingly counterintuitive F# compile-time errors and warnings belong to the field of pattern matching. For example, take a look the following naive detection of the integer argument sign:

let positive = function 
| x when x > 0 -> true 
| x when x <= 0 -> false 

Despite seeming completeness, this produces the compiler's warning:

incomplete pattern matches on this expression

Turns out that the F# compiler, if presented with the when guards, assumes that this construction designates an incomplete match case by definition. This is why regardless of the fact that the given set of cases is semantically complete, the compiler considers the function definition incomplete. Simply removing the excessive last when guard immediately fixes the problem (Ch13_3.fsx):

let positive' = function 
| x when x > 0 -> true 
| _ -> false 

Another common related problem is unreachable matching rules. Most of the time, unreachable matching rules get into play when the programmer mistakenly uses a variable instead of a literal in the sequence of rules, creating a premature catch-all case. In such cases, the compiler uses benign warnings, although almost always, the run-time results are messed-up. Therefore, perhaps denoting these occasions as errors would be a better design choice. A couple of years ago I wrote a blog post (https://infsharpmajor.wordpress.com/2011/10/13/union-matching-challenge/) on this matter, which I reproduce here as the illustration of the problem in the following snippet (Ch13_3.fsx):

type TickTack = Tick | Tack 
 
let ticker x = 
    match x with 
    | Tick -> printfn "Tick" 
    | Tock -> printfn "Tock" 
    | Tack -> printfn "Tack" 
ticker Tick 
ticker Tack 

This may trick you into expecting the Tick output followed by Tack, but in fact, the Tick output is to be followed by Tock!

The F# compiler issues two warnings for the preceding fragment. The first warning prompts you that typo Tock is taken as a variable and not a literal, like literals of type TickTack in two other cases:

Uppercase variable identifiers should not generally be used in patterns, and may indicate a misspelt pattern name

The second warning:

This rule will never be matched

directly indicates the outcome caused by the typo.

The moral here is that the F# developer should be attentive to warnings. Treating rule unreachability by the compiler as an error would be more adequate perhaps.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset