Почему построитель вычислений seq не позволяет "let!"

Я заметил, что следующий код дает ошибку при попытке скомпилировать его:

let xx =
  seq {
    let! i = [ 1; 2 ]
    let! j = [ 3; 4 ]
    yield (i,j)
  }

Ошибка: "Ошибка FS0795: использование" let! x = coll "в выражениях последовательности больше не разрешено. Вместо этого используйте" для x в coll ". Это сообщение, конечно, понятно и демонстрирует, как его исправить; фиксированный код:

let xx =
  seq {
    for i in [ 1; 2 ] do
      for j in [ 3; 4 ] do
        yield (i,j)
  }

Мой вопрос заключается не в том, как это исправить, но почему "пусть!" не допускается в выражениях последовательности в первую очередь? Я вижу, как тот факт, что пусть! итерации над выражением могут стать неожиданностью для некоторых, но этого недостаточно, чтобы запретить конструкцию. Я также вижу, как "для" здесь более сильная, так как версия с "let!" испекает в рамках итерации как "до конца выражения последовательности".

Однако возможность перебора последовательности без кода отступа была именно тем, что я искал (для пересечения древовидных структур). Я предполагаю, что для получения этой семантики мне придется создать новый конструктор выражений, который действует в основном как построитель выражений "seq", но позволяет "let!" для итерации, не так ли?


Добавлено, основываясь на комментарии Брайана ниже, предоставляя решение моей основной проблемы:

Я не понял, что отступы в блоках не нужны, а второй образец можно переписать как:

let xx =
  seq {
    for i in [ 1; 2 ] do
    for j in [ 3; 4 ] do
    yield (i,j)
  }

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

let yy =
  seq {
    for i in [ 1; 2 ] do
    let i42 = i+42
    for j in [ 3; 4 ] do
    yield (i42,j)
  }

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

Ответ 1

Несколько недель назад я написал статью с доном Симом, которая пытается объяснить некоторые мотивы выбора синтаксиса в выражениях вычисления F # (такие как выражения последовательности, асинхронные рабочие процессы и другие). Вы можете найти его здесь. Это не дает однозначного ответа на ваш вопрос, но может помочь.

В общем случае, когда у вас есть тип M<'T> и вы определяете построитель вычислений, вы можете добавить методы For и Bind, чтобы включить синтаксис For и let!:

For  : seq<'T> -> ('T -> M<'T>) -> M<'T>
Bind : M<'T>   -> ('T -> M<'T>) -> M<'T>

Вход для For всегда должен быть некоторой последовательностью seq<'T>, а вход для Bind должен быть типом M<'T>, для которого вы его определяете.

В выражениях последовательности эти две операции будут иметь один и тот же тип, и поэтому они должны будут делать то же самое. Хотя выражения последовательности могут предоставить оба варианта, вероятно, неплохо было бы разрешить только одно, потому что использование двух разных ключевых слов для одной вещи было бы запутанным. Общий принцип заключается в том, что код в блоке comp { .. } должен вести себя как обычный код - и поскольку For существует в нормальном коде F #, имеет смысл использовать тот же синтаксис внутри выражения вычисления для seq<'T>.