F # - Почему Seq.map не распространяет исключения?

Представьте себе следующий код:

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

Теперь давайте проверим это:

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

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

Это работает, как я и ожидал. Но если мы изменим List на Seq в функции, мы получим другое поведение:

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

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

Здесь с keys2 я вижу сообщение об исключении в объекте values в отладчике, но исключение не выдается...

Почему это так? Мне нужна аналогичная логика в моем приложении, и я бы предпочел работать с последовательностями.


person psfinaki    schedule 11.08.2017    source источник
comment
Это связано с ленивой оценкой последовательностей. . Попробуйте let values = keys |> Seq.map (fun k -> d.Item k) |> Seq.toList.   -  person Funk    schedule 11.08.2017


Ответы (1)


Это классический пример проблемы с побочными эффектами и отложенным вычислением. Seq функции, такие как Seq.map, оцениваются лениво, это означает, что результат Seq.map не будет вычисляться до тех пор, пока возвращаемая последовательность не будет пронумерована. В вашем примере этого никогда не происходит, потому что вы никогда ничего не делаете с values.

Если вы форсируете оценку последовательности, создавая конкретную коллекцию, например list, вы получите свое исключение, и функция вернет false:

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

Как вы заметили, использование List.map вместо Seq.map также решает вашу проблему, поскольку при вызове он будет с готовностью оцениваться, возвращая новый конкретный list.

Ключевой вывод заключается в том, что вы должны быть очень осторожны, сочетая побочные эффекты с ленивой оценкой. Вы не можете полагаться на эффекты, происходящие в том порядке, в котором вы изначально ожидаете.

person TheInnerLight    schedule 11.08.2017
comment
Да, так что дело не только в F#, но и в ленивом вычислении в целом (я только что попробовал это на своем родном языке C#). Почитал немного, теперь понятно. Благодарю вас! - person psfinaki; 11.08.2017
comment
@psfinaki Да, вы можете напрямую перевести это на C # с помощью IEnumerable/LINQ, и вы получите точно такое же поведение. - person TheInnerLight; 11.08.2017
comment
Я думаю, здесь важно отметить, что основной причиной этого сбоя является то, что программа полагается на неявные побочные эффекты (здесь — на знание того, что d.Item вызовет исключение) вместо явного кодирования намерения. - person Fyodor Soikin; 11.08.2017
comment
@FyodorSoikin Я вроде как понимаю, к чему вы клоните, но я бы сказал, что все побочные эффекты неявны по определению. F# async и Haskell IO являются примерами вещей, которые я бы отнес к категории явных эффектов, и этот пример можно безопасно построить с использованием такого подхода. Помимо этого, не так много решений, кроме как быть очень осторожным при сочетании побочных эффектов и ленивой оценки или просто полностью избегать этого. - person TheInnerLight; 11.08.2017
comment
Проблема заключается не в наличии неявных побочных эффектов как таковых, а в том, что программа полагается на них для своих вычислений. - person Fyodor Soikin; 11.08.2017