Почему интерпретации списка Haskell с несколькими генераторами трактуют самый правый генератор как самый жесткий цикл?

Я читаю Gentle Introduction и задаюсь вопросом, почему в понимании списка с двумя генераторами самый правый генератор повторяется "самый быстрый" (т.е. компилируется в самый внутренний цикл, я думаю). Обратите внимание на следующий выход GHCi:

*Main> concat [[(x,y) | x <- [0..2]] | y <- [0..2]]
[(0,0),(1,0),(2,0),(0,1),(1,1),(2,1),(0,2),(1,2),(2,2)]
*Main> [(x,y) | x <- [0..2], y <- [0..2]]
[(0,0),(0,1),(0,2),(1,0),(1,1),(1,2),(2,0),(2,1),(2,2)]

Если самый левый генератор был истребитель быстрее всего, два вышеуказанных выражения имели бы то же значение, которое, я думаю, делает выбор этого соглашения более естественным.

Так кто-нибудь знает, почему было выбрано противоположное соглашение? Я заметил, что у Python есть такое же соглашение, что и Haskell (возможно, даже заимствовано у Haskell?), А в мире Python слово кажется, что упорядочение был выбран "потому что тот порядок, в котором вы пишете цикл for", но я понимаю, что мышление с точки зрения циклов не совсем то, что делают большинство программистов Haskell...

Мысли?


Из моего комментария к Луи Вассерману ответ ниже:

Я предполагаю, что порядок, соответствующий экспликации понимания императивного стиля, считался более естественным, чем его соответствие с вложением списка. По сути, объяснение Хаскелла для этого такое же, как объяснение Python, которое я связал в вопросе, в конце концов, похоже.

Ответ 1

Таким образом, объем объектов разумным образом.

[(x, y) | x <- [1..10], y <- [1..x]]

имеет смысл - x имеет смысл для понимания на y - но

[(x, y) | y <- [1..x], x <- [1..10]]

делает несколько меньше смысла.

Кроме того, этот способ согласуется с синтаксисом монады do:

do x <- [1..10]
   y <- [1..x]
   return (x, y)

Ответ 2

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

[ (x,y) | x <- [1,2,3], y <- [x+1,x+2] ]

Это расширяется до

do x <- [1,2,3]
   y <- [x+1,x+2]
   return (x,y)

который расширяется до

[1,2,3] >>= \x ->
[x+1,x+2] >>= \y -> 
return (x,y)

который дает понять, что x находится в области действия, когда это необходимо.

Если разложение в знак do произошло справа налево, а не слева направо, то наше оригинальное выражение расширилось бы на

[x+1,x+2] >>= \y ->
[1,2,3] >>= \x ->
return (x,y)

что явно бессмысленно - оно относится к значению x в области, где x еще не связано. Поэтому нам нужно написать наше оригинальное понимание как

[ (x,y) | y <- [x+1,x+2], x <- [1,2,3] ]

чтобы получить желаемый результат, что кажется неестественным - в то время, когда ваш глаз просматривает фразу y <- [x+1,x+2], вы действительно не знаете, что такое x. Вам нужно будет прочитать понимание назад, чтобы узнать.

Таким образом, не обязательно, чтобы привязка самого права была развернута во "внутренний цикл", но имеет смысл, если вы считаете, что людям придется читать полученный код.

Ответ 3

На самом деле Python использует ту же структуру видимости, что и Haskell для понимания списков.

Сравните ваш Haskell:

*Main> concat [[(x,y) | x <- [0..2]] | y <- [0..2]]
[(0,0),(1,0),(2,0),(0,1),(1,1),(2,1),(0,2),(1,2),(2,2)]
*Main> [(x,y) | x <- [0..2], y <- [0..2]]
[(0,0),(0,1),(0,2),(1,0),(1,1),(1,2),(2,0),(2,1),(2,2)]

с этим Python:

>>> from itertools import chain
>>> list(chain(*[[(x,y) for x in range(3)] for y in range(3)]))
[(0, 0), (1, 0), (2, 0), (0, 1), (1, 1), (2, 1), (0, 2), (1, 2), (2, 2)]
>>> [(x,y) for x in range(3) for y in range(3)]
[(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2), (2, 0), (2, 1), (2, 2)]
>>>