4

Imagine the following code:

let d = dict [1, "one"; 2, "two" ]

let CollectionHasValidItems keys =
    try
        let values = keys |> List.map (fun k -> d.Item k)
        true
    with
        | :? KeyNotFoundException -> false

Now let us test it:

let keys1 = [ 1 ; 2 ]
let keys2 = [ 1 ; 2; 3 ]

let result1 = CollectionHasValidItems keys1 // true
let result2 = CollectionHasValidItems keys2 // false

This works as I would expect. But if we change List to Seq in the function, we get different behavior:

let keys1 = seq { 1 .. 2 } 
let keys2 = seq { 1 .. 3 }

let result1 = CollectionHasValidItems keys1 // true
let result2 = CollectionHasValidItems keys2 // true

Here with keys2 I can see the exception message within values object in the debugger but no exception is thrown...

Why is it like this? I need some similar logic in my app and would prefer to work with sequences.

psfinaki
  • 1,814
  • 15
  • 29
  • 3
    It's because of the [lazy evaluation](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/lazy-computations) of sequences. Try `let values = keys |> Seq.map (fun k -> d.Item k) |> Seq.toList`. – Funk Aug 11 '17 at 11:08

1 Answers1

6

This is a classic example of a problem with side effects and lazy evaluation. Seq functions such as Seq.map are lazily evaluated, that means that the result of Seq.map will not be computed until the returned sequence is enumerated. In your example, this never occurs because you never do anything with values.

If you force the evaluation of the sequence by generating a concrete collection, like a list, you will get your exception and the function will return false:

let CollectionHasValidItems keys =
    try
        let values = keys |> Seq.map (fun k -> d.Item k) |> Seq.toList
        true
    with
        | :? System.Collections.Generic.KeyNotFoundException -> false

As you've noticed, using List.map instead of Seq.map also resolves your issue because it will be eagerly evaluated when called, returning a new concrete list.

The key takeaway is, you have to be really careful about combining side effects with lazy evaluation. You can't rely on effects happening in the order that you initially expect.

TheInnerLight
  • 12,034
  • 1
  • 29
  • 52
  • 1
    Right, so it is not just about F# but about lazy evaluation in general (I have just tried it in my mother tongue C#). I have read up a little bit, now it makes sense. Thank you! – psfinaki Aug 11 '17 at 11:30
  • @psfinaki Yep, you can directly translate this into C# with `IEnumerable`/LINQ and you will get exactly the same behaviour. – TheInnerLight Aug 11 '17 at 12:34
  • I think it's important to note here that the root reason for this failure is that the program _relies_ on implicit side effects (here, the knowledge that `d.Item` will throw an exception) instead of encoding the intent explicitly. – Fyodor Soikin Aug 11 '17 at 13:51
  • 2
    @FyodorSoikin I sort of see what you're getting at but I would argue that all side effects are implicit by definition. F# `async` and Haskell `IO` are examples of things I'd put into the category of explicit effects and this example could be constructed safely using such an approach. Outside of that, there aren't a lot of solutions besides being very careful when combining side effects and lazy evaluation or simply avoiding it entirely. – TheInnerLight Aug 11 '17 at 14:13
  • The problem is not the presence of implicit side effects as such, but that the program _relies_ on them for its computation. – Fyodor Soikin Aug 11 '17 at 14:17